const HOSTERS = ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx'];
// --- State ---
let selectedFiles = []; // { path, name, size }
let selectedUploadHosters = [];
let config = { hosters: {}, hosterSettings: {}, globalSettings: {} };
let hosterSettings = {};
let uploading = false;
let healthCheckRunning = false;
let accountStatuses = {}; // { 'voe.sx': { status: 'ok'|'error'|'checking'|'unchecked', message: '' } }
let editingAccountHoster = null; // null = adding, string = editing
let autoHealthCheckEnabled = true;
let queuePersistTimer = null;
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 _jobIndexById = new Map(); // id -> job (O(1) lookup)
let _jobIndexByUploadId = new Map(); // uploadId -> job
let selectedJobIds = new Set();
let queueSortState = { key: 'filename', direction: 'asc' };
// History state
let historyRowsData = [];
let historySortState = { key: 'date', direction: 'desc' };
// Session-specific files for the "Files" panel (resets each session)
let sessionFilesData = [];
// --- Init ---
async function init() {
config = await window.api.getConfig();
hosterSettings = config.hosterSettings || {};
autoHealthCheckEnabled = loadAutoCheckPreference();
syncSelectedUploadHosters();
restoreQueueStateFromConfig();
renderHosterSummary();
renderHosterModal();
renderSettings();
renderAccounts();
setupListeners();
setupDragDrop();
loadHistory();
renderRecentUploadsPanel();
updateUploadView();
if (shouldAutoResumeQueue()) {
setTimeout(() => {
if (!uploading) startUpload();
}, 350);
}
// 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 selection ---
function hosterHasCredentials(name, hoster) {
if (name === 'vidmoly.me') return !!(hoster.username && hoster.password);
if (name === 'voe.sx' || name === 'doodstream.com') return !!(hoster.username && hoster.password) || !!hoster.apiKey;
return !!hoster.apiKey;
}
function getAvailableHosters() {
return HOSTERS
.map(name => {
const hoster = config.hosters[name] || {};
return { name, hoster, hasCreds: hosterHasCredentials(name, hoster) };
})
.filter(item => item.hasCreds);
}
function syncSelectedUploadHosters() {
const available = new Set(getAvailableHosters().map(item => item.name));
selectedUploadHosters = selectedUploadHosters.filter(name => available.has(name));
if (selectedUploadHosters.length === 0) {
selectedUploadHosters = HOSTERS.filter(name => {
const hoster = config.hosters[name] || {};
return !!hoster.enabled && hosterHasCredentials(name, hoster);
});
}
}
function getSelectedHosters() {
return selectedUploadHosters.slice();
}
function renderHosterSummary() {
const summary = document.getElementById('hosterSummary');
if (!summary) return;
const hosters = getSelectedHosters();
if (hosters.length === 0) {
summary.textContent = 'Keine Upload-Ziele ausgewaehlt';
} else if (hosters.length === 1) {
summary.textContent = `Aktives Ziel: ${hosters[0]}`;
} else {
summary.textContent = `${hosters.length} Ziele aktiv: ${hosters.join(', ')}`;
}
}
function renderHosterModal() {
const list = document.getElementById('hosterModalList');
const hint = document.getElementById('hosterModalHint');
if (!list || !hint) return;
const available = getAvailableHosters();
if (available.length === 0) {
list.innerHTML = '';
hint.textContent = 'Keine Hoster mit Zugangsdaten vorhanden. Bitte zuerst in den Einstellungen API-Key oder Login eintragen.';
return;
}
list.innerHTML = available.map(item => {
const checked = selectedUploadHosters.includes(item.name);
const h = config.hosters[item.name] || {};
const st = accountStatuses[item.name];
const subtitle = st && st.status === 'ok' ? 'Bereit'
: st && st.status === 'error' ? 'Login-Fehler'
: getCredentialLabel(item.name, h) + ' hinterlegt';
return `
`;
}).join('');
hint.textContent = 'Die Auswahl wird fuer neue Queue-Eintraege verwendet.';
list.querySelectorAll('input[data-hoster-modal]').forEach(input => {
input.addEventListener('change', () => {
input.closest('.hoster-option')?.classList.toggle('selected', input.checked);
});
});
}
function openHosterModal() {
syncSelectedUploadHosters();
renderHosterModal();
document.getElementById('hosterModal').style.display = 'flex';
}
function closeHosterModal() {
const modal = document.getElementById('hosterModal');
if (modal) modal.style.display = 'none';
}
function applyHosterSelection() {
selectedUploadHosters = Array.from(document.querySelectorAll('input[data-hoster-modal]:checked'))
.map(input => input.dataset.hosterModal);
// Move pending files to selectedFiles on confirm
if (_pendingFiles.length > 0) {
selectedFiles.push(..._pendingFiles);
_pendingFiles = [];
}
renderHosterSummary();
updateUploadView();
persistQueueStateSoon();
document.getElementById('hosterModal').style.display = 'none';
}
function cancelHosterModal() {
_pendingFiles = [];
closeHosterModal();
}
function normalizeRestoredJobStatus(status) {
if (status === 'done' || status === 'error' || status === 'skipped' || status === 'preview') return status;
return 'queued';
}
function restoreQueueStateFromConfig() {
const pending = config?.globalSettings?.pendingQueue;
if (!pending || typeof pending !== 'object') return;
selectedUploadHosters = Array.isArray(pending.selectedUploadHosters)
? pending.selectedUploadHosters.filter(Boolean)
: selectedUploadHosters;
selectedFiles = Array.isArray(pending.selectedFiles)
? pending.selectedFiles
.filter(file => file && file.path)
.map(file => ({ path: file.path, name: file.name || file.path.split(/[\\/]/).pop(), size: file.size || 0 }))
: [];
queueJobs = Array.isArray(pending.queueJobs)
? pending.queueJobs
.filter(job => job && job.fileName && job.hoster)
.map(job => ({
id: job.id || `restored-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
uploadId: null,
file: job.file || '',
fileName: job.fileName,
hoster: job.hoster,
status: normalizeRestoredJobStatus(job.status),
bytesUploaded: job.status === 'done' ? (job.bytesTotal || 0) : 0,
bytesTotal: job.bytesTotal || 0,
speedKbs: 0,
elapsed: 0,
remaining: 0,
error: job.error || null,
result: job.result || null,
attempt: 0,
maxAttempts: job.maxAttempts || 0,
link: '',
progress: job.status === 'done' ? 1 : 0
}))
: [];
}
function buildPersistedQueueState() {
const unfinishedJobs = queueJobs.filter(job => job.status !== 'done' && job.status !== 'skipped');
const selectedFileMap = new Map(selectedFiles.map(file => [file.path, file]));
for (const job of unfinishedJobs) {
if (job.file && !selectedFileMap.has(job.file)) {
selectedFileMap.set(job.file, {
path: job.file,
name: job.fileName,
size: job.bytesTotal || 0
});
}
}
if (selectedFileMap.size === 0 && queueJobs.every(job => job.status === 'done' || job.status === 'skipped')) {
return null;
}
return {
selectedUploadHosters: getSelectedHosters(),
selectedFiles: Array.from(selectedFileMap.values()),
queueJobs: queueJobs.map(job => ({
id: job.id,
file: job.file,
fileName: job.fileName,
hoster: job.hoster,
status: job.status,
bytesTotal: job.bytesTotal || 0,
error: job.error || null,
result: job.result || null,
maxAttempts: job.maxAttempts || 0
}))
};
}
async function persistQueueStateNow() {
const globalSettings = {
...(config.globalSettings || {}),
pendingQueue: buildPersistedQueueState()
};
config.globalSettings = globalSettings;
await window.api.saveGlobalSettings(globalSettings);
}
function persistQueueStateSoon() {
clearTimeout(queuePersistTimer);
// Use longer debounce during uploads to reduce disk I/O
const delay = uploading ? 3000 : 500;
queuePersistTimer = setTimeout(() => {
persistQueueStateNow().catch(() => {});
}, delay);
}
function clearPersistedQueueStateSoon() {
clearTimeout(queuePersistTimer);
queuePersistTimer = setTimeout(() => {
const globalSettings = {
...(config.globalSettings || {}),
pendingQueue: null
};
config.globalSettings = globalSettings;
window.api.saveGlobalSettings(globalSettings).catch(() => {});
}, 0);
}
function shouldAutoResumeQueue() {
if (!config?.globalSettings?.resumeQueueOnLaunch) return false;
return queueJobs.some(job => !['done', 'skipped'].includes(job.status));
}
// --- 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);
});
}
let _pendingFiles = []; // Files waiting for hoster modal confirmation
function addDroppedFiles(fileList) {
const files = Array.from(fileList);
const newFiles = [];
for (const file of files) {
if (!selectedFiles.find(f => f.path === file.path) && !_pendingFiles.find(f => f.path === file.path)) {
newFiles.push({ path: file.path, name: file.name, size: file.size });
}
}
if (newFiles.length > 0) {
_pendingFiles.push(...newFiles);
openHosterModal();
}
}
async function pickFiles() {
const paths = await window.api.selectFiles();
if (!paths) return;
const newFiles = [];
for (const p of paths) {
if (!selectedFiles.find(f => f.path === p) && !_pendingFiles.find(f => f.path === p)) {
const name = p.split('\\').pop().split('/').pop();
newFiles.push({ path: p, name, size: null });
}
}
if (newFiles.length > 0) {
_pendingFiles.push(...newFiles);
openHosterModal();
}
}
function updateUploadView() {
const dropZone = document.getElementById('dropZone');
const queueShell = document.getElementById('queueShell');
const queueActions = document.getElementById('queueActions');
if (selectedFiles.length === 0 && queueJobs.length === 0) {
dropZone.style.display = 'flex';
queueShell.style.display = 'none';
queueActions.style.display = 'none';
} else {
dropZone.style.display = 'none';
queueShell.style.display = 'flex';
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();
if (hosters.length === 0) {
queueJobs = queueJobs.filter(j => j.status !== 'preview');
rebuildJobIndex();
renderQueueTable();
persistQueueStateSoon();
return;
}
// Remove old preview jobs
queueJobs = queueJobs.filter(j => j.status !== 'preview');
// Build a Set for fast existence checks
const existingKeys = new Set();
for (const j of queueJobs) {
if (j.status !== 'error') existingKeys.add(`${j.file}|${j.hoster}`);
}
for (const file of selectedFiles) {
for (const hoster of hosters) {
const key = `${file.path}|${hoster}`;
if (!existingKeys.has(key)) {
const job = {
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: ''
};
queueJobs.push(job);
existingKeys.add(key);
}
}
}
rebuildJobIndex();
renderQueueTable();
persistQueueStateSoon();
}
// --- Job Index Management ---
function rebuildJobIndex() {
_jobIndexById.clear();
_jobIndexByUploadId.clear();
for (const job of queueJobs) {
_jobIndexById.set(job.id, job);
if (job.uploadId) _jobIndexByUploadId.set(job.uploadId, job);
}
}
function indexJob(job) {
_jobIndexById.set(job.id, job);
if (job.uploadId) _jobIndexByUploadId.set(job.uploadId, job);
}
function removeJobFromIndex(job) {
_jobIndexById.delete(job.id);
if (job.uploadId) _jobIndexByUploadId.delete(job.uploadId);
}
// --- Queue Table Rendering (debounced with virtual scrolling) ---
let _renderQueued = false;
let _sortedJobsCache = [];
const VIRTUAL_ROW_HEIGHT = 28;
const VIRTUAL_OVERSCAN = 10;
let _lastVisibleRange = { start: -1, end: -1 };
let _queueListenersBound = false;
function scheduleQueueRender() {
if (_renderQueued) return;
_renderQueued = true;
requestAnimationFrame(() => { _renderQueued = false; renderQueueTable(); });
}
function buildRowHtml(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 + '%'}
|
`;
}
function renderQueueTable() {
const tbody = document.getElementById('queueBody');
if (!tbody) return;
_sortedJobsCache = sortQueueJobs(queueJobs);
const totalRows = _sortedJobsCache.length;
// For small queues (<200 rows), use simple rendering
if (totalRows < 200) {
tbody.innerHTML = _sortedJobsCache.map(buildRowHtml).join('');
_lastVisibleRange = { start: -1, end: -1 };
} else {
// Virtual scrolling for large queues
_renderVirtualRows(tbody);
}
// Bind event delegation once
if (!_queueListenersBound) {
_queueListenersBound = true;
tbody.addEventListener('click', (e) => {
const row = e.target.closest('.queue-row');
if (row) handleRowClick(e, row);
});
tbody.addEventListener('contextmenu', (e) => {
const row = e.target.closest('.queue-row');
if (row) 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 _renderVirtualRows(tbody) {
const scrollContainer = document.getElementById('queueContainer');
if (!scrollContainer) return;
const totalRows = _sortedJobsCache.length;
const totalHeight = totalRows * VIRTUAL_ROW_HEIGHT;
const scrollTop = scrollContainer.scrollTop;
const viewportHeight = scrollContainer.clientHeight;
const startIdx = Math.max(0, Math.floor(scrollTop / VIRTUAL_ROW_HEIGHT) - VIRTUAL_OVERSCAN);
const endIdx = Math.min(totalRows, Math.ceil((scrollTop + viewportHeight) / VIRTUAL_ROW_HEIGHT) + VIRTUAL_OVERSCAN);
// Only re-render if visible range changed
if (startIdx === _lastVisibleRange.start && endIdx === _lastVisibleRange.end) return;
_lastVisibleRange = { start: startIdx, end: endIdx };
const topPad = startIdx * VIRTUAL_ROW_HEIGHT;
const bottomPad = (totalRows - endIdx) * VIRTUAL_ROW_HEIGHT;
let html = '';
if (topPad > 0) html += ` |
`;
for (let i = startIdx; i < endIdx; i++) {
html += buildRowHtml(_sortedJobsCache[i]);
}
if (bottomPad > 0) html += ` |
`;
tbody.innerHTML = html;
}
function _onQueueScroll() {
if (_sortedJobsCache.length >= 200) {
const tbody = document.getElementById('queueBody');
if (tbody) _renderVirtualRows(tbody);
}
}
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();
cancelHosterModal();
}
});
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(); }
persistQueueStateSoon();
} else if (action === 'copy-all-links') {
copyAllLinks();
} 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' || name === 'voe.sx' || name === 'byse.sx');
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);
persistQueueStateSoon();
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();
persistQueueStateSoon();
}
// --- Progress handling ---
function handleProgress(data) {
// Find matching job: O(1) by uploadId, fallback to linear search
let job = data.uploadId ? _jobIndexByUploadId.get(data.uploadId) : null;
if (!job) {
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;
_jobIndexByUploadId.set(data.uploadId, job);
}
}
if (!job) {
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);
indexJob(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();
persistQueueStateSoon();
}
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();
// Add completed jobs to session files panel
const dt = formatDateTime(new Date());
for (const job of queueJobs) {
if (job.status === 'done' && job.result) {
const link = job.result.download_url || job.result.embed_url || '';
if (link && !sessionFilesData.some(s => s.link === link && s.filename === job.fileName && s.host === job.hoster)) {
sessionFilesData.push({
date: dt.text, dateTs: dt.ts,
filename: job.fileName || '', host: job.hoster || '',
link, isError: false, order: sessionFilesData.length
});
}
} else if (job.status === 'error') {
sessionFilesData.push({
date: dt.text, dateTs: dt.ts,
filename: job.fileName || '', host: job.hoster || '',
link: `[Fehler] ${job.error || ''}`, isError: true, order: sessionFilesData.length
});
}
}
renderRecentUploadsPanel();
loadHistory();
// Auto-remove completed jobs from queue if enabled
const removeOnDone = config.globalSettings && config.globalSettings.removeFromQueueOnDone;
if (removeOnDone) {
const doneJobs = queueJobs.filter(j => j.status === 'done');
for (const job of doneJobs) {
removeJobFromIndex(job);
selectedJobIds.delete(job.id);
}
queueJobs = queueJobs.filter(j => j.status !== 'done');
renderQueueTable();
}
clearPersistedQueueStateSoon();
// Final stats update
document.getElementById('sbState').textContent = 'Fertig';
}
function handleStats(data) {
// stats logging removed for perf
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();
persistQueueStateSoon();
}
// --- 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' || n === 'voe.sx' || n === 'byse.sx');
if (hosters.length === 0) {
const allHosters = ['doodstream.com', 'vidmoly.me', 'voe.sx', 'byse.sx'].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 = '';
const globalSettings = config.globalSettings || {};
const generalPanel = document.createElement('div');
generalPanel.className = 'hoster-settings-panel';
generalPanel.innerHTML = `
`;
container.appendChild(generalPanel);
for (const name of HOSTERS) {
const hoster = config.hosters[name] || {};
const hs = hosterSettings[name] || {};
const panel = document.createElement('div');
panel.className = 'hoster-settings-panel';
panel.innerHTML = `
`;
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 ? '▶' : '▼';
});
}
document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath);
}
async function chooseLogFilePath() {
const folders = await window.api.selectFolder();
if (!folders || !folders[0]) return;
const normalized = folders[0].replace(/[\\\/]+$/, '');
document.getElementById('logFilePathInput').value = `${normalized}\\fileuploader.log`;
}
async function saveSettings() {
const newHosterSettings = {};
const globalSettings = {
...(config.globalSettings || {}),
logFilePath: (document.getElementById('logFilePathInput')?.value || '').trim(),
resumeQueueOnLaunch: !!document.getElementById('resumeQueueOnLaunchInput')?.checked,
removeFromQueueOnDone: !!document.getElementById('removeFromQueueOnDoneInput')?.checked
};
for (const name of HOSTERS) {
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.saveHosterSettings(newHosterSettings);
await window.api.saveGlobalSettings(globalSettings);
config = await window.api.getConfig();
hosterSettings = config.hosterSettings || {};
renderSettings();
const feedback = document.getElementById('saveFeedback');
feedback.textContent = 'Gespeichert!';
setTimeout(() => { feedback.textContent = ''; }, 2000);
}
// --- Accounts ---
function getAccountsWithCreds() {
return HOSTERS
.map(name => ({ name, hoster: config.hosters[name] || {} }))
.filter(item => hosterHasCredentials(item.name, item.hoster));
}
function getHostersWithoutCreds() {
return HOSTERS.filter(name => !hosterHasCredentials(name, config.hosters[name] || {}));
}
function getCredentialLabel(name, hoster) {
if (name === 'vidmoly.me') return hoster.username || 'Login';
if (name === 'voe.sx') return hoster.username && hoster.password ? (hoster.username || 'Login') : 'API-Key';
if (name === 'doodstream.com') return hoster.username && hoster.password ? (hoster.username || 'Login') : 'API-Key';
return 'API-Key';
}
function renderAccounts() {
const container = document.getElementById('accountsList');
if (!container) return;
const accounts = getAccountsWithCreds();
if (accounts.length === 0) {
container.innerHTML = `
Keine Accounts vorhanden
Klicke auf "Account hinzufuegen" um einen Hoster einzurichten.
`;
return;
}
container.innerHTML = accounts.map(({ name, hoster }) => {
const st = accountStatuses[name] || { status: 'unchecked', message: '' };
const statusLabels = { ok: 'Bereit', checking: 'Pruefe...', error: 'Fehler', unchecked: 'Nicht geprueft' };
const statusLabel = statusLabels[st.status] || 'Nicht geprueft';
const credLabel = getCredentialLabel(name, hoster);
return `
${escapeHtml(name)}
${escapeHtml(credLabel)}${st.status === 'error' && st.message ? ' — ' + escapeHtml(st.message) : ''}
${statusLabel}
`;
}).join('');
// Wire up buttons
container.querySelectorAll('[data-account-edit]').forEach(btn => {
btn.addEventListener('click', () => openAccountModal(btn.dataset.accountEdit));
});
container.querySelectorAll('[data-account-delete]').forEach(btn => {
btn.addEventListener('click', () => openDeleteAccountModal(btn.dataset.accountDelete));
});
container.querySelectorAll('[data-account-check]').forEach(btn => {
btn.addEventListener('click', () => checkSingleAccount(btn.dataset.accountCheck));
});
}
async function checkSingleAccount(hosterName) {
accountStatuses[hosterName] = { status: 'checking', message: '' };
renderAccounts();
try {
const rows = await executeHealthCheck([hosterName], 'auto');
const row = rows.find(r => r.hoster === hosterName);
if (row && row.status === 'ok') {
accountStatuses[hosterName] = { status: 'ok', message: row.message || '' };
} else {
accountStatuses[hosterName] = { status: 'error', message: (row && row.message) || 'Pruefung fehlgeschlagen' };
}
} catch (err) {
accountStatuses[hosterName] = { status: 'error', message: err.message || 'Pruefung fehlgeschlagen' };
}
renderAccounts();
}
function getCredsFieldsHtml(name, hoster) {
hoster = hoster || {};
if (name === 'vidmoly.me') {
return `
`;
}
if (name === 'voe.sx' || name === 'doodstream.com') {
return `
Login wird bevorzugt. API-Key nur als Fallback.
`;
}
// Default: API key only
return `
`;
}
function openAccountModal(editHoster) {
editingAccountHoster = editHoster || null;
const modal = document.getElementById('accountModal');
const title = document.getElementById('accountModalTitle');
const subtitle = document.getElementById('accountModalSubtitle');
const hosterRow = document.getElementById('accountHosterRow');
const hosterSelect = document.getElementById('accountHosterSelect');
const credsContainer = document.getElementById('accountCredsFields');
const statusEl = document.getElementById('accountModalStatus');
const saveBtn = document.getElementById('saveAccountBtn');
statusEl.textContent = '';
statusEl.className = 'account-modal-status';
if (editingAccountHoster) {
// Edit mode
title.textContent = 'Account bearbeiten';
subtitle.textContent = `Zugangsdaten fuer ${editingAccountHoster} bearbeiten.`;
hosterRow.style.display = 'none';
saveBtn.textContent = 'Speichern & Pruefen';
const hoster = config.hosters[editingAccountHoster] || {};
credsContainer.innerHTML = getCredsFieldsHtml(editingAccountHoster, hoster);
} else {
// Add mode
title.textContent = 'Account hinzufuegen';
subtitle.textContent = 'Waehle einen Hoster und gib deine Zugangsdaten ein.';
hosterRow.style.display = 'flex';
saveBtn.textContent = 'Anlegen & Pruefen';
const available = getHostersWithoutCreds();
if (available.length === 0) {
hosterSelect.innerHTML = '';
credsContainer.innerHTML = '';
} else {
hosterSelect.innerHTML = available.map(name => ``).join('');
credsContainer.innerHTML = getCredsFieldsHtml(available[0], {});
}
}
// Toggle visibility buttons
credsContainer.querySelectorAll('.toggle-vis').forEach(btn => {
btn.addEventListener('click', () => {
const input = btn.previousElementSibling;
input.type = input.type === 'password' ? 'text' : 'password';
});
});
modal.style.display = 'flex';
}
function closeAccountModal() {
document.getElementById('accountModal').style.display = 'none';
editingAccountHoster = null;
}
function openDeleteAccountModal(hosterName) {
const modal = document.getElementById('deleteAccountModal');
const msg = document.getElementById('deleteAccountMessage');
msg.textContent = `Account fuer "${hosterName}" wirklich loeschen? Alle Zugangsdaten werden entfernt.`;
modal.dataset.hoster = hosterName;
modal.style.display = 'flex';
}
function closeDeleteModal() {
document.getElementById('deleteAccountModal').style.display = 'none';
}
async function deleteAccount(hosterName) {
const hosters = { ...config.hosters };
// Reset credentials to defaults
if (hosterName === 'vidmoly.me') {
hosters[hosterName] = { enabled: false, authType: 'login', username: '', password: '' };
} else if (hosterName === 'voe.sx' || hosterName === 'doodstream.com') {
hosters[hosterName] = { enabled: false, username: '', password: '', apiKey: '' };
} else {
hosters[hosterName] = { enabled: false, apiKey: '' };
}
delete accountStatuses[hosterName];
await window.api.saveConfig({ hosters });
config = await window.api.getConfig();
syncSelectedUploadHosters();
renderAccounts();
renderHosterSummary();
renderHosterModal();
renderSettings();
closeDeleteModal();
}
function readAccountCredsFromModal(hosterName) {
if (hosterName === 'vidmoly.me') {
const username = (document.getElementById('accField_username')?.value || '').trim();
const password = (document.getElementById('accField_password')?.value || '').trim();
return { enabled: !!(username && password), authType: 'login', username, password };
}
if (hosterName === 'voe.sx' || hosterName === 'doodstream.com') {
const username = (document.getElementById('accField_username')?.value || '').trim();
const password = (document.getElementById('accField_password')?.value || '').trim();
const apiKey = (document.getElementById('accField_apiKey')?.value || '').trim();
return { enabled: !!(username && password) || !!apiKey, username, password, apiKey };
}
const apiKey = (document.getElementById('accField_apiKey')?.value || '').trim();
return { enabled: !!apiKey, apiKey };
}
async function saveAccount() {
const hosterName = editingAccountHoster || document.getElementById('accountHosterSelect')?.value;
if (!hosterName) return;
const creds = readAccountCredsFromModal(hosterName);
if (!creds.enabled) {
const statusEl = document.getElementById('accountModalStatus');
statusEl.textContent = 'Bitte Zugangsdaten eingeben.';
statusEl.className = 'account-modal-status error';
return;
}
// Save credentials
const hosters = { ...config.hosters };
hosters[hosterName] = creds;
await window.api.saveConfig({ hosters });
config = await window.api.getConfig();
// Show checking status
const statusEl = document.getElementById('accountModalStatus');
const saveBtn = document.getElementById('saveAccountBtn');
statusEl.textContent = 'Pruefe Login...';
statusEl.className = 'account-modal-status checking';
saveBtn.disabled = true;
accountStatuses[hosterName] = { status: 'checking', message: '' };
syncSelectedUploadHosters();
renderAccounts();
renderHosterSummary();
renderHosterModal();
renderSettings();
// Run health check
try {
const rows = await executeHealthCheck([hosterName], 'auto');
const row = rows.find(r => r.hoster === hosterName);
if (row && row.status === 'ok') {
accountStatuses[hosterName] = { status: 'ok', message: row.message || '' };
statusEl.textContent = 'Login erfolgreich!';
statusEl.className = 'account-modal-status ok';
setTimeout(() => closeAccountModal(), 1200);
} else {
const msg = (row && row.message) || 'Login fehlgeschlagen';
accountStatuses[hosterName] = { status: 'error', message: msg };
statusEl.textContent = msg;
statusEl.className = 'account-modal-status error';
}
} catch (err) {
accountStatuses[hosterName] = { status: 'error', message: err.message || 'Pruefung fehlgeschlagen' };
statusEl.textContent = err.message || 'Pruefung fehlgeschlagen';
statusEl.className = 'account-modal-status error';
} finally {
saveBtn.disabled = false;
renderAccounts();
}
}
// --- 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 renderRecentUploadsPanel() {
const tbody = document.getElementById('recentFilesBody');
if (!tbody) return;
if (!sessionFilesData.length) {
tbody.innerHTML = '| Noch keine Uploads in dieser Session. |
';
return;
}
const rows = sessionFilesData
.slice()
.sort((a, b) => b.dateTs - a.dateTs || b.order - a.order)
.slice(0, 20);
tbody.innerHTML = rows.map(row => `
| ${escapeHtml(row.date)} |
${escapeHtml(row.filename)} |
${escapeHtml(row.host)} |
${escapeHtml(row.link)} |
`).join('');
tbody.querySelectorAll('.recent-file-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 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 += `
| ${escapeHtml(row.date)} |
${escapeHtml(row.filename)} |
${escapeHtml(row.host)} |
${escapeHtml(row.link)} |
`;
});
html += '
';
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('chooseHostersBtn').addEventListener('click', openHosterModal);
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('confirmHosterModalBtn').addEventListener('click', applyHosterSelection);
document.getElementById('cancelHosterModalBtn').addEventListener('click', cancelHosterModal);
document.getElementById('closeHosterModalBtn').addEventListener('click', cancelHosterModal);
document.getElementById('selectAllHostersBtn').addEventListener('click', () => {
document.querySelectorAll('input[data-hoster-modal]').forEach(input => {
input.checked = true;
input.closest('.hoster-option')?.classList.add('selected');
});
});
document.getElementById('clearHostersBtn').addEventListener('click', () => {
document.querySelectorAll('input[data-hoster-modal]').forEach(input => {
input.checked = false;
input.closest('.hoster-option')?.classList.remove('selected');
});
});
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 {}
});
}
// Virtual scroll for large queues
const queueContainer = document.getElementById('queueContainer');
if (queueContainer) queueContainer.addEventListener('scroll', _onQueueScroll, { passive: true });
// 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);
});
document.getElementById('hosterModal').addEventListener('click', (e) => {
if (e.target.id === 'hosterModal') cancelHosterModal();
});
// Account management
document.getElementById('addAccountBtn').addEventListener('click', () => openAccountModal(null));
document.getElementById('closeAccountModalBtn').addEventListener('click', closeAccountModal);
document.getElementById('cancelAccountModalBtn').addEventListener('click', closeAccountModal);
document.getElementById('saveAccountBtn').addEventListener('click', saveAccount);
document.getElementById('accountModal').addEventListener('click', (e) => {
if (e.target.id === 'accountModal') closeAccountModal();
});
// Account hoster select change → update credential fields
document.getElementById('accountHosterSelect').addEventListener('change', (e) => {
const credsContainer = document.getElementById('accountCredsFields');
credsContainer.innerHTML = getCredsFieldsHtml(e.target.value, {});
credsContainer.querySelectorAll('.toggle-vis').forEach(btn => {
btn.addEventListener('click', () => {
const input = btn.previousElementSibling;
input.type = input.type === 'password' ? 'text' : 'password';
});
});
document.getElementById('accountModalStatus').textContent = '';
document.getElementById('accountModalStatus').className = 'account-modal-status';
});
// Delete account modal
document.getElementById('closeDeleteModalBtn').addEventListener('click', closeDeleteModal);
document.getElementById('cancelDeleteBtn').addEventListener('click', closeDeleteModal);
document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
const modal = document.getElementById('deleteAccountModal');
const hoster = modal.dataset.hoster;
if (hoster) deleteAccount(hoster);
});
document.getElementById('deleteAccountModal').addEventListener('click', (e) => {
if (e.target.id === 'deleteAccountModal') closeDeleteModal();
});
}
// --- 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();