const HOSTERS = ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx']; let selectedFiles = []; // { path, name, size } let config = { hosters: {} }; let progressElements = new Map(); // uploadId -> DOM refs let uploading = false; let healthCheckRunning = false; const AUTO_CHECK_PREF_KEY = 'autoHealthCheckBeforeUpload'; let autoHealthCheckEnabled = true; const SORT_DEFAULT_DIRECTION = { date: 'desc', filename: 'asc', host: 'asc', link: 'asc' }; function getDefaultSortDirection(key) { return SORT_DEFAULT_DIRECTION[key] || 'asc'; } function formatDateTime(value) { const date = value instanceof Date ? value : new Date(value); const safeDate = Number.isNaN(date.getTime()) ? new Date() : date; return { ts: safeDate.getTime(), text: safeDate.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) + ' ' + safeDate.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) }; } // --- Init --- async function init() { config = await window.api.getConfig(); autoHealthCheckEnabled = loadAutoCheckPreference(); renderHosterChips(); renderSettings(); setHealthCheckStatus('Bereit fuer Check'); renderHealthCheckResults([]); setupListeners(); syncAutoCheckToggle(); setupDragDrop(); loadHistory(); } // --- Tab switching --- document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); tab.classList.add('active'); document.getElementById(`${tab.dataset.view}-view`).classList.add('active'); if (tab.dataset.view === 'history') loadHistory(); }); }); // --- Hoster chips on upload page --- function hosterHasCredentials(name, hoster) { if (name === 'vidmoly.me') return !!(hoster.username && hoster.password); return !!hoster.apiKey; } function renderHosterChips() { const container = document.getElementById('hosterSelect'); container.innerHTML = ''; for (const name of HOSTERS) { const hoster = config.hosters[name] || {}; const hasCreds = hosterHasCredentials(name, hoster); const chip = document.createElement('label'); chip.className = 'hoster-chip' + (hoster.enabled && hasCreds ? ' selected' : '') + (!hasCreds ? ' no-key' : ''); chip.innerHTML = ` ${name} `; chip.querySelector('input').addEventListener('change', (e) => { chip.classList.toggle('selected', e.target.checked); }); container.appendChild(chip); } } function getSelectedHosters() { return Array.from(document.querySelectorAll('#hosterSelect input:checked')) .map(cb => cb.dataset.hoster); } // --- File selection --- function setupDragDrop() { const dropZone = document.getElementById('dropZone'); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.remove('drag-over'); const files = Array.from(e.dataTransfer.files); for (const file of files) { if (!selectedFiles.find(f => f.path === file.path)) { selectedFiles.push({ path: file.path, name: file.name, size: file.size }); } } renderFileList(); }); dropZone.addEventListener('click', () => pickFiles()); } async function pickFiles() { const paths = await window.api.selectFiles(); if (!paths) return; for (const p of paths) { if (!selectedFiles.find(f => f.path === p)) { const name = p.split('\\').pop().split('/').pop(); selectedFiles.push({ path: p, name, size: 0 }); } } renderFileList(); } function renderFileList() { const container = document.getElementById('fileList'); const actions = document.getElementById('uploadActions'); if (selectedFiles.length === 0) { container.innerHTML = ''; actions.style.display = 'none'; document.getElementById('dropZone').classList.remove('hidden'); return; } document.getElementById('dropZone').classList.add('hidden'); actions.style.display = 'flex'; container.innerHTML = selectedFiles.map((f, i) => { const sizeStr = f.size > 0 ? formatSize(f.size) : ''; return `
${escapeHtml(f.name)} ${sizeStr}
`; }).join(''); container.querySelectorAll('.remove-btn').forEach(btn => { btn.addEventListener('click', () => { selectedFiles.splice(parseInt(btn.dataset.index), 1); renderFileList(); }); }); } function setupListeners() { document.getElementById('pickFilesBtn').addEventListener('click', (e) => { e.stopPropagation(); pickFiles(); }); document.getElementById('startUploadBtn').addEventListener('click', startUpload); document.getElementById('cancelUploadBtn').addEventListener('click', cancelUpload); document.getElementById('clearFilesBtn').addEventListener('click', () => { selectedFiles = []; renderFileList(); }); document.getElementById('copyAllLinksBtn').addEventListener('click', copyAllLinks); document.getElementById('newUploadBtn').addEventListener('click', resetUploadView); document.getElementById('saveSettingsBtn').addEventListener('click', saveSettings); document.getElementById('clearHistoryBtn').addEventListener('click', clearHistory); document.getElementById('runHealthCheckBtn').addEventListener('click', runHealthCheck); const autoToggle = document.getElementById('autoHealthCheckToggle'); if (autoToggle) { autoToggle.addEventListener('change', (e) => { autoHealthCheckEnabled = !!e.target.checked; saveAutoCheckPreference(autoHealthCheckEnabled); }); } // Upload progress events window.api.onUploadProgress(handleProgress); window.api.onUploadBatchDone(handleBatchDone); // Copy buttons (delegated) document.addEventListener('click', (e) => { if (e.target.classList.contains('copy-btn')) { const url = e.target.dataset.url; if (url) { window.api.copyToClipboard(url); e.target.textContent = 'Kopiert!'; e.target.classList.add('copied'); setTimeout(() => { e.target.textContent = 'Kopieren'; e.target.classList.remove('copied'); }, 1500); } } }); } function loadAutoCheckPreference() { try { const raw = window.localStorage.getItem(AUTO_CHECK_PREF_KEY); if (raw === null) return true; return raw === '1'; } catch { return true; } } function saveAutoCheckPreference(enabled) { try { window.localStorage.setItem(AUTO_CHECK_PREF_KEY, enabled ? '1' : '0'); } catch {} } function syncAutoCheckToggle() { const autoToggle = document.getElementById('autoHealthCheckToggle'); if (!autoToggle) return; autoToggle.checked = !!autoHealthCheckEnabled; } function setHealthCheckButtonBusy(isBusy, label) { const btn = document.getElementById('runHealthCheckBtn'); if (!btn) return; btn.disabled = !!isBusy; btn.textContent = isBusy ? (label || 'Pruefe...') : 'Hoster Check'; } function getHealthCheckHosters() { const selected = getSelectedHosters().filter(name => name === 'doodstream.com' || name === 'vidmoly.me'); if (selected.length > 0) return selected; return ['doodstream.com', 'vidmoly.me'] .filter((name) => hosterHasCredentials(name, config.hosters[name] || {})); } function normalizeHealthStatus(status) { if (status === 'ok' || status === 'warn' || status === 'error' || status === 'skipped') { return status; } return 'skipped'; } function healthStatusLabel(status) { if (status === 'ok') return 'OK'; if (status === 'warn') return 'WARN'; if (status === 'error') return 'ERR'; return 'SKIP'; } function setHealthCheckStatus(text) { const statusEl = document.getElementById('healthCheckStatus'); if (!statusEl) return; statusEl.textContent = text || ''; } function renderHealthCheckResults(results) { const container = document.getElementById('healthCheckResults'); if (!container) return; if (!results || results.length === 0) { container.innerHTML = ''; return; } container.innerHTML = results.map((item) => { const status = normalizeHealthStatus(item.status); const hoster = escapeHtml(item.hoster || 'unbekannt'); const message = escapeHtml(item.message || ''); const tag = healthStatusLabel(status); return `
${hoster} [${tag}] ${message}
`; }).join(''); } async function executeHealthCheck(hosters, mode) { const label = mode === 'auto' ? 'Auto-Check' : 'Check'; setHealthCheckStatus(`Pruefe ${hosters.join(', ')} ...`); renderHealthCheckResults([]); const result = await window.api.runHealthCheck({ hosters }); const rows = result && Array.isArray(result.results) ? result.results : []; renderHealthCheckResults(rows); const okCount = rows.filter((r) => r.status === 'ok').length; const warnCount = rows.filter((r) => r.status === 'warn').length; const errCount = rows.filter((r) => r.status === 'error').length; setHealthCheckStatus(`${label} fertig: ${okCount} OK, ${warnCount} Warnung, ${errCount} Fehler`); return rows; } async function runHealthCheck() { if (healthCheckRunning || uploading) return; const hosters = getHealthCheckHosters(); if (hosters.length === 0) { alert('Bitte doodstream.com und/oder vidmoly.me mit Zugangsdaten aktivieren.'); return; } healthCheckRunning = true; setHealthCheckButtonBusy(true, 'Pruefe...'); try { await executeHealthCheck(hosters, 'manual'); } catch (err) { setHealthCheckStatus('Health-Check fehlgeschlagen'); renderHealthCheckResults([{ hoster: 'system', status: 'error', message: err.message || 'Unbekannter Fehler' }]); } finally { healthCheckRunning = false; setHealthCheckButtonBusy(false); } } // --- Upload --- async function startUpload() { if (healthCheckRunning) { alert('Bitte warten, bis der laufende Hoster-Check fertig ist.'); return; } const hosters = getSelectedHosters(); if (hosters.length === 0) { alert('Bitte mindestens einen Hoster auswaehlen.'); return; } if (selectedFiles.length === 0) return; if (autoHealthCheckEnabled) { const checkHosters = hosters.filter((name) => name === 'doodstream.com' || name === 'vidmoly.me'); if (checkHosters.length > 0) { healthCheckRunning = true; setHealthCheckButtonBusy(true, 'Auto-Check...'); try { const rows = await executeHealthCheck(checkHosters, 'auto'); const errors = rows.filter((r) => r.status === 'error'); if (errors.length > 0) { const details = errors .map((r) => `${r.hoster || 'hoster'}: ${r.message || 'Fehler'}`) .join('\n'); alert(`Auto-Check fehlgeschlagen:\n${details}\n\nUpload wurde nicht gestartet.`); return; } } catch (err) { const msg = err && err.message ? err.message : 'Unbekannter Fehler'; setHealthCheckStatus('Auto-Check fehlgeschlagen'); renderHealthCheckResults([{ hoster: 'system', status: 'error', message: msg }]); alert(`Auto-Check fehlgeschlagen: ${msg}\nUpload wurde nicht gestartet.`); return; } finally { healthCheckRunning = false; setHealthCheckButtonBusy(false); } } } uploading = true; document.getElementById('uploadActions').style.display = 'none'; document.getElementById('cancelActions').style.display = 'flex'; document.getElementById('resultsSection').style.display = 'block'; const resultsTitle = document.getElementById('resultsTitle'); if (resultsTitle) resultsTitle.textContent = 'Ergebnisse (live)'; resetLiveResultsState(); renderResultsTable(); const newUploadBtn = document.getElementById('newUploadBtn'); if (newUploadBtn) newUploadBtn.disabled = true; buildProgressUI(selectedFiles, hosters); document.getElementById('progressSection').style.display = 'flex'; const result = await window.api.startUpload({ files: selectedFiles.map(f => f.path), hosters }); if (result && result.error) { alert(result.error); resetUploadView(); } } async function cancelUpload() { await window.api.cancelUpload(); uploading = false; document.getElementById('cancelActions').style.display = 'none'; } function resetUploadView() { uploading = false; selectedFiles = []; progressElements.clear(); resetLiveResultsState(); document.getElementById('fileList').innerHTML = ''; document.getElementById('progressSection').style.display = 'none'; document.getElementById('progressSection').innerHTML = ''; document.getElementById('resultsSection').style.display = 'none'; document.getElementById('cancelActions').style.display = 'none'; document.getElementById('uploadActions').style.display = 'none'; document.getElementById('dropZone').classList.remove('hidden'); const newUploadBtn = document.getElementById('newUploadBtn'); if (newUploadBtn) newUploadBtn.disabled = false; const resultsTitle = document.getElementById('resultsTitle'); if (resultsTitle) resultsTitle.textContent = 'Ergebnisse'; } // --- Progress UI --- function buildProgressUI(files, hosters) { const section = document.getElementById('progressSection'); section.innerHTML = ''; progressElements.clear(); for (const file of files) { const card = document.createElement('div'); card.className = 'progress-card'; let html = `
${escapeHtml(file.name)}
`; for (const hoster of hosters) { const uid = `${file.path}__${hoster}`; html += `
${hoster}
0% Warte...
`; } card.innerHTML = html; section.appendChild(card); } } function handleProgress(data) { // Find matching progress row const rows = document.querySelectorAll('.progress-row'); for (const row of rows) { const hoster = row.querySelector('.progress-hoster').textContent; const fileName = row.closest('.progress-card').querySelector('.file-title').textContent; if (hoster === data.hoster && fileName === data.fileName) { const fill = row.querySelector('.progress-fill'); const pct = row.querySelector('.progress-percent'); const stat = row.querySelector('.progress-status'); if (data.status === 'getting-server') { stat.textContent = 'Server...'; stat.className = 'progress-status'; } else if (data.status === 'uploading') { const percent = Math.round(data.progress * 100); fill.style.width = `${percent}%`; pct.textContent = `${percent}%`; stat.textContent = 'Uploading...'; stat.className = 'progress-status'; } else if (data.status === 'done') { fill.style.width = '100%'; fill.classList.add('done'); pct.textContent = '100%'; stat.textContent = 'Fertig'; stat.className = 'progress-status done'; } else if (data.status === 'error') { fill.classList.add('error'); fill.style.width = '100%'; pct.textContent = ''; stat.textContent = data.error || 'Fehler'; stat.className = 'progress-status error'; stat.title = data.error || 'Fehler'; } break; } } if (data && (data.status === 'done' || data.status === 'error')) { upsertLiveResultRow(data); } } function handleBatchDone(summary) { uploading = false; document.getElementById('cancelActions').style.display = 'none'; mergeSummaryIntoResults(summary); renderResultsTable(); const resultsTitle = document.getElementById('resultsTitle'); if (resultsTitle) resultsTitle.textContent = 'Ergebnisse'; const newUploadBtn = document.getElementById('newUploadBtn'); if (newUploadBtn) newUploadBtn.disabled = false; document.getElementById('resultsSection').style.display = 'block'; } // --- Results UI (table like z-o-o-m) --- let selectedRows = new Set(); let resultsRowsData = []; let resultsSortState = { key: 'date', direction: getDefaultSortDirection('date') }; let resultsOrderCounter = 0; let resultRowIndexByUploadId = new Map(); let historyRowsData = []; let historySortState = { key: 'date', direction: getDefaultSortDirection('date') }; function resetLiveResultsState() { selectedRows.clear(); resultsRowsData = []; resultsSortState = { key: 'date', direction: getDefaultSortDirection('date') }; resultsOrderCounter = 0; resultRowIndexByUploadId = new Map(); } function createResultRow({ dateTs, dateText, filename, host, link, isError, uploadId }) { return { date: dateText, dateTs, filename: filename || '', host: host || '', link: link || '', isError: !!isError, order: resultsOrderCounter++, uploadId: uploadId || null }; } function upsertLiveResultRow(data) { const { ts, text } = formatDateTime(new Date()); const result = data && data.result && typeof data.result === 'object' ? data.result : {}; const rowData = createResultRow({ dateTs: ts, dateText: text, filename: data.fileName || '', host: data.hoster || '', link: data.status === 'error' ? `[Fehler] ${data.error || 'Fehler'}` : (result.download_url || result.embed_url || ''), isError: data.status === 'error', uploadId: data.uploadId }); const existingIndex = resultRowIndexByUploadId.get(data.uploadId); if (typeof existingIndex === 'number' && resultsRowsData[existingIndex]) { const existingOrder = resultsRowsData[existingIndex].order; resultsRowsData[existingIndex] = { ...rowData, order: existingOrder }; } else { const insertedIndex = resultsRowsData.push(rowData) - 1; if (data.uploadId) resultRowIndexByUploadId.set(data.uploadId, insertedIndex); } renderResultsTable(); } function mergeSummaryIntoResults(summary) { if (!summary || !Array.isArray(summary.files)) return; const { ts, text } = formatDateTime(summary.timestamp || new Date()); for (const file of summary.files) { for (const r of (file.results || [])) { const link = r.status === 'error' ? `[Fehler] ${r.error || 'Fehler'}` : (r.download_url || r.embed_url || ''); const isError = r.status === 'error'; const existingIndex = resultsRowsData.findIndex((row) => row.filename === (file.name || '') && row.host === (r.hoster || '') && row.link === link && row.isError === isError ); if (existingIndex === -1) { resultsRowsData.push(createResultRow({ dateTs: ts, dateText: text, filename: file.name || '', host: r.hoster || '', link, isError })); } } } } function getResultsSortIndicator(columnKey) { if (resultsSortState.key !== columnKey) return '↕'; return resultsSortState.direction === 'asc' ? '▲' : '▼'; } function sortResultsRows(rows) { const sortKey = resultsSortState.key; const factor = resultsSortState.direction === 'asc' ? 1 : -1; return rows.slice().sort((a, b) => { let cmp = 0; if (sortKey === 'date') { cmp = a.dateTs - b.dateTs; } else { const aVal = String(a[sortKey] || ''); const bVal = String(b[sortKey] || ''); cmp = aVal.localeCompare(bVal, 'de', { sensitivity: 'base', numeric: true }); } if (cmp !== 0) return cmp * factor; return a.order - b.order; }); } function renderResultsTable() { const container = document.getElementById('resultsContainer'); if (!container) return; if (!resultsRowsData.length) { container.innerHTML = '

Warte auf erste Upload-Ergebnisse...

'; return; } const sortedRows = sortResultsRows(resultsRowsData); const headerCell = (key, label) => { const active = resultsSortState.key === key; const indicator = getResultsSortIndicator(key); return `${label}${indicator}`; }; let html = ` ${headerCell('date', 'Date')} ${headerCell('filename', 'Filename')} ${headerCell('host', 'Host')} ${headerCell('link', 'Link')} `; sortedRows.forEach((row, index) => { html += ``; }); html += '
${escapeHtml(row.date)} ${escapeHtml(row.filename)} ${escapeHtml(row.host)}
'; container.innerHTML = html; container.querySelectorAll('th.sortable').forEach((th) => { th.addEventListener('click', () => { const key = th.dataset.sortKey; if (!key) return; if (resultsSortState.key === key) { resultsSortState.direction = resultsSortState.direction === 'asc' ? 'desc' : 'asc'; } else { resultsSortState.key = key; resultsSortState.direction = getDefaultSortDirection(key); } selectedRows.clear(); renderResultsTable(); }); }); // Click handler: select row + copy link container.querySelectorAll('.result-row').forEach(tr => { tr.addEventListener('click', (e) => { const idx = tr.dataset.index; const link = tr.dataset.link; const isError = tr.classList.contains('error'); if (e.ctrlKey || e.metaKey) { // Ctrl+Click: toggle selection if (selectedRows.has(idx)) { selectedRows.delete(idx); tr.classList.remove('selected'); } else { selectedRows.add(idx); tr.classList.add('selected'); } // Copy all selected links const links = []; container.querySelectorAll('.result-row.selected').forEach(r => { if (!r.classList.contains('error')) links.push(r.dataset.link); }); if (links.length > 0) { window.api.copyToClipboard(links.join('\n')); showCopyToast(`${links.length} Links kopiert`); } } else if (e.shiftKey && selectedRows.size > 0) { // Shift+Click: range select const allRows = Array.from(container.querySelectorAll('.result-row')); const lastSelected = Math.max(...Array.from(selectedRows).map(Number)); const current = parseInt(idx); const from = Math.min(lastSelected, current); const to = Math.max(lastSelected, current); for (let i = from; i <= to; i++) { selectedRows.add(String(i)); allRows[i].classList.add('selected'); } const links = []; container.querySelectorAll('.result-row.selected').forEach(r => { if (!r.classList.contains('error')) links.push(r.dataset.link); }); if (links.length > 0) { window.api.copyToClipboard(links.join('\n')); showCopyToast(`${links.length} Links kopiert`); } } else { // Normal click: select only this row, copy its link container.querySelectorAll('.result-row').forEach(r => r.classList.remove('selected')); selectedRows.clear(); selectedRows.add(idx); tr.classList.add('selected'); if (!isError && link) { window.api.copyToClipboard(link); showCopyToast('Link kopiert'); } } }); }); } function buildResultsUI(summary) { resetLiveResultsState(); mergeSummaryIntoResults(summary); renderResultsTable(); } function showCopyToast(msg) { let toast = document.getElementById('copyToast'); if (!toast) { toast = document.createElement('div'); toast.id = 'copyToast'; toast.className = 'copy-toast'; document.body.appendChild(toast); } toast.textContent = msg; toast.classList.add('show'); clearTimeout(toast._timer); toast._timer = setTimeout(() => toast.classList.remove('show'), 1500); } function copyAllLinks() { const links = []; document.querySelectorAll('#resultsContainer .result-row:not(.error)').forEach(r => { links.push(r.dataset.link); }); if (links.length > 0) { window.api.copyToClipboard(links.join('\n')); const btn = document.getElementById('copyAllLinksBtn'); btn.textContent = 'Kopiert!'; setTimeout(() => { btn.textContent = 'Alle Links kopieren'; }, 1500); } } // --- Settings --- function renderSettings() { const grid = document.getElementById('settingsGrid'); grid.innerHTML = ''; for (const name of HOSTERS) { const hoster = config.hosters[name] || {}; if (name === 'vidmoly.me') { // Vidmoly uses username/password const block = document.createElement('div'); block.className = 'settings-block'; block.innerHTML = `
${name}
`; block.querySelector('.toggle-vis').addEventListener('click', () => { const pwInput = block.querySelector('[data-field="password"]'); pwInput.type = pwInput.type === 'password' ? 'text' : 'password'; }); grid.appendChild(block); } else { // API key hosters const row = document.createElement('div'); row.className = 'settings-row'; row.innerHTML = ` ${name} `; row.querySelector('.toggle-vis').addEventListener('click', () => { const input = row.querySelector('.key-input'); input.type = input.type === 'password' ? 'text' : 'password'; }); grid.appendChild(row); } } } async function saveSettings() { const hosters = {}; for (const name of HOSTERS) { if (name === 'vidmoly.me') { const usernameInput = document.querySelector(`.key-input[data-hoster="${name}"][data-field="username"]`); const passwordInput = document.querySelector(`.key-input[data-hoster="${name}"][data-field="password"]`); const username = usernameInput ? usernameInput.value.trim() : ''; const password = passwordInput ? passwordInput.value.trim() : ''; hosters[name] = { enabled: !!(username && password), authType: 'login', username, password }; } else { const input = document.querySelector(`.key-input[data-hoster="${name}"]`); const apiKey = input ? input.value.trim() : ''; hosters[name] = { enabled: !!apiKey, apiKey }; } } await window.api.saveConfig({ hosters }); config = await window.api.getConfig(); renderHosterChips(); renderHealthCheckResults([]); setHealthCheckStatus('Bereit fuer Check'); const feedback = document.getElementById('saveFeedback'); feedback.textContent = 'Gespeichert!'; setTimeout(() => { feedback.textContent = ''; }, 2000); } // --- History --- async function loadHistory() { const history = await window.api.getHistory(); const container = document.getElementById('historyContainer'); if (!history || history.length === 0) { historyRowsData = []; container.innerHTML = '

Noch keine Uploads.

'; return; } historySortState = { key: 'date', direction: getDefaultSortDirection('date') }; historyRowsData = []; let order = 0; for (const batch of history) { const formattedDate = formatDateTime(batch && batch.timestamp ? batch.timestamp : new Date()); for (const file of (batch.files || [])) { for (const result of (file.results || [])) { historyRowsData.push({ date: formattedDate.text, dateTs: formattedDate.ts, filename: file.name || '', host: result.hoster || '', link: result.status === 'error' ? `[Fehler] ${result.error || 'Fehler'}` : (result.download_url || result.embed_url || ''), isError: result.status === 'error', order: order++ }); } } } renderHistoryTable(container); } function getHistorySortIndicator(columnKey) { if (historySortState.key !== columnKey) return '↕'; return historySortState.direction === 'asc' ? '▲' : '▼'; } function sortHistoryRows(rows) { const sortKey = historySortState.key; const factor = historySortState.direction === 'asc' ? 1 : -1; return rows.slice().sort((a, b) => { let cmp = 0; if (sortKey === 'date') { cmp = a.dateTs - b.dateTs; } else { const aVal = String(a[sortKey] || ''); const bVal = String(b[sortKey] || ''); cmp = aVal.localeCompare(bVal, 'de', { sensitivity: 'base', numeric: true }); } if (cmp !== 0) return cmp * factor; return a.order - b.order; }); } function renderHistoryTable(container) { if (!container) return; if (!historyRowsData.length) { container.innerHTML = '

Noch keine Uploads.

'; return; } const rows = sortHistoryRows(historyRowsData); const headerCell = (key, label) => { const active = historySortState.key === key; const indicator = getHistorySortIndicator(key); return `${label}${indicator}`; }; let html = ` ${headerCell('date', 'Date')} ${headerCell('filename', 'Filename')} ${headerCell('host', 'Host')} ${headerCell('link', 'Link')} `; rows.forEach((row) => { html += ``; }); html += '
${escapeHtml(row.date)} ${escapeHtml(row.filename)} ${escapeHtml(row.host)}
'; container.innerHTML = html; container.querySelectorAll('th.sortable').forEach((th) => { th.addEventListener('click', () => { const key = th.dataset.historySortKey; if (!key) return; if (historySortState.key === key) { historySortState.direction = historySortState.direction === 'asc' ? 'desc' : 'asc'; } else { historySortState.key = key; historySortState.direction = getDefaultSortDirection(key); } renderHistoryTable(container); }); }); container.querySelectorAll('.history-row').forEach((row) => { row.addEventListener('click', () => { if (row.classList.contains('error')) return; const link = row.dataset.link; if (!link) return; container.querySelectorAll('.history-row').forEach((r) => r.classList.remove('selected')); row.classList.add('selected'); window.api.copyToClipboard(link); showCopyToast('Link kopiert'); }); }); } async function clearHistory() { if (!confirm('Verlauf wirklich loeschen?')) return; await window.api.clearHistory(); loadHistory(); } // --- Utilities --- function formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; } function escapeHtml(str) { if (!str) return ''; return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function escapeAttr(str) { if (!str) return ''; return str.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, '''); } // --- Start --- init();