diff --git a/README.md b/README.md
index bf0b300..1906082 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Multi-Hoster-Upload is a Windows desktop application for sending file batches to
Download the current Setup or Portable build from the [latest GitHub release](https://github.com/Sucukdeluxe/Multi-Hoster-Upload/releases/latest).
-The latest public release is version 2.1.5. Use the release page for the executables and the full English changelog.
+The latest public release is version 2.1.6. Use the release page for the executables and the full English changelog.
## Features
diff --git a/main.js b/main.js
index ad88be0..b830100 100644
--- a/main.js
+++ b/main.js
@@ -1402,10 +1402,10 @@ function createWindow() {
try {
const choice = dialog.showMessageBoxSync(mainWindow, {
type: 'error',
- title: 'Renderer abgestürzt',
- message: `Der Renderer-Prozess ist abgestürzt (${details.reason}).`,
- detail: 'Bitte Diagnose-Paket exportieren und einsenden. Klick "Neu laden" um die UI wiederherzustellen — laufende Uploads im Main-Process bleiben aktiv.',
- buttons: ['Neu laden', 'Beenden'],
+ title: shellText('Renderer abgestürzt', 'Renderer crashed'),
+ message: shellText(`Der Renderer-Prozess ist abgestürzt (${details.reason}).`, `The renderer process crashed (${details.reason}).`),
+ detail: shellText('Bitte Diagnose-Paket exportieren und einsenden. Klick "Neu laden" um die UI wiederherzustellen — laufende Uploads im Main-Process bleiben aktiv.', 'Export and send a diagnostics package. Click "Reload" to restore the interface; uploads running in the main process remain active.'),
+ buttons: [shellText('Neu laden', 'Reload'), shellText('Beenden', 'Quit')],
defaultId: 0,
cancelId: 1
});
@@ -1460,14 +1460,7 @@ function createTray() {
} catch {}
}
tray = new Tray(icon || nativeImage.createEmpty());
- tray.setToolTip('Multi-Hoster-Upload');
-
- const contextMenu = Menu.buildFromTemplate([
- { label: 'Öffnen', click: () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } } },
- { type: 'separator' },
- { label: 'Beenden', click: () => { app.quit(); } }
- ]);
- tray.setContextMenu(contextMenu);
+ refreshTrayLanguage();
tray.on('click', () => {
if (mainWindow) { mainWindow.show(); mainWindow.focus(); }
@@ -1702,7 +1695,7 @@ ipcMain.handle('save-text-file', async (_event, defaultName, content, filters) =
? filters
: [{ name: 'Textdatei', extensions: ['txt', 'csv', 'log'] }];
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, {
- title: 'Speichern unter',
+ title: shellText('Speichern unter', 'Save as'),
defaultPath: safeName,
filters: safeFilters
});
@@ -1718,11 +1711,11 @@ ipcMain.handle('export-history', async (_event, format) => {
const datePrefix = new Date().toISOString().slice(0, 10);
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, {
- title: 'Upload-Verlauf exportieren',
+ title: shellText('Upload-Verlauf exportieren', 'Export upload history'),
defaultPath: `upload-history-${datePrefix}.${normalizedFormat}`,
filters: normalizedFormat === 'json'
- ? [{ name: 'JSON-Datei', extensions: ['json'] }]
- : [{ name: 'CSV-Datei', extensions: ['csv'] }]
+ ? [{ name: shellText('JSON-Datei', 'JSON file'), extensions: ['json'] }]
+ : [{ name: shellText('CSV-Datei', 'CSV file'), extensions: ['csv'] }]
});
if (canceled || !filePath) return { ok: false, canceled: true };
@@ -1810,6 +1803,25 @@ function getUploadBrowseDirectory() {
return app.getPath('downloads');
}
+function getConfiguredLanguage() {
+ try { return configStore.load().globalSettings?.language === 'de' ? 'de' : 'en'; }
+ catch { return 'en'; }
+}
+
+function shellText(german, english) {
+ return getConfiguredLanguage() === 'de' ? german : english;
+}
+
+function refreshTrayLanguage() {
+ if (!tray || tray.isDestroyed()) return;
+ tray.setToolTip('Multi Hoster Uploader');
+ tray.setContextMenu(Menu.buildFromTemplate([
+ { label: shellText('Öffnen', 'Open'), click: () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } } },
+ { type: 'separator' },
+ { label: shellText('Beenden', 'Quit'), click: () => { app.quit(); } }
+ ]));
+}
+
async function rememberUploadBrowseDirectory(selectedPath, selectedDirectory = false) {
if (!selectedPath) return;
const directory = selectedDirectory ? selectedPath : path.dirname(selectedPath);
@@ -1821,8 +1833,8 @@ ipcMain.handle('select-files', async () => {
defaultPath: getUploadBrowseDirectory(),
properties: ['openFile', 'multiSelections'],
filters: [
- { name: 'Alle Dateien', extensions: ['*'] },
- { name: 'Videos', extensions: ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm'] }
+ { name: shellText('Alle Dateien', 'All files'), extensions: ['*'] },
+ { name: shellText('Videos', 'Videos'), extensions: ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm'] }
]
});
if (result.canceled || !result.filePaths.length) return null;
@@ -2368,7 +2380,7 @@ ipcMain.handle('create-support-bundle', async () => {
const defaultName = `multi-hoster-support-${stamp}.txt`;
const desktop = (() => { try { return app.getPath('desktop'); } catch { return app.getPath('userData'); } })();
const res = await dialog.showSaveDialog(mainWindow || undefined, {
- title: 'Diagnose-Paket speichern',
+ title: shellText('Diagnose-Paket speichern', 'Save diagnostics package'),
defaultPath: path.join(desktop, defaultName),
filters: [{ name: 'Text', extensions: ['txt'] }]
});
@@ -2516,11 +2528,11 @@ ipcMain.handle('export-backup', async () => {
const _bd = new Date();
const _bdate = `${String(_bd.getDate()).padStart(2, '0')}-${String(_bd.getMonth() + 1).padStart(2, '0')}-${_bd.getFullYear()}`;
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, {
- title: 'Backup exportieren',
+ title: shellText('Backup exportieren', 'Export backup'),
defaultPath: `${_bdate}-multihoster-backup.mhu`,
filters: [
- { name: 'Multi-Hoster Backup (verschlüsselt)', extensions: ['mhu'] },
- { name: 'Multi-Hoster Backup (Klartext JSON)', extensions: ['json'] }
+ { name: shellText('Multi-Hoster Backup (verschlüsselt)', 'Multi Hoster backup (encrypted)'), extensions: ['mhu'] },
+ { name: shellText('Multi-Hoster Backup (Klartext JSON)', 'Multi Hoster backup (plain JSON)'), extensions: ['json'] }
]
});
if (canceled || !filePath) return { ok: false, canceled: true };
@@ -2542,9 +2554,9 @@ ipcMain.handle('import-backup', async (_event, legacyPassword) => {
buffer = readBackupFile(sourcePath);
} else {
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
- title: 'Backup importieren',
+ title: shellText('Backup importieren', 'Import backup'),
filters: [
- { name: 'Multi-Hoster Backup', extensions: ['mhu', 'json'] },
+ { name: 'Multi Hoster backup', extensions: ['mhu', 'json'] },
{ name: 'Verschlüsselt (.mhu)', extensions: ['mhu'] },
{ name: 'Klartext (.json)', extensions: ['json'] }
],
@@ -2642,10 +2654,10 @@ ipcMain.handle('read-own-upload-log', () => {
ipcMain.handle('import-upload-log', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
- title: 'Upload-Log importieren',
+ title: shellText('Upload-Log importieren', 'Import upload log'),
filters: [
- { name: 'Log-Dateien', extensions: ['log', 'txt'] },
- { name: 'Alle Dateien', extensions: ['*'] }
+ { name: shellText('Log-Dateien', 'Log files'), extensions: ['log', 'txt'] },
+ { name: shellText('Alle Dateien', 'All files'), extensions: ['*'] }
],
properties: ['openFile']
});
@@ -2807,6 +2819,7 @@ ipcMain.handle('save-global-settings', async (_event, globalSettings) => {
if (uploadManager) {
try { uploadManager.updateSettings(null, globalSettings); } catch (error) { debugLog(`global settings runtime update failed: ${error.message}`); }
}
+ refreshTrayLanguage();
return true;
});
diff --git a/package-lock.json b/package-lock.json
index 6e06ed0..7708c6a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "multi-hoster-uploader",
- "version": "2.1.5",
+ "version": "2.1.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "multi-hoster-uploader",
- "version": "2.1.5",
+ "version": "2.1.6",
"dependencies": {
"chokidar": "^3.6.0",
"undici": "^7.29.0",
diff --git a/package.json b/package.json
index 17f55c0..8e97b5c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "multi-hoster-uploader",
- "version": "2.1.5",
+ "version": "2.1.6",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js",
"scripts": {
diff --git a/renderer/app.js b/renderer/app.js
index 7727560..66305b1 100644
--- a/renderer/app.js
+++ b/renderer/app.js
@@ -3,13 +3,43 @@ const uiLocalizer = window.I18n.createDomLocalizer(document);
uiLocalizer.start('en');
function setUiLanguage(value) {
- return uiLocalizer.setLanguage(window.I18n.normalizeLanguage(value));
+ const language = uiLocalizer.setLanguage(window.I18n.normalizeLanguage(value));
+ if (document.body && typeof queueJobs !== 'undefined') refreshLocalizedRuntimeUi();
+ return language;
}
function getUiLocale() {
return uiLocalizer.getLanguage() === 'de' ? 'de-DE' : 'en-US';
}
+function localizeUiText(value) {
+ return window.I18n.translateText(value, uiLocalizer.getLanguage());
+}
+
+function formatRemoteClientStatus(port, count) {
+ const clients = count === 1 ? '1 Client' : `${count} Clients`;
+ return localizeUiText(`Aktiv auf Port ${port} — ${clients} verbunden`);
+}
+
+function refreshLocalizedRuntimeUi() {
+ renderQueueTable();
+ renderAccounts();
+ renderRecentUploadsPanel();
+ historyRowsData.forEach(row => {
+ if (row.rawTimestamp !== undefined) {
+ const formatted = formatDateTime(row.rawTimestamp);
+ row.date = formatted.text;
+ row.dateTs = formatted.ts;
+ }
+ });
+ const historyContainer = document.getElementById('historyContainer');
+ if (historyContainer && historyRowsData.length) renderHistoryTable(historyContainer);
+ updateStatusBar();
+ const activeRecentTab = document.querySelector('.recent-tab.active');
+ const hint = document.getElementById('recentFilesHint');
+ if (hint && activeRecentTab) hint.textContent = localizeUiText(activeRecentTab.dataset.panel === 'statsTab' ? 'Upload-Statistiken' : 'Zuletzt erzeugte Upload-Links');
+}
+
// Dropdown options for "Add Account" modal: value -> label
const HOSTER_ADD_OPTIONS = [
{ value: 'doodstream.com', label: 'Doodstream (Web Login)', hoster: 'doodstream.com', authType: 'login' },
@@ -492,7 +522,7 @@ async function init() {
if (el && el.style.color === 'rgb(16, 185, 129)') {
window.api.remoteStatus().then(status => {
if (status.running) {
- el.textContent = `Aktiv auf Port ${status.port} — ${status.clientCount} Client(s) verbunden`;
+ el.textContent = formatRemoteClientStatus(status.port, status.clientCount);
}
}).catch(() => {});
}
@@ -627,6 +657,7 @@ function initMenuBar() {
for (const k in dropdowns) closePanel(dropdowns[k]);
for (const k in triggers) triggers[k].classList.remove('open');
menuBar.querySelectorAll('.menu-submenu-dropdown').forEach(closePanel);
+ menuBar.querySelectorAll('.menu-submenu-trigger').forEach(trigger => trigger.setAttribute('aria-expanded', 'false'));
}
function openMenuNamed(name) {
@@ -649,8 +680,34 @@ function initMenuBar() {
menuBar.querySelectorAll('.menu-submenu').forEach(sm => {
const sub = sm.querySelector('.menu-submenu-dropdown');
- sm.addEventListener('mouseenter', () => openPanel(sub));
- sm.addEventListener('mouseleave', () => closePanel(sub));
+ const trigger = sm.querySelector('.menu-submenu-trigger');
+ const openSubmenu = (focusFirst = false) => {
+ openPanel(sub);
+ trigger?.setAttribute('aria-expanded', 'true');
+ if (focusFirst) requestAnimationFrame(() => sub.querySelector('[data-menu-action]')?.focus());
+ };
+ const closeSubmenu = () => {
+ closePanel(sub);
+ trigger?.setAttribute('aria-expanded', 'false');
+ };
+ sm.addEventListener('mouseenter', () => openSubmenu());
+ sm.addEventListener('mouseleave', closeSubmenu);
+ trigger?.addEventListener('click', (event) => {
+ event.stopPropagation();
+ if (trigger.getAttribute('aria-expanded') === 'true') closeSubmenu();
+ else openSubmenu();
+ });
+ trigger?.addEventListener('keydown', (event) => {
+ if (!['Enter', ' ', 'ArrowRight', 'ArrowDown'].includes(event.key)) return;
+ event.preventDefault();
+ openSubmenu(true);
+ });
+ sub.addEventListener('keydown', (event) => {
+ if (event.key !== 'Escape' && event.key !== 'ArrowLeft') return;
+ event.preventDefault();
+ closeSubmenu();
+ trigger?.focus();
+ });
});
menuBar.querySelectorAll('[data-menu-action]').forEach(item => {
@@ -676,7 +733,7 @@ async function _handleMenuAction(action) {
case 'backup-import': doBackupImport(); break;
case 'online-backup-create': doOnlineBackupCreate(); break;
case 'online-backup-restore': openOnlineBackupRestore(); break;
- case 'restart': if (confirm('Anwendung neu starten?')) window.api.restartApp(); break;
+ case 'restart': if (await showAppConfirm({ title: 'Anwendung neu starten?', message: 'Nicht gespeicherte laufende Aktionen werden beendet.', confirmText: 'Neustart', danger: true })) window.api.restartApp(); break;
case 'quit': window.api.quitApp(); break;
case 'open-settings': document.querySelector('.tab[data-view="settings"]')?.click(); break;
case 'open-log-folder': window.api.openLogFolder(); break;
@@ -1234,6 +1291,11 @@ function setupDragDrop() {
addDroppedFiles(e.dataTransfer.files).catch(console.error);
});
dropZone.addEventListener('click', () => pickFiles());
+ dropZone.addEventListener('keydown', (event) => {
+ if (!['Enter', ' '].includes(event.key)) return;
+ event.preventDefault();
+ pickFiles();
+ });
// Also handle drops on queue container
uploadView.addEventListener('dragover', (e) => { e.preventDefault(); });
@@ -1425,6 +1487,21 @@ function updateQueueActionButtons() {
if (moveUpBtn) moveUpBtn.disabled = !hasMovableSelection;
if (moveDownBtn) moveDownBtn.disabled = !hasMovableSelection;
if (moveBottomBtn) moveBottomBtn.disabled = !hasMovableSelection;
+ syncDataActionState();
+}
+
+function syncDataActionState() {
+ const completedLinks = queueJobs.some(job => job.status === 'done' && (job.result?.download_url || job.result?.embed_url));
+ const hasRecent = sessionFilesData.length > 0;
+ const hasHistory = historyRowsData.length > 0;
+ const copyAll = document.getElementById('copyAllLinksBtn');
+ const clearRecent = document.getElementById('clearRecentFilesBtn');
+ const exportRecent = document.getElementById('exportRecentFilesBtn');
+ const exportHistoryButton = document.getElementById('exportHistoryBtn');
+ if (copyAll) copyAll.disabled = !completedLinks;
+ if (clearRecent) clearRecent.disabled = !hasRecent;
+ if (exportRecent) exportRecent.disabled = !hasRecent;
+ if (exportHistoryButton) exportHistoryButton.disabled = !hasHistory;
}
function clearDedupKeysForPaths(pathSet) {
@@ -1647,11 +1724,11 @@ function buildRowHtml(job) {
const pct = Math.min(100, Math.round((job.progress || 0) * 100));
const link = job.result ? (job.result.download_url || job.result.embed_url || '') : '';
- return `
+ return `
| ${escapeHtml(job.fileName)} |
${uploadedSize} |
${escapeHtml(job.hoster)} |
- ${escapeHtml(statusText)} |
+ ${escapeHtml(statusText)} |
${elapsed} |
${remaining} |
${speed} |
@@ -1684,6 +1761,7 @@ function _updateRowInPlace(tr, job) {
const newClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`;
if (tr.className !== newClass) tr.className = newClass;
if (tr.dataset.link !== link) tr.dataset.link = link;
+ tr.setAttribute('aria-selected', String(selectedJobIds.has(job.id)));
const cells = tr.children;
if (cells.length < 8) return false; // structure mismatch, needs full rebuild
@@ -1694,8 +1772,10 @@ function _updateRowInPlace(tr, job) {
if (badge) {
const badgeClass = `status-badge ${statusClass}`;
if (badge.className !== badgeClass) badge.className = badgeClass;
- if (badge.textContent !== statusText) badge.textContent = statusText;
+ const label = badge.querySelector('.status-badge-label') || badge;
+ if (label.textContent !== statusText) label.textContent = statusText;
}
+ if (cells[3].title !== statusText) cells[3].title = statusText;
if (cells[4].textContent !== elapsed) cells[4].textContent = elapsed;
if (cells[5].textContent !== remaining) cells[5].textContent = remaining;
if (cells[6].textContent !== speed) cells[6].textContent = speed;
@@ -1769,6 +1849,10 @@ function renderQueueTable() {
_sortedJobsCache = sortQueueJobs(visibleJobs);
if (selectionChanged) updateQueueActionButtons();
const totalRows = _sortedJobsCache.length;
+ const queueContainer = document.getElementById('queueContainer');
+ const filteredEmpty = totalRows === 0 && queueJobs.length > 0 && uploadSidebarFilter !== 'all';
+ queueContainer?.classList.toggle('filter-empty', filteredEmpty);
+ if (queueContainer) queueContainer.dataset.emptyLabel = filteredEmpty ? localizeUiText('Keine Uploads entsprechen diesem Filter.') : '';
if (totalRows < 200) {
// Try in-place update if row count matches (fast path)
@@ -1807,14 +1891,36 @@ function renderQueueTable() {
const row = e.target.closest('.queue-row');
if (row) handleRowContextMenu(e, row);
});
+ tbody.addEventListener('keydown', (e) => {
+ const row = e.target.closest('.queue-row');
+ if (!row) return;
+ if ((e.shiftKey && e.key === 'F10') || e.key === 'ContextMenu') {
+ e.preventDefault();
+ const rect = row.getBoundingClientRect();
+ handleRowContextMenu({ preventDefault() {}, clientX: rect.left + 24, clientY: rect.top + 20 }, row);
+ return;
+ }
+ if (['Enter', ' '].includes(e.key)) {
+ e.preventDefault();
+ handleRowClick(e, row);
+ }
+ });
}
// Update retry button visibility
const hasFailedJobs = queueJobs.some(j => j.status === 'error');
document.getElementById('retryFailedBtn').style.display = hasFailedJobs ? 'inline-block' : 'none';
+ syncQueueSortHeaders();
updateQueueActionButtons();
}
+function syncQueueSortHeaders() {
+ document.querySelectorAll('#queueTable th.sortable').forEach(header => {
+ const active = header.dataset.sort === queueSortState.key;
+ header.setAttribute('aria-sort', active ? (queueSortState.direction === 'asc' ? 'ascending' : 'descending') : 'none');
+ });
+}
+
function _renderVirtualRows(tbody) {
const scrollContainer = document.getElementById('queueContainer');
if (!scrollContainer) return;
@@ -1945,28 +2051,31 @@ function getAccountLabel(job) {
if (!Array.isArray(accounts)) return '';
const idx = accounts.findIndex(a => a && a.id === job.accountId);
if (idx < 0) return '';
- return idx === 0 ? 'Primär' : `Fallback #${idx}`;
+ return idx === 0 ? localizeUiText('Primär') : `Fallback #${idx}`;
}
function getStatusText(job) {
const shortErr = job.error ? String(job.error).replace(/\s+/g, ' ').slice(0, 100) : '';
const acc = getAccountLabel(job);
const accSuffix = acc ? ` · ${acc}` : '';
+ let text;
switch (job.status) {
- case 'preview': return 'Bereit';
- case 'queued': return 'Wartet';
- case 'getting-server': return `Server...${accSuffix}`;
- case 'uploading': return `Upload${accSuffix}`;
+ case 'preview': text = 'Bereit'; break;
+ case 'queued': text = 'Wartet'; break;
+ case 'getting-server': text = `Server…${accSuffix}`; break;
+ case 'uploading': text = `Upload${accSuffix}`; break;
case 'retrying': {
const base = `Retry ${job.attempt}/${job.maxAttempts}${accSuffix}`;
- return shortErr ? `${base}: ${shortErr}` : base;
+ text = shortErr ? `${base}: ${shortErr}` : base;
+ break;
}
- case 'done': return 'Fertig';
- case 'aborted': return 'Abgebrochen';
- case 'error': return shortErr ? `Fehlgeschlagen: ${shortErr}` : 'Fehlgeschlagen';
- case 'skipped': return shortErr ? `Übersprungen: ${shortErr}` : 'Übersprungen';
- default: return job.status;
+ case 'done': text = 'Fertig'; break;
+ case 'aborted': text = shortErr || 'Abgebrochen'; break;
+ case 'error': text = shortErr ? (/^Fehlgeschlagen(?::|$)/.test(shortErr) ? shortErr : `Fehlgeschlagen: ${shortErr}`) : 'Fehlgeschlagen'; break;
+ case 'skipped': text = shortErr ? `Übersprungen: ${shortErr}` : 'Übersprungen'; break;
+ default: text = job.status;
}
+ return localizeUiText(text);
}
// --- Queue interactions ---
@@ -2013,6 +2122,7 @@ let alwaysOnTopState = false;
// to queueJobs (the length-based signature is good enough — a job's hoster
// never changes after it's created).
let _hosterCountsCache = { sig: '', result: new Map() };
+let contextMenuReturnFocus = null;
function _getHosterCounts() {
const sig = `${queueJobs.length}`;
if (_hosterCountsCache.sig === sig) return _hosterCountsCache.result;
@@ -2066,6 +2176,8 @@ function showContextMenu(x, y) {
const item = document.createElement('div');
item.className = 'ctx-item ctx-item-danger';
item.dataset.action = `delete-hoster:${hoster}`;
+ item.setAttribute('role', 'menuitem');
+ item.tabIndex = -1;
item.textContent = `${getHosterLabel(hoster)} (${count})`;
deleteHosterContainer.appendChild(item);
});
@@ -2077,6 +2189,8 @@ function showContextMenu(x, y) {
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';
+ contextMenuReturnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
+ getVisibleContextMenuItems(menu)[0]?.focus();
// Flip submenus if they would overflow the viewport right edge
menu.querySelectorAll('.ctx-submenu-items').forEach(sub => {
@@ -2089,13 +2203,37 @@ function showContextMenu(x, y) {
});
}
+function getVisibleContextMenuItems(menu) {
+ return [...menu.querySelectorAll('.ctx-item[data-action]')].filter(item => window.getComputedStyle(item).display !== 'none');
+}
+
+function showRecentContextMenu(row, x, y) {
+ const id = parseInt(row.dataset.order, 10);
+ if (!selectedRecentIds.has(id)) {
+ selectedRecentIds.clear();
+ selectedRecentIds.add(id);
+ applyRecentSelectionClasses();
+ }
+ const menu = document.getElementById('recentContextMenu');
+ menu.style.display = 'block';
+ menu.style.left = Math.min(x, window.innerWidth - menu.offsetWidth - 5) + 'px';
+ menu.style.top = Math.min(y, window.innerHeight - menu.offsetHeight - 5) + 'px';
+ contextMenuReturnFocus = row;
+ getVisibleContextMenuItems(menu)[0]?.focus();
+}
+
function hideContextMenu() {
document.getElementById('contextMenu').style.display = 'none';
document.getElementById('recentContextMenu').style.display = 'none';
+ const returnFocus = contextMenuReturnFocus;
+ contextMenuReturnFocus = null;
+ if (returnFocus?.isConnected) returnFocus.focus();
}
-function deleteSelectedRecentFiles() {
+async function deleteSelectedRecentFiles() {
if (selectedRecentIds.size === 0) return;
+ const count = selectedRecentIds.size;
+ if (!await showAppConfirm({ title: 'Ausgewählte Einträge entfernen?', message: count === 1 ? 'Ein ausgewählter Eintrag wird aus diesem Panel entfernt.' : `${count} ausgewählte Einträge werden aus diesem Panel entfernt.`, confirmText: 'Entfernen', danger: true })) return;
let removedDone = 0, removedErr = 0;
sessionFilesData = sessionFilesData.filter(r => {
if (!selectedRecentIds.has(r.order)) return true;
@@ -2110,9 +2248,9 @@ function deleteSelectedRecentFiles() {
renderRecentUploadsPanel();
}
-function clearAllRecentFiles() {
+async function clearAllRecentFiles() {
if (sessionFilesData.length === 0) return;
- if (!confirm(`Wirklich alle ${sessionFilesData.length} Links aus diesem Panel entfernen?`)) return;
+ if (!await showAppConfirm({ title: 'Alle Links entfernen?', message: `Wirklich alle ${sessionFilesData.length} Links aus diesem Panel entfernen?`, confirmText: 'Alle entfernen', danger: true })) return;
sessionFilesData = [];
_sessionFileKeys.clear();
_sessionDoneCount = 0;
@@ -2124,7 +2262,7 @@ function clearAllRecentFiles() {
async function exportAllRecentFiles() {
if (sessionFilesData.length === 0) {
- alert('Keine Einträge zum Exportieren.');
+ await showAppAlert('Keine Einträge zum Exportieren.');
return;
}
const rows = sortRecentFiles(sessionFilesData);
@@ -2145,7 +2283,7 @@ async function exportAllRecentFiles() {
]);
if (result && result.ok) showCopyToast(`${rows.length} Einträge exportiert`);
} catch (err) {
- alert('Export fehlgeschlagen: ' + (err.message || err));
+ await showAppAlert('Export fehlgeschlagen: ' + (err.message || err), 'Export fehlgeschlagen');
}
}
@@ -2164,7 +2302,7 @@ async function doBackupExport() {
const result = await window.api.exportBackup();
if (result && result.ok) showCopyToast('Backup exportiert');
} catch (err) {
- alert('Export fehlgeschlagen: ' + (err.message || err));
+ await showAppAlert('Export fehlgeschlagen: ' + (err.message || err), 'Export fehlgeschlagen');
}
}
@@ -2392,14 +2530,14 @@ async function doBackupImport(legacyPassword) {
}
if (result.ok) {
if (Array.isArray(result.warnings) && result.warnings.length) {
- alert(`Backup importiert. Bitte prüfen: ${result.warnings.join(', ')}.`);
+ await showAppAlert(`Backup importiert. Bitte prüfen: ${result.warnings.join(', ')}.`, 'Backup importiert');
}
if (queuePersistenceError) showImportQueuePersistenceError(queuePersistenceError);
} else if (result.error) {
- alert('Import fehlgeschlagen: ' + result.error);
+ await showAppAlert('Import fehlgeschlagen: ' + result.error, 'Import fehlgeschlagen');
}
} catch (err) {
- alert('Import fehlgeschlagen: ' + (err.message || err));
+ await showAppAlert('Import fehlgeschlagen: ' + (err.message || err), 'Import fehlgeschlagen');
}
}
@@ -2449,29 +2587,7 @@ document.addEventListener('keydown', (e) => {
if (selectedRecentIds.size > 0) {
deleteSelectedRecentFiles();
} else if (selectedJobIds.size > 0) {
- const deletedIds = [...selectedJobIds];
- // Cancel active uploads for deleted jobs
- const activeIds = deletedIds.filter(id => {
- const j = _jobIndexById.get(id);
- return j && (j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server');
- });
- if (activeIds.length > 0) window.api.cancelSelectedJobs(activeIds);
- const _deletedKeys = [];
- queueJobs = queueJobs.filter(j => {
- if (selectedJobIds.has(j.id)) {
- if (j.file && j.hoster && j.status !== 'done') _deletedKeys.push(`${j.file}|${j.hoster}`);
- removeJobFromIndex(j);
- return false;
- }
- return true;
- });
- selectedJobIds.clear();
- syncSelectedFilesFromQueue();
- suppressPreviewKeysStillSelected(_deletedKeys);
- renderQueueTable();
- if (queueJobs.length === 0) { selectedFiles = []; updateUploadView(); }
- updateStatusBar();
- persistQueueStateSoon(true);
+ handleContextAction('delete-selected');
}
}
}
@@ -2498,7 +2614,8 @@ async function handleContextAction(action) {
} else if (action === 'show-log') {
showJobLogModal();
} else if (action === 'delete-selected') {
- // Cancel active uploads for deleted jobs
+ const count = selectedJobIds.size;
+ if (!count || !await showAppConfirm({ title: 'Uploads entfernen?', message: count === 1 ? 'Ein ausgewählter Upload wird aus der Liste entfernt.' : `${count} ausgewählte Uploads werden aus der Liste entfernt.`, confirmText: 'Entfernen', danger: true })) return;
const activeIds = [...selectedJobIds].filter(id => {
const j = _jobIndexById.get(id);
return j && (j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server');
@@ -2523,7 +2640,7 @@ async function handleContextAction(action) {
} else if (action === 'copy-all-links') {
copyAllLinks();
} else if (action === 'delete-all') {
- // Cancel all active uploads
+ if (!queueJobs.length || !await showAppConfirm({ title: 'Alle Uploads entfernen?', message: `${queueJobs.length} ${queueJobs.length === 1 ? 'Upload wird' : 'Uploads werden'} aus der Liste entfernt.`, confirmText: 'Alle entfernen', danger: true })) return;
const activeIds = queueJobs
.filter(j => j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server')
.map(j => j.id);
@@ -2543,7 +2660,8 @@ async function handleContextAction(action) {
config.globalSettings = { ...(config.globalSettings || {}), alwaysOnTop: alwaysOnTopState };
} else if (action.startsWith('delete-hoster:')) {
const hoster = action.replace('delete-hoster:', '');
- // Cancel active uploads for this hoster
+ const hosterCount = queueJobs.filter(job => job.hoster === hoster).length;
+ if (!hosterCount || !await showAppConfirm({ title: 'Hoster entfernen?', message: `${hosterCount} ${hosterCount === 1 ? 'Upload wird' : 'Uploads werden'} für ${getHosterLabel(hoster)} entfernt.`, confirmText: 'Hoster entfernen', danger: true })) return;
const activeIds = queueJobs
.filter(j => j.hoster === hoster && (j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server' || j.status === 'preview'))
.map(j => j.id);
@@ -2584,7 +2702,7 @@ async function startUpload(opts) {
const hosters = getSelectedHosters();
if (queueJobs.length === 0 && selectedFiles.length > 0) {
if (hosters.length === 0) {
- alert('Bitte mindestens einen Hoster auswählen.');
+ await showAppAlert('Bitte mindestens einen Hoster auswählen.');
uploading = false;
updateQueueActionButtons();
return;
@@ -2626,7 +2744,7 @@ async function startUpload(opts) {
persistQueueStateSoon();
if (result && result.error) {
- alert(result.error);
+ await showAppAlert(result.error, 'Upload-Start fehlgeschlagen');
uploading = false;
updateQueueActionButtons();
updateStatusBar();
@@ -2635,7 +2753,7 @@ async function startUpload(opts) {
uploading = false;
updateQueueActionButtons();
updateStatusBar();
- alert(`Upload-Start fehlgeschlagen: ${err.message}`);
+ await showAppAlert(`Upload-Start fehlgeschlagen: ${err.message}`, 'Upload-Start fehlgeschlagen');
}
}
@@ -2741,7 +2859,7 @@ async function startSelectedUpload(explicitJobs) {
persistQueueStateSoon();
if (result && result.error) {
- alert(result.error);
+ await showAppAlert(result.error, 'Upload-Start fehlgeschlagen');
uploading = false;
updateQueueActionButtons();
updateStatusBar();
@@ -2750,7 +2868,7 @@ async function startSelectedUpload(explicitJobs) {
uploading = false;
updateQueueActionButtons();
updateStatusBar();
- alert(`Upload-Start fehlgeschlagen: ${err.message}`);
+ await showAppAlert(`Upload-Start fehlgeschlagen: ${err.message}`, 'Upload-Start fehlgeschlagen');
}
}
@@ -3076,7 +3194,7 @@ function _retryFailedFromBuckets(buckets, transientOnly) {
}
if (jobsToRetry.length === 0) { showCopyToast('Keine passenden Jobs für Retry gefunden.'); return; }
renderQueueTable();
- showCopyToast(`${jobsToRetry.length} Job(s) zum erneuten Upload zurückgesetzt`);
+ showCopyToast(jobsToRetry.length === 1 ? '1 Job zum erneuten Upload zurückgesetzt' : `${jobsToRetry.length} Jobs zum erneuten Upload zurückgesetzt`);
if (typeof startUpload === 'function') startUpload();
}
@@ -3263,6 +3381,8 @@ async function finishUploadsInProgress() {
}
async function abortAllUploads() {
+ if (!uploading) return;
+ if (!await showAppConfirm({ title: 'Alle Uploads abbrechen?', message: 'Alle laufenden Uploads werden abgebrochen und in die Warteschlange zurückgesetzt.', confirmText: 'Alle abbrechen', danger: true })) return;
await cancelUpload();
}
@@ -3553,16 +3673,30 @@ function _getAccountSidebarCategory(name, account) {
function _applyAccountSidebarFilter() {
const entries = new Map(getAllAccountsFlat().map(entry => [entry.account.id, entry]));
+ const list = document.getElementById('accountsList');
+ let visibleAccounts = 0;
document.querySelectorAll('#accountsList .account-hoster-group').forEach(group => {
let matches = 0;
group.querySelectorAll('.account-card').forEach(card => {
const entry = entries.get(card.dataset.accountId);
const visible = !!entry && (accountSidebarFilter === 'all' || _getAccountSidebarCategory(entry.name, entry.account) === accountSidebarFilter);
card.hidden = !visible;
- if (visible) matches++;
+ if (visible) { matches++; visibleAccounts++; }
});
group.hidden = matches === 0;
});
+ let empty = list?.querySelector('.account-filter-empty');
+ if (visibleAccounts === 0 && entries.size > 0) {
+ if (!empty) {
+ empty = document.createElement('div');
+ empty.className = 'empty-state account-filter-empty';
+ list.appendChild(empty);
+ }
+ empty.textContent = localizeUiText('Keine Accounts entsprechen diesem Filter.');
+ empty.hidden = false;
+ } else if (empty) {
+ empty.hidden = true;
+ }
}
function setAccountSidebarFilter(value) {
@@ -3759,32 +3893,77 @@ function renderHealthCheckResults(_results) {
}
let _appAlertResolve = null;
+let _appAlertReturnFocus = null;
+let _appAlertInertState = [];
-function closeAppAlert() {
+function _setAppAlertBackgroundInert(active) {
+ const modal = document.getElementById('appAlertModal');
+ if (!modal) return;
+ if (active) {
+ if (_appAlertInertState.length > 0) return;
+ _appAlertInertState = Array.from(document.body.children)
+ .filter(element => element !== modal && 'inert' in element)
+ .map(element => ({ element, inert: element.inert }));
+ _appAlertInertState.forEach(({ element }) => { element.inert = true; });
+ return;
+ }
+ _appAlertInertState.forEach(({ element, inert }) => {
+ if (element.isConnected) element.inert = inert;
+ });
+ _appAlertInertState = [];
+}
+
+function closeAppAlert(result = false) {
const modal = document.getElementById('appAlertModal');
if (!modal) return;
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
+ _setAppAlertBackgroundInert(false);
const resolve = _appAlertResolve;
_appAlertResolve = null;
- if (resolve) resolve();
+ const returnFocus = _appAlertReturnFocus;
+ _appAlertReturnFocus = null;
+ if (returnFocus?.isConnected) returnFocus.focus();
+ if (resolve) resolve(result);
}
-function showAppAlert(message, title = 'Hinweis') {
+function showAppDialog({ message, title = 'Hinweis', confirmText = 'OK', cancelText = 'Abbrechen', alternateText = '', showCancel = false, danger = false } = {}) {
const modal = document.getElementById('appAlertModal');
const titleEl = document.getElementById('appAlertTitle');
const messageEl = document.getElementById('appAlertMessage');
const confirm = document.getElementById('appAlertConfirmBtn');
- if (!modal || !titleEl || !messageEl || !confirm) return Promise.resolve();
- if (_appAlertResolve) closeAppAlert();
- titleEl.textContent = title;
- messageEl.textContent = String(message || '');
+ const cancel = document.getElementById('appAlertCancelBtn');
+ const alternate = document.getElementById('appAlertAlternateBtn');
+ if (!modal || !titleEl || !messageEl || !confirm || !cancel || !alternate) return Promise.resolve(false);
+ if (_appAlertResolve) closeAppAlert(false);
+ _appAlertReturnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
+ titleEl.textContent = localizeUiText(title);
+ messageEl.textContent = localizeUiText(String(message || ''));
+ confirm.textContent = localizeUiText(confirmText);
+ confirm.className = `btn ${danger ? 'btn-danger' : 'btn-primary'}`;
+ cancel.textContent = localizeUiText(cancelText);
+ cancel.hidden = !showCancel;
+ alternate.textContent = localizeUiText(alternateText);
+ alternate.hidden = !alternateText;
modal.style.display = 'flex';
modal.setAttribute('aria-hidden', 'false');
- confirm.focus();
+ _setAppAlertBackgroundInert(true);
+ (showCancel ? cancel : confirm).focus();
return new Promise(resolve => { _appAlertResolve = resolve; });
}
+function showAppAlert(message, title = 'Hinweis') {
+ return showAppDialog({ message, title });
+}
+
+function showAppConfirm({ message, title = 'Bestätigen', confirmText = 'Bestätigen', cancelText = 'Abbrechen', danger = false } = {}) {
+ return showAppDialog({ message, title, confirmText, cancelText, showCancel: true, danger });
+}
+
+function showAppChoice({ message, title, confirmText, alternateText, cancelText = 'Abbrechen' } = {}) {
+ return showAppDialog({ message, title, confirmText, alternateText, cancelText, showCancel: true });
+}
+
async function executeHealthCheck(hosters, _mode) {
renderHealthCheckResults([]);
const result = await window.api.runHealthCheck({ hosters });
@@ -3881,14 +4060,14 @@ function renderSettings() {
const remoteSettings = globalSettings.remote || {};
const pageDefinitions = [
- { id: 'allgemein', label: 'Allgemein', search: 'fenster vordergrund drop target oberfläche updates aktualisierung version' },
- { id: 'uploads', label: 'Uploads', search: 'upload queue warteschlange fertig abschluss entfernen parallel geschwindigkeit speed limit fortsetzen wiederherstellen hoster' },
- { id: 'automatik', label: 'Automatik', search: 'automatisch retry wiederholen ordner überwachen dateierweiterungen unterordner duplikate' },
- { id: 'benachrichtigungen', label: 'Benachrichtigungen', search: 'webhook discord meldung ping erwähnung batch fertig' },
- { id: 'logs', label: 'Logs & Support', search: 'log protokoll debug verbose diagnose support paket datei ordner' },
- { id: 'remote', label: 'Fernsteuerung', search: 'remote fernsteuerung server input port api token verbindung' },
- { id: 'diagnose', label: 'Diagnose-Zugriff', search: 'diagnose zugriff lesen allowlist netzwerk lokal verbindung code support' },
- { id: 'backup', label: 'Backup & Übertragen', search: 'backup sichern export import online schlüssel einstellungen accounts übertragen' }
+ { id: 'allgemein', label: 'Allgemein', search: 'fenster window vordergrund foreground always on top drop target oberfläche interface updates update aktualisierung version language sprache' },
+ { id: 'uploads', label: 'Uploads', search: 'upload queue warteschlange waiting fertig completed completion abschluss entfernen remove parallel geschwindigkeit speed limit fortsetzen resume wiederherstellen restore hoster' },
+ { id: 'automatik', label: 'Automatik', search: 'automatisch automation automatic retry wiederholen ordner folder monitor überwachen watch dateierweiterungen extensions unterordner subfolders duplikate duplicates' },
+ { id: 'benachrichtigungen', label: 'Benachrichtigungen', search: 'benachrichtigungen notifications webhook discord meldung message ping erwähnung mention batch fertig completed' },
+ { id: 'logs', label: 'Logs & Support', search: 'log logs protokoll logging debug verbose diagnose diagnostics support paket package datei file ordner folder' },
+ { id: 'remote', label: 'Fernsteuerung', search: 'remote control fernsteuerung server input port api token verbindung connection client' },
+ { id: 'diagnose', label: 'Diagnose-Zugriff', search: 'diagnose diagnostics access zugriff lesen read allowlist netzwerk network lokal local verbindung connection code support' },
+ { id: 'backup', label: 'Backup & Übertragen', search: 'backup transfer sichern export import online schlüssel key einstellungen settings accounts übertragen' }
];
const pageHeader = (title, description) => `