const HOSTERS = ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx']; // --- State --- let selectedFiles = []; // { path, name, size } let config = { hosters: {}, hosterSettings: {}, globalSettings: {} }; let hosterSettings = {}; let uploading = false; let healthCheckRunning = false; let autoHealthCheckEnabled = true; const AUTO_CHECK_PREF_KEY = 'autoHealthCheckBeforeUpload'; // Queue state let queueJobs = []; // { id, file, fileName, hoster, status, bytesUploaded, bytesTotal, speedKbs, elapsed, remaining, error, result, attempt, maxAttempts, link } let selectedJobIds = new Set(); let queueSortState = { key: 'filename', direction: 'asc' }; // History state let historyRowsData = []; let historySortState = { key: 'date', direction: 'desc' }; // --- Init --- async function init() { config = await window.api.getConfig(); hosterSettings = config.hosterSettings || {}; autoHealthCheckEnabled = loadAutoCheckPreference(); renderHosterChips(); renderSettings(); setupListeners(); setupDragDrop(); loadHistory(); // Version display try { const version = await window.api.getVersion(); const versionLabel = document.getElementById('versionLabel'); if (versionLabel) versionLabel.textContent = `v${version}`; } catch {} // Update listeners window.api.onUpdateAvailable(showUpdateBanner); window.api.onUpdateProgress(handleUpdateProgress); // Upload event listeners — with debug logging to file window.api.onUploadProgress((data) => { window.api.debugLog('RX upload-progress: ' + data.status + ' ' + data.hoster + ' ' + (data.fileName || '')); handleProgress(data); }); window.api.onUploadBatchDone((data) => { window.api.debugLog('RX upload-batch-done'); handleBatchDone(data); }); window.api.onUploadStats((data) => { window.api.debugLog('RX upload-stats: state=' + data.state + ' active=' + data.activeJobs); handleStats(data); }); window.api.onShutdownCountdown(handleShutdownCountdown); window.api.debugLog('init complete, all listeners registered'); // Restore always-on-top state try { const onTop = await window.api.getAlwaysOnTop(); alwaysOnTopState = !!onTop; } catch {} } // --- 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 --- 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); if (!uploading && selectedFiles.length > 0) buildQueuePreview(); updateStartButton(); }); 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'); // Allow drop on the entire upload view const uploadView = document.getElementById('upload-view'); 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'); addDroppedFiles(e.dataTransfer.files); }); dropZone.addEventListener('click', () => pickFiles()); // Also handle drops on queue container uploadView.addEventListener('dragover', (e) => { e.preventDefault(); }); uploadView.addEventListener('drop', (e) => { e.preventDefault(); if (e.target.closest('.drop-zone')) return; // handled above addDroppedFiles(e.dataTransfer.files); }); } function addDroppedFiles(fileList) { const files = Array.from(fileList); for (const file of files) { if (!selectedFiles.find(f => f.path === file.path)) { selectedFiles.push({ path: file.path, name: file.name, size: file.size }); } } updateUploadView(); } 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: null }); // size resolved by upload-manager } } updateUploadView(); } function updateUploadView() { const dropZone = document.getElementById('dropZone'); const queueContainer = document.getElementById('queueContainer'); const queueActions = document.getElementById('queueActions'); if (selectedFiles.length === 0 && queueJobs.length === 0) { dropZone.style.display = 'flex'; queueContainer.style.display = 'none'; queueActions.style.display = 'none'; } else { dropZone.style.display = 'none'; queueContainer.style.display = 'block'; queueActions.style.display = 'flex'; if (!uploading && selectedFiles.length > 0) { buildQueuePreview(); } } updateStartButton(); } function updateStartButton() { const btn = document.getElementById('startUploadBtn'); const hosters = getSelectedHosters(); const hasFiles = selectedFiles.length > 0 || queueJobs.some(j => j.status === 'queued' || j.status === 'error' || j.status === 'preview'); btn.disabled = uploading || hosters.length === 0 || !hasFiles; } // Build preview jobs from selected files x selected hosters (before upload starts) function buildQueuePreview() { const hosters = getSelectedHosters(); // Remove old preview jobs (status 'preview') queueJobs = queueJobs.filter(j => j.status !== 'preview'); for (const file of selectedFiles) { for (const hoster of hosters) { // Don't add if already in queue (from a previous upload) const exists = queueJobs.find(j => j.file === file.path && j.hoster === hoster && j.status !== 'error'); if (!exists) { queueJobs.push({ id: `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, file: file.path, fileName: file.name, hoster, status: 'preview', bytesUploaded: 0, bytesTotal: file.size || 0, speedKbs: 0, elapsed: 0, remaining: 0, error: null, result: null, attempt: 0, maxAttempts: 0, link: '' }); } } } renderQueueTable(); } // --- Queue Table Rendering (debounced) --- let _renderQueued = false; function scheduleQueueRender() { if (_renderQueued) return; _renderQueued = true; requestAnimationFrame(() => { _renderQueued = false; renderQueueTable(); }); } function renderQueueTable() { const tbody = document.getElementById('queueBody'); if (!tbody) return; // Preserve scroll position const scrollContainer = document.getElementById('queueContainer'); const scrollTop = scrollContainer ? scrollContainer.scrollTop : 0; const sorted = sortQueueJobs(queueJobs); tbody.innerHTML = sorted.map((job) => { const statusClass = `status-${job.status}`; const rowClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`; const uploadedSize = job.status === 'preview' ? (job.bytesTotal > 0 ? formatSize(job.bytesTotal) : '...') : `${formatSize(job.bytesUploaded)} / ${formatSize(job.bytesTotal)}`; const statusText = getStatusText(job); const elapsed = formatTime(job.elapsed); const remaining = formatTime(job.remaining); const speed = job.speedKbs > 0 ? `${formatSpeed(job.speedKbs)}` : ''; const pct = Math.min(100, Math.round((job.progress || 0) * 100)); const link = job.result ? (job.result.download_url || job.result.embed_url || '') : ''; return ` ${escapeHtml(job.fileName)} ${uploadedSize} ${escapeHtml(job.hoster)} ${statusText} ${elapsed} ${remaining} ${speed}
${job.status === 'preview' ? '' : pct + '%'}
`; }).join(''); // Restore scroll position if (scrollContainer) scrollContainer.scrollTop = scrollTop; // Attach click handlers tbody.querySelectorAll('.queue-row').forEach(row => { row.addEventListener('click', (e) => handleRowClick(e, row)); row.addEventListener('contextmenu', (e) => handleRowContextMenu(e, row)); }); // Update retry button visibility const hasFailedJobs = queueJobs.some(j => j.status === 'error'); document.getElementById('retryFailedBtn').style.display = hasFailedJobs ? 'inline-block' : 'none'; } function sortQueueJobs(jobs) { const { key, direction } = queueSortState; const factor = direction === 'asc' ? 1 : -1; return jobs.slice().sort((a, b) => { let cmp = 0; if (key === 'filename') cmp = a.fileName.localeCompare(b.fileName, 'de', { sensitivity: 'base', numeric: true }); else if (key === 'size') cmp = (a.bytesTotal || 0) - (b.bytesTotal || 0); else if (key === 'host') cmp = a.hoster.localeCompare(b.hoster); else if (key === 'status') cmp = getStatusOrder(a.status) - getStatusOrder(b.status); else if (key === 'speed') cmp = (a.speedKbs || 0) - (b.speedKbs || 0); return cmp * factor; }); } function getStatusOrder(status) { const order = { uploading: 0, 'getting-server': 1, retrying: 2, queued: 3, preview: 4, done: 5, error: 6, skipped: 7 }; return order[status] ?? 4; } function getStatusText(job) { switch (job.status) { case 'preview': return 'Ready'; case 'queued': return 'Queued'; case 'getting-server': return 'Server...'; case 'uploading': return 'Process'; case 'retrying': return `Retry ${job.attempt}/${job.maxAttempts}`; case 'done': return 'Done'; case 'error': return 'Failed'; case 'skipped': return 'Skipped'; default: return job.status; } } // --- Queue interactions --- function handleRowClick(e, row) { const jobId = row.dataset.jobId; if (e.ctrlKey || e.metaKey) { if (selectedJobIds.has(jobId)) selectedJobIds.delete(jobId); else selectedJobIds.add(jobId); } else if (e.shiftKey && selectedJobIds.size > 0) { const allRows = Array.from(document.querySelectorAll('.queue-row')); const lastIdx = allRows.findIndex(r => selectedJobIds.has(r.dataset.jobId)); const curIdx = allRows.indexOf(row); const from = Math.min(lastIdx, curIdx); const to = Math.max(lastIdx, curIdx); for (let i = from; i <= to; i++) selectedJobIds.add(allRows[i].dataset.jobId); } else { selectedJobIds.clear(); selectedJobIds.add(jobId); // Single click on done job -> copy link const job = queueJobs.find(j => j.id === jobId); if (job && job.status === 'done' && job.result) { const link = job.result.download_url || job.result.embed_url || ''; if (link) { window.api.copyToClipboard(link); showCopyToast('Link kopiert'); } } } renderQueueTable(); } // --- Context menu --- let alwaysOnTopState = false; function handleRowContextMenu(e, row) { e.preventDefault(); const jobId = row.dataset.jobId; if (!selectedJobIds.has(jobId)) { selectedJobIds.clear(); selectedJobIds.add(jobId); renderQueueTable(); } showContextMenu(e.clientX, e.clientY); } function showContextMenu(x, y) { const menu = document.getElementById('contextMenu'); // Update "Always on top" text const aotItem = menu.querySelector('[data-action="always-on-top"]'); if (aotItem) aotItem.textContent = alwaysOnTopState ? 'Immer im Vordergrund ✓' : 'Immer im Vordergrund'; menu.style.display = 'block'; const menuX = Math.min(x, window.innerWidth - menu.offsetWidth - 5); menu.style.left = menuX + 'px'; menu.style.top = Math.min(y, window.innerHeight - menu.offsetHeight - 5) + 'px'; // Flip submenus if they would overflow the viewport right edge menu.querySelectorAll('.ctx-submenu-items').forEach(sub => { sub.classList.toggle('flip-left', menuX + menu.offsetWidth + sub.offsetWidth > window.innerWidth); }); } function hideContextMenu() { document.getElementById('contextMenu').style.display = 'none'; } document.addEventListener('click', (e) => { if (!e.target.closest('.context-menu')) hideContextMenu(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideContextMenu(); }); document.getElementById('contextMenu').addEventListener('click', (e) => { const item = e.target.closest('.ctx-item'); if (!item) return; const action = item.dataset.action; if (!action) return; hideContextMenu(); handleContextAction(action); }); async function handleContextAction(action) { if (action === 'copy-links') { const links = getSelectedJobLinks(); if (links.length) { window.api.copyToClipboard(links.join('\n')); showCopyToast(`${links.length} Links kopiert`); } } else if (action === 'retry-selected') { retrySelectedJobs(); } else if (action === 'delete-selected') { queueJobs = queueJobs.filter(j => !selectedJobIds.has(j.id)); selectedJobIds.clear(); renderQueueTable(); if (queueJobs.length === 0) { selectedFiles = []; updateUploadView(); } } else if (action === 'copy-all-links') { copyAllLinks(); } else if (action === 'delete-all') { if (!uploading) { queueJobs = []; selectedFiles = []; selectedJobIds.clear(); updateUploadView(); } } else if (action === 'always-on-top') { alwaysOnTopState = !alwaysOnTopState; await window.api.setAlwaysOnTop(alwaysOnTopState); } else if (action.startsWith('shutdown-')) { const mode = action.replace('shutdown-', ''); await window.api.setShutdownAfterFinish(mode); } } function getSelectedJobLinks() { return queueJobs .filter(j => selectedJobIds.has(j.id) && j.status === 'done' && j.result) .map(j => j.result.download_url || j.result.embed_url || '') .filter(Boolean); } // --- Upload --- async function startUpload() { if (healthCheckRunning || uploading) return; const hosters = getSelectedHosters(); if (hosters.length === 0) { alert('Bitte mindestens einen Hoster auswaehlen.'); return; } // Include files from preview/queued jobs that may not be in selectedFiles (e.g. retries) const previewFiles = queueJobs .filter(j => j.status === 'preview' || j.status === 'queued') .map(j => j.file) .filter(Boolean); for (const fp of previewFiles) { if (!selectedFiles.find(f => f.path === fp)) { const job = queueJobs.find(j => j.file === fp); selectedFiles.push({ path: fp, name: job ? job.fileName : fp.split(/[\\/]/).pop(), size: job ? job.bytesTotal : null }); } } if (selectedFiles.length === 0 && previewFiles.length === 0) return; // Auto health check if (autoHealthCheckEnabled) { const checkHosters = hosters.filter(name => name === 'doodstream.com' || name === 'vidmoly.me'); if (checkHosters.length > 0) { healthCheckRunning = true; try { const rows = await executeHealthCheck(checkHosters, 'auto'); const errors = rows.filter(r => r.status === 'error'); if (errors.length > 0) { alert(`Auto-Check fehlgeschlagen:\n${errors.map(r => `${r.hoster}: ${r.message}`).join('\n')}\n\nUpload wurde nicht gestartet.`); return; } } catch (err) { alert(`Auto-Check fehlgeschlagen: ${err.message}\nUpload wurde nicht gestartet.`); return; } finally { healthCheckRunning = false; } } } uploading = true; // Convert preview jobs to queued queueJobs.forEach(j => { if (j.status === 'preview') j.status = 'queued'; }); renderQueueTable(); document.getElementById('startUploadBtn').style.display = 'none'; document.getElementById('cancelUploadBtn').style.display = 'inline-block'; const uploadPayload = { files: selectedFiles.map(f => f.path), hosters }; console.log('[startUpload] sending payload:', uploadPayload); const result = await window.api.startUpload(uploadPayload); console.log('[startUpload] response:', result); if (result && result.error) { alert(result.error); uploading = false; document.getElementById('startUploadBtn').style.display = 'inline-block'; document.getElementById('cancelUploadBtn').style.display = 'none'; } } async function cancelUpload() { await window.api.cancelUpload(); uploading = false; document.getElementById('startUploadBtn').style.display = 'inline-block'; document.getElementById('cancelUploadBtn').style.display = 'none'; updateStartButton(); } // --- Progress handling --- function handleProgress(data) { console.log('[upload-progress]', data.status, data.hoster, data.fileName, data.error || ''); // Find matching job by fileName + hoster, or by uploadId let job = data.uploadId ? queueJobs.find(j => j.uploadId === data.uploadId) : null; if (!job) { // Match by file+hoster for queued/preview jobs (prefer queued, then preview) job = queueJobs.find(j => j.fileName === data.fileName && j.hoster === data.hoster && j.status === 'queued' ) || queueJobs.find(j => j.fileName === data.fileName && j.hoster === data.hoster && j.status === 'preview' ); if (job && data.uploadId) job.uploadId = data.uploadId; } if (!job) { // Create new job entry job = { id: data.uploadId || `job-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, uploadId: data.uploadId, file: '', fileName: data.fileName, hoster: data.hoster, status: data.status, bytesUploaded: 0, bytesTotal: data.bytesTotal || 0, speedKbs: 0, elapsed: 0, remaining: 0, error: null, result: null, attempt: 0, maxAttempts: 0, link: '' }; queueJobs.push(job); } // Update job state job.status = data.status; job.bytesUploaded = data.bytesUploaded || 0; job.bytesTotal = data.bytesTotal || job.bytesTotal; job.speedKbs = data.speedKbs || 0; job.elapsed = data.elapsed || 0; job.remaining = data.remaining || 0; job.error = data.error || null; job.result = data.result || job.result; job.attempt = data.attempt || 0; job.maxAttempts = data.maxAttempts || 0; job.progress = data.progress || 0; scheduleQueueRender(); } function handleBatchDone(summary) { console.log('[batch-done]', summary); uploading = false; selectedFiles = []; // Clear selected files after batch document.getElementById('startUploadBtn').style.display = 'inline-block'; document.getElementById('cancelUploadBtn').style.display = 'none'; updateStartButton(); renderQueueTable(); // Final stats update document.getElementById('sbState').textContent = 'Fertig'; } function handleStats(data) { console.log('[upload-stats]', data.state, 'active=' + data.activeJobs); document.getElementById('sbState').textContent = data.state === 'uploading' ? 'Upload laeuft...' : 'Bereit'; document.getElementById('sbSpeed').textContent = formatSpeed(data.globalSpeedKbs || 0); document.getElementById('sbTotal').textContent = formatSize(data.totalBytes || 0); document.getElementById('sbElapsed').textContent = formatTime(data.elapsed || 0); } // --- Retry --- function retrySelectedJobs() { // For now just mark failed jobs back to preview so user can restart queueJobs.forEach(j => { if (selectedJobIds.has(j.id) && j.status === 'error') { j.status = 'preview'; j.error = null; j.bytesUploaded = 0; j.speedKbs = 0; j.elapsed = 0; j.remaining = 0; j.progress = 0; j.uploadId = null; // Re-add to selectedFiles if not present if (!selectedFiles.find(f => f.path === j.file || f.name === j.fileName)) { selectedFiles.push({ path: j.file, name: j.fileName, size: j.bytesTotal }); } } }); selectedJobIds.clear(); renderQueueTable(); updateStartButton(); } // --- Health Check --- function setHealthCheckStatus(text) { // Minimal inline status } 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 = item.status || 'skipped'; return `
${escapeHtml(item.hoster || '')} [${status.toUpperCase()}] ${escapeHtml(item.message || '')}
`; }).join(''); } async function executeHealthCheck(hosters, mode) { renderHealthCheckResults([]); const result = await window.api.runHealthCheck({ hosters }); const rows = result && Array.isArray(result.results) ? result.results : []; renderHealthCheckResults(rows); return rows; } async function runHealthCheck() { if (healthCheckRunning || uploading) return; const hosters = getSelectedHosters().filter(n => n === 'doodstream.com' || n === 'vidmoly.me'); if (hosters.length === 0) { const allHosters = ['doodstream.com', 'vidmoly.me'].filter(n => hosterHasCredentials(n, config.hosters[n] || {})); if (allHosters.length === 0) { alert('Keine Hoster mit Zugangsdaten fuer Health-Check.'); return; } hosters.push(...allHosters); } healthCheckRunning = true; try { await executeHealthCheck(hosters, 'manual'); } catch (err) { renderHealthCheckResults([{ hoster: 'system', status: 'error', message: err.message }]); } finally { healthCheckRunning = false; } } // --- Settings --- function renderSettings() { const container = document.getElementById('settingsHosters'); container.innerHTML = ''; for (const name of HOSTERS) { const hoster = config.hosters[name] || {}; const hs = hosterSettings[name] || {}; const panel = document.createElement('div'); panel.className = 'hoster-settings-panel'; let credsHtml = ''; if (name === 'vidmoly.me') { credsHtml = `
`; } else { credsHtml = `
`; } panel.innerHTML = `
${name} ${hosterHasCredentials(name, hoster) ? 'Aktiv' : 'Inaktiv'}
`; container.appendChild(panel); // Toggle panel panel.querySelector('.hoster-panel-header').addEventListener('click', () => { const body = panel.querySelector('.hoster-panel-body'); const arrow = panel.querySelector('.panel-arrow'); const isOpen = body.style.display !== 'none'; body.style.display = isOpen ? 'none' : 'block'; arrow.innerHTML = isOpen ? '▶' : '▼'; }); // Toggle visibility panel.querySelectorAll('.toggle-vis').forEach(btn => { btn.addEventListener('click', () => { const input = btn.previousElementSibling; input.type = input.type === 'password' ? 'text' : 'password'; }); }); } } async function saveSettings() { const hosters = {}; const newHosterSettings = {}; for (const name of HOSTERS) { // Credentials if (name === 'vidmoly.me') { const username = (document.querySelector(`.key-input[data-hoster="${name}"][data-field="username"]`)?.value || '').trim(); const password = (document.querySelector(`.key-input[data-hoster="${name}"][data-field="password"]`)?.value || '').trim(); hosters[name] = { enabled: !!(username && password), authType: 'login', username, password }; } else { const apiKey = (document.querySelector(`.key-input[data-hoster="${name}"][data-field="apiKey"]`)?.value || '').trim(); hosters[name] = { enabled: !!apiKey, apiKey }; } // Upload settings const hs = {}; document.querySelectorAll(`.hs-input[data-hoster="${name}"]`).forEach(input => { const field = input.dataset.hs; hs[field] = parseInt(input.value) || 0; }); newHosterSettings[name] = hs; } await window.api.saveConfig({ hosters }); await window.api.saveHosterSettings(newHosterSettings); config = await window.api.getConfig(); hosterSettings = config.hosterSettings || {}; renderHosterChips(); renderSettings(); renderHealthCheckResults([]); 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: 'desc' }; historyRowsData = []; let order = 0; for (const batch of history) { const dt = formatDateTime(batch.timestamp || new Date()); for (const file of (batch.files || [])) { for (const result of (file.results || [])) { historyRowsData.push({ date: dt.text, dateTs: dt.ts, filename: file.name || '', host: result.hoster || '', link: result.status === 'error' ? `[Fehler] ${result.error || ''}` : (result.download_url || result.embed_url || ''), isError: result.status === 'error', order: order++ }); } } } renderHistoryTable(container); } function renderHistoryTable(container) { if (!container || !historyRowsData.length) { if (container) container.innerHTML = '

Noch keine Uploads.

'; return; } const rows = sortHistoryRows(historyRowsData); const headerCell = (key, label) => { const active = historySortState.key === key; const dir = active ? (historySortState.direction === 'asc' ? '▲' : '▼') : '↕'; return `${label}${dir}`; }; 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.historySort; if (historySortState.key === key) historySortState.direction = historySortState.direction === 'asc' ? 'desc' : 'asc'; else { historySortState.key = key; historySortState.direction = key === 'date' ? 'desc' : 'asc'; } renderHistoryTable(container); }); }); container.querySelectorAll('.history-row').forEach(row => { row.addEventListener('click', () => { if (row.classList.contains('error')) return; const link = row.dataset.link; if (link) { window.api.copyToClipboard(link); showCopyToast('Link kopiert'); } }); }); } function sortHistoryRows(rows) { const { key, direction } = historySortState; const factor = direction === 'asc' ? 1 : -1; return rows.slice().sort((a, b) => { let cmp = key === 'date' ? a.dateTs - b.dateTs : String(a[key] || '').localeCompare(String(b[key] || ''), 'de', { sensitivity: 'base', numeric: true }); return (cmp || a.order - b.order) * factor; }); } // --- Setup Listeners --- function setupListeners() { document.getElementById('addFilesBtn').addEventListener('click', pickFiles); document.getElementById('startUploadBtn').addEventListener('click', startUpload); document.getElementById('cancelUploadBtn').addEventListener('click', cancelUpload); document.getElementById('runHealthCheckBtn').addEventListener('click', runHealthCheck); document.getElementById('copyAllLinksBtn').addEventListener('click', copyAllLinks); document.getElementById('retryFailedBtn').addEventListener('click', () => { queueJobs.forEach(j => { if (j.status === 'error') selectedJobIds.add(j.id); }); retrySelectedJobs(); }); document.getElementById('clearQueueBtn').addEventListener('click', () => { if (!uploading) { queueJobs = []; selectedFiles = []; selectedJobIds.clear(); updateUploadView(); } }); document.getElementById('saveSettingsBtn').addEventListener('click', saveSettings); document.getElementById('clearHistoryBtn').addEventListener('click', async () => { if (!confirm('Verlauf wirklich loeschen?')) return; await window.api.clearHistory(); loadHistory(); }); // Auto health check toggle const autoToggle = document.getElementById('autoHealthCheckToggle'); if (autoToggle) { autoToggle.checked = autoHealthCheckEnabled; autoToggle.addEventListener('change', (e) => { autoHealthCheckEnabled = !!e.target.checked; try { localStorage.setItem(AUTO_CHECK_PREF_KEY, autoHealthCheckEnabled ? '1' : '0'); } catch {} }); } // Queue table sorting document.querySelectorAll('#queueTable th.sortable').forEach(th => { th.addEventListener('click', () => { const key = th.dataset.sort; if (queueSortState.key === key) queueSortState.direction = queueSortState.direction === 'asc' ? 'desc' : 'asc'; else { queueSortState.key = key; queueSortState.direction = 'asc'; } renderQueueTable(); }); }); // Shutdown cancel document.getElementById('cancelShutdownBtn').addEventListener('click', async () => { await window.api.cancelShutdown(); if (shutdownCountdownInterval) { clearInterval(shutdownCountdownInterval); shutdownCountdownInterval = null; } document.getElementById('shutdownOverlay').style.display = 'none'; }); // Right-click on upload view background document.getElementById('upload-view').addEventListener('contextmenu', (e) => { if (e.target.closest('.queue-row')) return; // handled per row e.preventDefault(); showContextMenu(e.clientX, e.clientY); }); } // --- Update UI --- function showUpdateBanner(info) { const banner = document.getElementById('updateBanner'); const msg = document.getElementById('updateMessage'); if (!banner || !msg) return; msg.textContent = `Update v${info.remoteVersion} verfuegbar`; banner.style.display = 'flex'; document.getElementById('installUpdateBtn').onclick = async () => { msg.textContent = 'Update wird heruntergeladen...'; document.getElementById('installUpdateBtn').disabled = true; await window.api.installUpdate(); }; document.getElementById('dismissUpdateBtn').onclick = () => { banner.style.display = 'none'; }; } function handleUpdateProgress(data) { const msg = document.getElementById('updateMessage'); if (!msg) return; if (data.stage === 'downloading') msg.textContent = `Downloading... ${data.percent || 0}%`; else if (data.stage === 'verifying') msg.textContent = 'Verifiziere...'; else if (data.stage === 'launching') msg.textContent = 'Setup wird gestartet...'; else if (data.stage === 'done') msg.textContent = 'Update installiert. App wird neu gestartet...'; else if (data.stage === 'error') { msg.textContent = `Update fehlgeschlagen: ${data.error}`; const btn = document.getElementById('installUpdateBtn'); if (btn) { btn.disabled = false; btn.textContent = 'Erneut versuchen'; } } } // --- Shutdown --- let shutdownCountdownInterval = null; function handleShutdownCountdown(data) { const overlay = document.getElementById('shutdownOverlay'); const msgEl = document.getElementById('shutdownMessage'); const secEl = document.getElementById('shutdownSeconds'); overlay.style.display = 'flex'; const labels = { sleep: 'Ruhezustand', shutdown: 'Herunterfahren', restart: 'Neustart' }; let remaining = data.seconds || 60; secEl.textContent = remaining; msgEl.textContent = `${labels[data.mode] || data.mode} in ${remaining}s...`; if (shutdownCountdownInterval) clearInterval(shutdownCountdownInterval); shutdownCountdownInterval = setInterval(() => { remaining--; secEl.textContent = remaining; msgEl.textContent = `${labels[data.mode] || data.mode} in ${remaining}s...`; if (remaining <= 0) { clearInterval(shutdownCountdownInterval); } }, 1000); } // --- Link operations --- function copyAllLinks() { const links = queueJobs .filter(j => j.status === 'done' && j.result) .map(j => j.result.download_url || j.result.embed_url || '') .filter(Boolean); if (links.length > 0) { window.api.copyToClipboard(links.join('\n')); showCopyToast(`${links.length} Links kopiert`); } } // --- Utilities --- function formatSize(bytes) { if (!bytes || bytes <= 0) return '0 B'; 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 formatSpeed(kbs) { if (!kbs || kbs <= 0) return '0 kB/s'; if (kbs >= 1024) return (kbs / 1024).toFixed(1) + ' MB/s'; return Math.round(kbs) + ' kB/s'; } function formatTime(seconds) { if (!seconds || seconds <= 0) return '00:00'; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; if (h > 0) return `${pad(h)}:${pad(m)}:${pad(s)}`; return `${pad(m)}:${pad(s)}`; } function pad(n) { return String(Math.floor(n)).padStart(2, '0'); } 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' }) }; } function loadAutoCheckPreference() { try { const r = localStorage.getItem(AUTO_CHECK_PREF_KEY); return r === null || r === '1'; } catch { return true; } } function escapeHtml(str) { if (!str) return ''; return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function escapeAttr(str) { if (!str) return ''; return String(str).replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, '''); } function showCopyToast(msg) { const toast = document.getElementById('copyToast'); toast.textContent = msg; toast.classList.add('show'); clearTimeout(toast._timer); toast._timer = setTimeout(() => toast.classList.remove('show'), 1500); } // --- Start --- init();