1055 lines
34 KiB
JavaScript
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}">×</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">👁</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">👁</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
function escapeAttr(str) {
|
|
if (!str) return '';
|
|
return str.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''');
|
|
}
|
|
|
|
// --- 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();
|