From 173f9f9e563ef8191aa94cfc0b41b7ad3566870d Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Tue, 12 May 2026 18:49:28 +0200 Subject: [PATCH] add export data button to user settings, this lets users export their uploads/favorites at will. --- public/s/js/settings.js | 113 ++++++++++++++++++++++++++++++++++++ src/inc/locales/de.json | 8 ++- src/inc/locales/en.json | 8 ++- src/inc/locales/nl.json | 8 ++- src/inc/locales/zange.json | 8 ++- src/inc/routes/settings.mjs | 19 ++++++ views/settings.html | 30 ++++++++++ 7 files changed, 190 insertions(+), 4 deletions(-) diff --git a/public/s/js/settings.js b/public/s/js/settings.js index 8d7b589..97c7b9f 100644 --- a/public/s/js/settings.js +++ b/public/s/js/settings.js @@ -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.'; + } + }); + } + })(); diff --git a/src/inc/locales/de.json b/src/inc/locales/de.json index 9552ee2..2a156f0 100644 --- a/src/inc/locales/de.json +++ b/src/inc/locales/de.json @@ -207,7 +207,13 @@ "matrix_instructions": "1. Unten ein allgemeines Link-Token generieren.
2. !link im Hauptraum senden
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", diff --git a/src/inc/locales/en.json b/src/inc/locales/en.json index d557c55..8f62c09 100644 --- a/src/inc/locales/en.json +++ b/src/inc/locales/en.json @@ -207,7 +207,13 @@ "matrix_instructions": "1. Generate a generic link token below.
2. Send !link in the general
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", diff --git a/src/inc/locales/nl.json b/src/inc/locales/nl.json index cd0038e..a8ab557 100644 --- a/src/inc/locales/nl.json +++ b/src/inc/locales/nl.json @@ -207,7 +207,13 @@ "matrix_instructions": "1. Generate a generic link token below.
2. Send !link in the general
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", diff --git a/src/inc/locales/zange.json b/src/inc/locales/zange.json index b571acb..ccfa187 100644 --- a/src/inc/locales/zange.json +++ b/src/inc/locales/zange.json @@ -206,7 +206,13 @@ "matrix_instructions": "1. Erzeugen Sie unten ein allgemeines Verknüpfungskennzeichen.
2. Senden Sie !link im Hauptraum
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", diff --git a/src/inc/routes/settings.mjs b/src/inc/routes/settings.mjs index cb7521e..2e5a7ec 100644 --- a/src/inc/routes/settings.mjs +++ b/src/inc/routes/settings.mjs @@ -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; diff --git a/views/settings.html b/views/settings.html index 500d9f4..b19d240 100644 --- a/views/settings.html +++ b/views/settings.html @@ -257,6 +257,35 @@ @endif +

{{ t('settings.export_data_title') || 'Export Data' }}

+
+

{{ 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.' }}

+ +
+ +
+
+ +
+ + + + +
+

{{ t('settings.account') }}

@@ -358,6 +387,7 @@
@endif +