diff --git a/lib/config-store.js b/lib/config-store.js index 78e891f..5cefcf3 100644 --- a/lib/config-store.js +++ b/lib/config-store.js @@ -86,8 +86,6 @@ const DEFAULTS = { history: [] }; -const MAX_HISTORY = 100; - class ConfigStore { constructor(app) { const dir = app && app.isPackaged @@ -253,9 +251,6 @@ class ConfigStore { return this._enqueueWrite(() => { const config = this.load(); config.history.push(entry); - if (config.history.length > MAX_HISTORY) { - config.history = config.history.slice(-MAX_HISTORY); - } return this._atomicWrite(JSON.stringify(config, null, 2)); }); } diff --git a/main.js b/main.js index 4db690d..2a75aff 100644 --- a/main.js +++ b/main.js @@ -123,6 +123,97 @@ function appendUploadLog(hoster, link, fileName) { } catch {} } +function flattenHistoryForExport(history) { + const rows = []; + const list = Array.isArray(history) ? history : []; + + for (const batch of list) { + const batchId = batch && batch.id ? String(batch.id) : ''; + const rawTs = batch && batch.timestamp ? String(batch.timestamp) : ''; + const parsedTs = rawTs ? new Date(rawTs) : null; + const batchTimestamp = parsedTs && !Number.isNaN(parsedTs.getTime()) + ? parsedTs.toISOString() + : rawTs; + const files = Array.isArray(batch && batch.files) ? batch.files : []; + + for (const file of files) { + const fileName = file && file.name ? String(file.name) : ''; + const filePath = file && file.path ? String(file.path) : ''; + const fileSize = Number.isFinite(Number(file && file.size)) ? Number(file.size) : ''; + const results = Array.isArray(file && file.results) ? file.results : []; + + if (results.length === 0) { + rows.push({ + batchId, + batchTimestamp, + fileName, + filePath, + fileSize, + hoster: '', + status: '', + link: '', + error: '' + }); + continue; + } + + for (const result of results) { + const link = result && (result.download_url || result.embed_url || result.file_code) + ? String(result.download_url || result.embed_url || result.file_code) + : ''; + rows.push({ + batchId, + batchTimestamp, + fileName, + filePath, + fileSize, + hoster: result && result.hoster ? String(result.hoster) : '', + status: result && result.status ? String(result.status) : '', + link, + error: result && (result.error || result.message) ? String(result.error || result.message) : '' + }); + } + } + } + + return rows; +} + +function toCsvCell(value) { + const text = value === null || value === undefined ? '' : String(value); + if (!/[",\r\n]/.test(text)) return text; + return `"${text.replace(/"/g, '""')}"`; +} + +function buildHistoryCsv(rows) { + const header = [ + 'Batch ID', + 'Batch Timestamp', + 'File Name', + 'File Path', + 'File Size Bytes', + 'Hoster', + 'Status', + 'Link', + 'Error' + ]; + const lines = [header.map(toCsvCell).join(',')]; + for (const row of rows) { + lines.push([ + row.batchId, + row.batchTimestamp, + row.fileName, + row.filePath, + row.fileSize, + row.hoster, + row.status, + row.link, + row.error + ].map(toCsvCell).join(',')); + } + return `${lines.join('\n')}\n`; +} + // --- Multi-account helpers --- function hosterAccountHasCreds(name, account) { if (!account) return false; @@ -571,6 +662,43 @@ ipcMain.handle('get-history', () => { return configStore.loadHistory(); }); +ipcMain.handle('export-history', async (_event, format) => { + const normalizedFormat = String(format || 'csv').toLowerCase() === 'json' ? 'json' : 'csv'; + const history = configStore.loadHistory(); + const rows = flattenHistoryForExport(history); + const datePrefix = new Date().toISOString().slice(0, 10); + + const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, { + title: 'Upload-Verlauf exportieren', + defaultPath: `upload-history-${datePrefix}.${normalizedFormat}`, + filters: normalizedFormat === 'json' + ? [{ name: 'JSON-Datei', extensions: ['json'] }] + : [{ name: 'CSV-Datei', extensions: ['csv'] }] + }); + + if (canceled || !filePath) return { ok: false, canceled: true }; + + if (normalizedFormat === 'json') { + const payload = { + exportedAt: new Date().toISOString(), + totalBatches: Array.isArray(history) ? history.length : 0, + totalRows: rows.length, + history + }; + fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf-8'); + } else { + fs.writeFileSync(filePath, buildHistoryCsv(rows), 'utf-8'); + } + + return { + ok: true, + path: filePath, + format: normalizedFormat, + totalBatches: Array.isArray(history) ? history.length : 0, + totalRows: rows.length + }; +}); + ipcMain.handle('run-health-check', async (_event, payload) => { const config = configStore.load(); const hosters = payload && Array.isArray(payload.hosters) ? payload.hosters : []; diff --git a/preload.js b/preload.js index 753d29b..6affa90 100644 --- a/preload.js +++ b/preload.js @@ -6,6 +6,7 @@ contextBridge.exposeInMainWorld('api', { saveConfig: (config) => ipcRenderer.invoke('save-config', config), getHistory: () => ipcRenderer.invoke('get-history'), clearHistory: () => ipcRenderer.invoke('clear-history'), + exportHistory: (format) => ipcRenderer.invoke('export-history', format), // Hoster settings getHosterSettings: () => ipcRenderer.invoke('get-hoster-settings'), diff --git a/renderer/app.js b/renderer/app.js index 26d11e1..59c77dc 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -2934,6 +2934,26 @@ async function loadHistory() { renderHistoryTable(container); } +async function exportHistory() { + const history = await window.api.getHistory(); + if (!history || history.length === 0) { + alert('Kein Verlauf zum Exportieren vorhanden.'); + return; + } + + const asCsv = confirm('Verlauf als CSV exportieren?\n\nOK = CSV\nAbbrechen = JSON'); + const format = asCsv ? 'csv' : 'json'; + const result = await window.api.exportHistory(format); + + if (!result || result.canceled) return; + if (!result.ok) { + alert(result.error || 'Export fehlgeschlagen.'); + return; + } + + showCopyToast(`Verlauf exportiert (${result.totalRows || 0} Zeilen)`); +} + function sortRecentFiles(data) { const sorted = data.slice(); const { key, direction } = recentSortState; @@ -3193,6 +3213,7 @@ function setupListeners() { await window.api.clearHistory(); loadHistory(); }); + document.getElementById('exportHistoryBtn').addEventListener('click', exportHistory); // Auto health check toggle const autoToggle = document.getElementById('autoHealthCheckToggle'); diff --git a/renderer/index.html b/renderer/index.html index 997f820..803e789 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -227,7 +227,10 @@

Upload-Verlauf

- +
+ + +
diff --git a/tests/config-store.test.js b/tests/config-store.test.js index 82e4146..6bae85c 100644 --- a/tests/config-store.test.js +++ b/tests/config-store.test.js @@ -93,14 +93,14 @@ describe('ConfigStore', () => { assert.equal(config.hosterSettings['doodstream.com'].retries, 10); // preserved }); - it('appendHistory adds entries and caps at 100', async () => { + it('appendHistory keeps complete history without truncation', async () => { for (let i = 0; i < 105; i++) { await store.appendHistory({ id: `batch-${i}`, timestamp: new Date().toISOString(), files: [] }); } const history = store.loadHistory(); - assert.equal(history.length, 100); - assert.equal(history[0].id, 'batch-5'); // first 5 dropped - assert.equal(history[99].id, 'batch-104'); + assert.equal(history.length, 105); + assert.equal(history[0].id, 'batch-0'); + assert.equal(history[104].id, 'batch-104'); }); it('clearHistory empties the array', async () => {