add export data button to user settings, this lets users export their uploads/favorites at will.
This commit is contained in:
@@ -1397,4 +1397,117 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ==== Export Data Logic ====
|
||||
const btnStartExport = document.getElementById('btn-start-export');
|
||||
const exportProgressContainer = document.getElementById('export-progress-container');
|
||||
const exportProgressBar = document.getElementById('export-progress-bar');
|
||||
const exportStatusText = document.getElementById('export-status-text');
|
||||
const chkExportUploads = document.getElementById('export_uploads');
|
||||
const chkExportFavorites = document.getElementById('export_favorites');
|
||||
|
||||
if (btnStartExport) {
|
||||
btnStartExport.addEventListener('click', async () => {
|
||||
const exportUploads = chkExportUploads.checked;
|
||||
const exportFavorites = chkExportFavorites.checked;
|
||||
|
||||
if (!exportUploads && !exportFavorites) {
|
||||
alert('Please select at least one option to export.');
|
||||
return;
|
||||
}
|
||||
|
||||
btnStartExport.disabled = true;
|
||||
exportProgressContainer.style.display = 'block';
|
||||
exportProgressBar.style.width = '0%';
|
||||
exportStatusText.textContent = 'Fetching data list...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/settings/export-data');
|
||||
const data = await res.json();
|
||||
|
||||
let filesToDownload = [];
|
||||
if (exportUploads) {
|
||||
filesToDownload = filesToDownload.concat(data.uploads.map(f => ({ ...f, exportType: 'uploads' })));
|
||||
}
|
||||
if (exportFavorites) {
|
||||
filesToDownload = filesToDownload.concat(data.favorites.map(f => ({ ...f, exportType: 'favorites' })));
|
||||
}
|
||||
|
||||
if (filesToDownload.length === 0) {
|
||||
alert('No data found to export.');
|
||||
btnStartExport.disabled = false;
|
||||
exportProgressContainer.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
const uploadsFolder = zip.folder("uploads");
|
||||
const favoritesFolder = zip.folder("favorites");
|
||||
|
||||
const metadata = {
|
||||
exported_at: new Date().toISOString(),
|
||||
user: window.f0ckSession?.user,
|
||||
uploads: exportUploads ? data.uploads : [],
|
||||
favorites: exportFavorites ? data.favorites : []
|
||||
};
|
||||
zip.file("metadata.json", JSON.stringify(metadata, null, 2));
|
||||
|
||||
const total = filesToDownload.length;
|
||||
let completed = 0;
|
||||
|
||||
const downloadFile = async (fileInfo) => {
|
||||
const folder = fileInfo.exportType === 'uploads' ? uploadsFolder : favoritesFolder;
|
||||
|
||||
if (fileInfo.mime === 'video/youtube') {
|
||||
const ytId = fileInfo.dest.replace(/^yt:/, '');
|
||||
folder.file(`${fileInfo.id}_youtube_${ytId}.txt`, `https://www.youtube.com/watch?v=${ytId}`);
|
||||
completed++;
|
||||
const percent = Math.round((completed / total) * 100);
|
||||
exportProgressBar.style.width = percent + '%';
|
||||
exportStatusText.textContent = `Processing files: ${completed} / ${total}`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = (window.f0ckMediaBase || '/b') + '/' + fileInfo.dest;
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
folder.file(`${fileInfo.id}_${fileInfo.dest}`, blob);
|
||||
} catch (err) {
|
||||
console.error('Failed to download file:', fileInfo.id, err);
|
||||
} finally {
|
||||
completed++;
|
||||
const percent = Math.round((completed / total) * 100);
|
||||
exportProgressBar.style.width = percent + '%';
|
||||
exportStatusText.textContent = `Processing files: ${completed} / ${total}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Download in batches to avoid overwhelming the browser/server
|
||||
const batchSize = 5;
|
||||
for (let i = 0; i < filesToDownload.length; i += batchSize) {
|
||||
const batch = filesToDownload.slice(i, i + batchSize);
|
||||
await Promise.all(batch.map(downloadFile));
|
||||
}
|
||||
|
||||
exportStatusText.textContent = 'Generating ZIP file...';
|
||||
const content = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(content);
|
||||
link.download = `f0ckm_export_${new Date().toISOString().split('T')[0]}.zip`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
exportStatusText.textContent = 'Export complete!';
|
||||
btnStartExport.disabled = false;
|
||||
} catch (err) {
|
||||
console.error('Export failed:', err);
|
||||
alert('Export failed. See console for details.');
|
||||
btnStartExport.disabled = false;
|
||||
exportStatusText.textContent = 'Export failed.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -207,7 +207,13 @@
|
||||
"matrix_instructions": "1. Unten ein allgemeines Link-Token generieren.<br>2. <code>!link</code> im Hauptraum senden<br>3. Token in der Bot-DM eingeben",
|
||||
"your_token": "Dein Token:",
|
||||
"one_time_use": "Einmalig verwendbar.",
|
||||
"generate_token": "Link-Token generieren"
|
||||
"generate_token": "Link-Token generieren",
|
||||
"export_data_title": "Daten exportieren",
|
||||
"export_data_desc": "Lade eine Kopie deiner Daten herunter. Dieser Vorgang findet vollständig in deinem Browser statt.",
|
||||
"export_uploads": "Meine Uploads",
|
||||
"export_favorites": "Meine Favoriten",
|
||||
"export_preparing": "Wird vorbereitet...",
|
||||
"start_export": "Export generieren (ZIP)"
|
||||
},
|
||||
"filter": {
|
||||
"tag_placeholder": "Tag ausschließen",
|
||||
|
||||
@@ -207,7 +207,13 @@
|
||||
"matrix_instructions": "1. Generate a generic link token below.<br>2. Send <code>!link</code> in the general<br>3. Reply with your token in the bot dm",
|
||||
"your_token": "Your Token:",
|
||||
"one_time_use": "Valid for one-time use.",
|
||||
"generate_token": "Generate Link Token"
|
||||
"generate_token": "Generate Link Token",
|
||||
"export_data_title": "Export Data",
|
||||
"export_data_desc": "Download a copy of your data. This process happens entirely in your browser.",
|
||||
"export_uploads": "My Uploads",
|
||||
"export_favorites": "My Favorites",
|
||||
"export_preparing": "Preparing...",
|
||||
"start_export": "Generate Export (ZIP)"
|
||||
},
|
||||
"filter": {
|
||||
"tag_placeholder": "Tag to exclude",
|
||||
|
||||
@@ -207,7 +207,13 @@
|
||||
"matrix_instructions": "1. Generate a generic link token below.<br>2. Send <code>!link</code> in the general<br>3. Reply with your token in the bot dm",
|
||||
"your_token": "Je Token:",
|
||||
"one_time_use": "Valid for one-time use.",
|
||||
"generate_token": "Koppelingstoken Genereren"
|
||||
"generate_token": "Koppelingstoken Genereren",
|
||||
"export_data_title": "Gegevens exporteren",
|
||||
"export_data_desc": "Download een kopie van je gegevens. Dit proces vindt volledig plaats in je browser om je privacy te beschermen en serverbronnen te besparen.",
|
||||
"export_uploads": "Jouw uploads",
|
||||
"export_favorites": "Jouw favorieten",
|
||||
"export_preparing": "Voorbereiden...",
|
||||
"start_export": "Export genereren (ZIP)"
|
||||
},
|
||||
"filter": {
|
||||
"tag_placeholder": "Tag om uit te sluiten",
|
||||
|
||||
@@ -206,7 +206,13 @@
|
||||
"matrix_instructions": "1. Erzeugen Sie unten ein allgemeines Verknüpfungskennzeichen.<br>2. Senden Sie <code>!link</code> im Hauptraum<br>3. Antworten Sie mit Ihrem Kennzeichen in der Bot-Direktnachricht",
|
||||
"your_token": "Ihr Kennzeichen:",
|
||||
"one_time_use": "Gültig für den einmaligen Gebrauch.",
|
||||
"generate_token": "Verknüpfungskennzeichen erzeugen"
|
||||
"generate_token": "Verknüpfungskennzeichen erzeugen",
|
||||
"export_data_title": "Daten hinaustransportieren",
|
||||
"export_data_desc": "Laden Sie eine Kopie Ihrer Daten hinunter. Dieser Vorgang findet vollumfänglich in Ihrem Brauser statt.",
|
||||
"export_uploads": "Meine Aufladungen",
|
||||
"export_favorites": "Meine Favorisierungen",
|
||||
"export_preparing": "Vorbereitung wird getroffen...",
|
||||
"start_export": "Paket schnüren"
|
||||
},
|
||||
"filter": {
|
||||
"tag_placeholder": "Auszuschließendes Etikett",
|
||||
|
||||
@@ -58,6 +58,25 @@ export default (router, tpl) => {
|
||||
}, req)
|
||||
});
|
||||
});
|
||||
group.get('/export-data', auth, async (req, res) => {
|
||||
const uploads = await db`
|
||||
select id, dest, mime from items
|
||||
where username = ${req.session.user} and active = true
|
||||
order by id desc
|
||||
`;
|
||||
|
||||
const favorites = await db`
|
||||
select i.id, i.dest, i.mime from items i
|
||||
join favorites f on f.item_id = i.id
|
||||
where f.user_id = ${+req.session.id} and i.active = true
|
||||
order by f.item_id desc
|
||||
`;
|
||||
|
||||
res.reply({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ uploads, favorites })
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return router;
|
||||
|
||||
@@ -257,6 +257,35 @@
|
||||
@endif
|
||||
|
||||
</div>
|
||||
<h2>{{ t('settings.export_data_title') || 'Export Data' }}</h2>
|
||||
<div class="export-settings-wrapper" style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 4px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;">
|
||||
<p>{{ t('settings.export_data_desc') || 'Download a copy of your data. This process happens entirely in your browser to protect your privacy and save server resources.' }}</p>
|
||||
|
||||
<div class="setting-item" style="margin-bottom: 15px;">
|
||||
<label class="checkbox-container" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
||||
<input type="checkbox" id="export_uploads" checked>
|
||||
<span>{{ t('settings.export_uploads') || 'Your Uploads' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item" style="margin-bottom: 20px;">
|
||||
<label class="checkbox-container" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
||||
<input type="checkbox" id="export_favorites" checked>
|
||||
<span>{{ t('settings.export_favorites') || 'Your Favorites' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="export-progress-container" style="display: none; margin-bottom: 20px;">
|
||||
<div class="progress-bar-wrapper" style="height: 20px; background: rgba(255,255,255,0.1); border-radius: 10px; overflow: hidden; margin-bottom: 5px;">
|
||||
<div id="export-progress-bar" style="height: 100%; width: 0%; background: var(--accent); transition: width 0.3s;"></div>
|
||||
</div>
|
||||
<div id="export-status-text" style="font-size: 0.9em; color: var(--text-muted);">{{ t('settings.export_preparing') || 'Preparing...' }}</div>
|
||||
</div>
|
||||
|
||||
<button type="button" id="btn-start-export" class="button button-primary">
|
||||
<i class="fa-solid fa-download" style="margin-right: 5px;"></i> {{ t('settings.start_export') || 'Generate Export (ZIP)' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2>{{ t('settings.account') }}</h2>
|
||||
<div class="account-settings-wrapper"
|
||||
style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 4px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;">
|
||||
@@ -358,6 +387,7 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||
<script src="/s/js/settings.js?v=@mtime(/public/s/js/settings.js)"></script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user