Multi-Hoster-Upload/renderer/app.js
2026-03-10 02:34:48 +01:00

1055 lines
34 KiB
JavaScript

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();
// 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);
}
// --- 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 = `
<input type="checkbox" data-hoster="${name}" ${hoster.enabled && hasCreds ? 'checked' : ''} ${!hasCreds ? 'disabled' : ''}>
<span class="hoster-dot"></span>
<span>${name}</span>
`;
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 `<div class="file-item">
<span class="file-name">${escapeHtml(f.name)}</span>
<span class="file-size">${sizeStr}</span>
<button class="remove-btn" data-index="${i}">&times;</button>
</div>`;
}).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 `<div class="health-check-badge ${status}">
<span class="health-check-hoster">${hoster}</span>
<span>[${tag}]</span>
<span class="health-check-msg">${message}</span>
</div>`;
}).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 = `<div class="file-title">${escapeHtml(file.name)}</div>`;
for (const hoster of hosters) {
const uid = `${file.path}__${hoster}`;
html += `
<div class="progress-row" data-uid="${uid}">
<span class="progress-hoster">${hoster}</span>
<div class="progress-track"><div class="progress-fill" id="fill-${uid}"></div></div>
<span class="progress-percent" id="pct-${uid}">0%</span>
<span class="progress-status" id="stat-${uid}">Warte...</span>
</div>`;
}
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 = '<p class="empty-state">Warte auf erste Upload-Ergebnisse...</p>';
return;
}
const sortedRows = sortResultsRows(resultsRowsData);
const headerCell = (key, label) => {
const active = resultsSortState.key === key;
const indicator = getResultsSortIndicator(key);
return `<th class="sortable${active ? ' active' : ''}" data-sort-key="${key}">${label}<span class="sort-indicator">${indicator}</span></th>`;
};
let html = `<table class="results-table">
<thead>
<tr>
${headerCell('date', 'Date')}
${headerCell('filename', 'Filename')}
${headerCell('host', 'Host')}
${headerCell('link', 'Link')}
</tr>
</thead>
<tbody>`;
sortedRows.forEach((row, index) => {
html += `<tr class="result-row${row.isError ? ' error' : ''}" data-index="${index}" data-link="${escapeAttr(row.link)}">
<td class="col-date">${escapeHtml(row.date)}</td>
<td class="col-filename">${escapeHtml(row.filename)}</td>
<td class="col-host">${escapeHtml(row.host)}</td>
<td class="col-link">${escapeHtml(row.link)}</td>
</tr>`;
});
html += '</tbody></table>';
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 = `
<div class="settings-row">
<span class="hoster-label">${name}</span>
<input type="text" class="key-input" data-hoster="${name}" data-field="username" value="${escapeAttr(hoster.username || '')}" placeholder="Username">
</div>
<div class="settings-row">
<span class="hoster-label"></span>
<input type="password" class="key-input" data-hoster="${name}" data-field="password" value="${escapeAttr(hoster.password || '')}" placeholder="Passwort">
<button class="toggle-vis" title="Passwort anzeigen/verbergen">&#128065;</button>
</div>
`;
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 = `
<span class="hoster-label">${name}</span>
<input type="password" class="key-input" data-hoster="${name}" data-field="apiKey" value="${escapeAttr(hoster.apiKey || '')}" placeholder="API Key eingeben...">
<button class="toggle-vis" title="Key anzeigen/verbergen">&#128065;</button>
`;
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 = '<p class="empty-state">Noch keine Uploads.</p>';
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 = '<p class="empty-state">Noch keine Uploads.</p>';
return;
}
const rows = sortHistoryRows(historyRowsData);
const headerCell = (key, label) => {
const active = historySortState.key === key;
const indicator = getHistorySortIndicator(key);
return `<th class="sortable${active ? ' active' : ''}" data-history-sort-key="${key}">${label}<span class="sort-indicator">${indicator}</span></th>`;
};
let html = `<table class="results-table history-table">
<thead>
<tr>
${headerCell('date', 'Date')}
${headerCell('filename', 'Filename')}
${headerCell('host', 'Host')}
${headerCell('link', 'Link')}
</tr>
</thead>
<tbody>`;
rows.forEach((row) => {
html += `<tr class="history-row${row.isError ? ' error' : ''}" data-link="${escapeAttr(row.link)}">
<td class="col-date">${escapeHtml(row.date)}</td>
<td class="col-filename">${escapeHtml(row.filename)}</td>
<td class="col-host">${escapeHtml(row.host)}</td>
<td class="col-link">${escapeHtml(row.link)}</td>
</tr>`;
});
html += '</tbody></table>';
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function escapeAttr(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
// --- 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'; }
}
}
// --- Start ---
init();