fix: complete localized accessible UI audit

Localize dynamic renderer and shell text in English and German, replace native prompts with safe styled dialogs, align empty and dirty states, restore compact responsive information, and add keyboard support for queue, account, menu, and sorting interactions. Version the verified patch as 2.1.6.
This commit is contained in:
Sucukdeluxe
2026-08-11 09:36:41 +02:00
parent 1a71a1dba8
commit 7c97915d64
10 changed files with 1039 additions and 272 deletions
+1 -1
View File
@@ -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). 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 ## Features
+40 -27
View File
@@ -1402,10 +1402,10 @@ function createWindow() {
try { try {
const choice = dialog.showMessageBoxSync(mainWindow, { const choice = dialog.showMessageBoxSync(mainWindow, {
type: 'error', type: 'error',
title: 'Renderer abgestürzt', title: shellText('Renderer abgestürzt', 'Renderer crashed'),
message: `Der Renderer-Prozess ist abgestürzt (${details.reason}).`, message: shellText(`Der Renderer-Prozess ist abgestürzt (${details.reason}).`, `The renderer process crashed (${details.reason}).`),
detail: 'Bitte Diagnose-Paket exportieren und einsenden. Klick "Neu laden" um die UI wiederherzustellen — laufende Uploads im Main-Process bleiben aktiv.', 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: ['Neu laden', 'Beenden'], buttons: [shellText('Neu laden', 'Reload'), shellText('Beenden', 'Quit')],
defaultId: 0, defaultId: 0,
cancelId: 1 cancelId: 1
}); });
@@ -1460,14 +1460,7 @@ function createTray() {
} catch {} } catch {}
} }
tray = new Tray(icon || nativeImage.createEmpty()); tray = new Tray(icon || nativeImage.createEmpty());
tray.setToolTip('Multi-Hoster-Upload'); refreshTrayLanguage();
const contextMenu = Menu.buildFromTemplate([
{ label: 'Öffnen', click: () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } } },
{ type: 'separator' },
{ label: 'Beenden', click: () => { app.quit(); } }
]);
tray.setContextMenu(contextMenu);
tray.on('click', () => { tray.on('click', () => {
if (mainWindow) { mainWindow.show(); mainWindow.focus(); } if (mainWindow) { mainWindow.show(); mainWindow.focus(); }
@@ -1702,7 +1695,7 @@ ipcMain.handle('save-text-file', async (_event, defaultName, content, filters) =
? filters ? filters
: [{ name: 'Textdatei', extensions: ['txt', 'csv', 'log'] }]; : [{ name: 'Textdatei', extensions: ['txt', 'csv', 'log'] }];
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, { const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, {
title: 'Speichern unter', title: shellText('Speichern unter', 'Save as'),
defaultPath: safeName, defaultPath: safeName,
filters: safeFilters filters: safeFilters
}); });
@@ -1718,11 +1711,11 @@ ipcMain.handle('export-history', async (_event, format) => {
const datePrefix = new Date().toISOString().slice(0, 10); const datePrefix = new Date().toISOString().slice(0, 10);
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, { const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, {
title: 'Upload-Verlauf exportieren', title: shellText('Upload-Verlauf exportieren', 'Export upload history'),
defaultPath: `upload-history-${datePrefix}.${normalizedFormat}`, defaultPath: `upload-history-${datePrefix}.${normalizedFormat}`,
filters: normalizedFormat === 'json' filters: normalizedFormat === 'json'
? [{ name: 'JSON-Datei', extensions: ['json'] }] ? [{ name: shellText('JSON-Datei', 'JSON file'), extensions: ['json'] }]
: [{ name: 'CSV-Datei', extensions: ['csv'] }] : [{ name: shellText('CSV-Datei', 'CSV file'), extensions: ['csv'] }]
}); });
if (canceled || !filePath) return { ok: false, canceled: true }; if (canceled || !filePath) return { ok: false, canceled: true };
@@ -1810,6 +1803,25 @@ function getUploadBrowseDirectory() {
return app.getPath('downloads'); 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) { async function rememberUploadBrowseDirectory(selectedPath, selectedDirectory = false) {
if (!selectedPath) return; if (!selectedPath) return;
const directory = selectedDirectory ? selectedPath : path.dirname(selectedPath); const directory = selectedDirectory ? selectedPath : path.dirname(selectedPath);
@@ -1821,8 +1833,8 @@ ipcMain.handle('select-files', async () => {
defaultPath: getUploadBrowseDirectory(), defaultPath: getUploadBrowseDirectory(),
properties: ['openFile', 'multiSelections'], properties: ['openFile', 'multiSelections'],
filters: [ filters: [
{ name: 'Alle Dateien', extensions: ['*'] }, { name: shellText('Alle Dateien', 'All files'), extensions: ['*'] },
{ name: 'Videos', extensions: ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm'] } { name: shellText('Videos', 'Videos'), extensions: ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm'] }
] ]
}); });
if (result.canceled || !result.filePaths.length) return null; 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 defaultName = `multi-hoster-support-${stamp}.txt`;
const desktop = (() => { try { return app.getPath('desktop'); } catch { return app.getPath('userData'); } })(); const desktop = (() => { try { return app.getPath('desktop'); } catch { return app.getPath('userData'); } })();
const res = await dialog.showSaveDialog(mainWindow || undefined, { const res = await dialog.showSaveDialog(mainWindow || undefined, {
title: 'Diagnose-Paket speichern', title: shellText('Diagnose-Paket speichern', 'Save diagnostics package'),
defaultPath: path.join(desktop, defaultName), defaultPath: path.join(desktop, defaultName),
filters: [{ name: 'Text', extensions: ['txt'] }] filters: [{ name: 'Text', extensions: ['txt'] }]
}); });
@@ -2516,11 +2528,11 @@ ipcMain.handle('export-backup', async () => {
const _bd = new Date(); const _bd = new Date();
const _bdate = `${String(_bd.getDate()).padStart(2, '0')}-${String(_bd.getMonth() + 1).padStart(2, '0')}-${_bd.getFullYear()}`; const _bdate = `${String(_bd.getDate()).padStart(2, '0')}-${String(_bd.getMonth() + 1).padStart(2, '0')}-${_bd.getFullYear()}`;
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, { const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, {
title: 'Backup exportieren', title: shellText('Backup exportieren', 'Export backup'),
defaultPath: `${_bdate}-multihoster-backup.mhu`, defaultPath: `${_bdate}-multihoster-backup.mhu`,
filters: [ filters: [
{ name: 'Multi-Hoster Backup (verschlüsselt)', extensions: ['mhu'] }, { name: shellText('Multi-Hoster Backup (verschlüsselt)', 'Multi Hoster backup (encrypted)'), extensions: ['mhu'] },
{ name: 'Multi-Hoster Backup (Klartext JSON)', extensions: ['json'] } { name: shellText('Multi-Hoster Backup (Klartext JSON)', 'Multi Hoster backup (plain JSON)'), extensions: ['json'] }
] ]
}); });
if (canceled || !filePath) return { ok: false, canceled: true }; if (canceled || !filePath) return { ok: false, canceled: true };
@@ -2542,9 +2554,9 @@ ipcMain.handle('import-backup', async (_event, legacyPassword) => {
buffer = readBackupFile(sourcePath); buffer = readBackupFile(sourcePath);
} else { } else {
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, { const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
title: 'Backup importieren', title: shellText('Backup importieren', 'Import backup'),
filters: [ filters: [
{ name: 'Multi-Hoster Backup', extensions: ['mhu', 'json'] }, { name: 'Multi Hoster backup', extensions: ['mhu', 'json'] },
{ name: 'Verschlüsselt (.mhu)', extensions: ['mhu'] }, { name: 'Verschlüsselt (.mhu)', extensions: ['mhu'] },
{ name: 'Klartext (.json)', extensions: ['json'] } { name: 'Klartext (.json)', extensions: ['json'] }
], ],
@@ -2642,10 +2654,10 @@ ipcMain.handle('read-own-upload-log', () => {
ipcMain.handle('import-upload-log', async () => { ipcMain.handle('import-upload-log', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, { const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
title: 'Upload-Log importieren', title: shellText('Upload-Log importieren', 'Import upload log'),
filters: [ filters: [
{ name: 'Log-Dateien', extensions: ['log', 'txt'] }, { name: shellText('Log-Dateien', 'Log files'), extensions: ['log', 'txt'] },
{ name: 'Alle Dateien', extensions: ['*'] } { name: shellText('Alle Dateien', 'All files'), extensions: ['*'] }
], ],
properties: ['openFile'] properties: ['openFile']
}); });
@@ -2807,6 +2819,7 @@ ipcMain.handle('save-global-settings', async (_event, globalSettings) => {
if (uploadManager) { if (uploadManager) {
try { uploadManager.updateSettings(null, globalSettings); } catch (error) { debugLog(`global settings runtime update failed: ${error.message}`); } try { uploadManager.updateSettings(null, globalSettings); } catch (error) { debugLog(`global settings runtime update failed: ${error.message}`); }
} }
refreshTrayLanguage();
return true; return true;
}); });
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "2.1.5", "version": "2.1.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "2.1.5", "version": "2.1.6",
"dependencies": { "dependencies": {
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
"undici": "^7.29.0", "undici": "^7.29.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "2.1.5", "version": "2.1.6",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
+485 -176
View File
File diff suppressed because it is too large Load Diff
+65 -2
View File
@@ -4,6 +4,7 @@
if (root) root.I18n = api; if (root) root.I18n = api;
})(typeof window !== 'undefined' ? window : globalThis, function () { })(typeof window !== 'undefined' ? window : globalThis, function () {
const pairs = [ const pairs = [
['Alternative', 'Alternate'],
['Arbeitsbereich', 'Workspace'], ['Arbeitsbereich', 'Workspace'],
['Accounts verwalten', 'Manage accounts'], ['Accounts verwalten', 'Manage accounts'],
['Archiv', 'Archive'], ['Archiv', 'Archive'],
@@ -17,6 +18,10 @@
['Ausgabe-Format der kopierten Links', 'Output format for copied links'], ['Ausgabe-Format der kopierten Links', 'Output format for copied links'],
['Auto-Check vor Upload', 'Auto-check before upload'], ['Auto-Check vor Upload', 'Auto-check before upload'],
['Automatischer Check vor dem Upload', 'Automatic check before upload'], ['Automatischer Check vor dem Upload', 'Automatic check before upload'],
['Wiederholungen', 'Retries'],
['Maximale Geschwindigkeit (MB/s)', 'Maximum speed (MB/s)'],
['Neustart unter (kB/s)', 'Restart below (kB/s)'],
['Maximale Größe (MB)', 'Maximum size (MB)'],
['Bildschirm und Eingabesteuerung bleiben gesperrt.', 'Screen and input control remain locked.'], ['Bildschirm und Eingabesteuerung bleiben gesperrt.', 'Screen and input control remain locked.'],
['Bindet nur an', 'Binds only to'], ['Bindet nur an', 'Binds only to'],
['Bindet an', 'Binds to'], ['Bindet an', 'Binds to'],
@@ -27,7 +32,7 @@
['Erlaubt', 'Allowed'], ['Erlaubt', 'Allowed'],
['Erneut versuchen', 'Retry'], ['Erneut versuchen', 'Retry'],
['Exportieren', 'Export'], ['Exportieren', 'Export'],
['Fehlgeschlagene erneut', 'Retry failed'], ['Fehlgeschlagene erneut', 'Retry failed uploads'],
['Fensterverhalten, Drop-Target und Programmupdates.', 'Window behavior, drop target, and application updates.'], ['Fensterverhalten, Drop-Target und Programmupdates.', 'Window behavior, drop target, and application updates.'],
['Gesamt:', 'Total:'], ['Gesamt:', 'Total:'],
['Globales Speed-Limit', 'Global speed limit'], ['Globales Speed-Limit', 'Global speed limit'],
@@ -52,6 +57,8 @@
['Noch keine Accounts', 'No accounts yet'], ['Noch keine Accounts', 'No accounts yet'],
['Nur lokal (127.0.0.1) — Tunnel/VPN', 'Local only (127.0.0.1) — tunnel/VPN'], ['Nur lokal (127.0.0.1) — Tunnel/VPN', 'Local only (127.0.0.1) — tunnel/VPN'],
['Parallele Uploads verringern', 'Decrease parallel uploads'], ['Parallele Uploads verringern', 'Decrease parallel uploads'],
['Priorität ändern. Alt und Pfeil nach oben oder unten verwenden', 'Change priority. Use Alt and the up or down arrow key'],
['Ziehen oder Alt und Pfeiltasten zum Sortieren', 'Drag or use Alt and arrow keys to sort'],
['Restzeit:', 'Time remaining:'], ['Restzeit:', 'Time remaining:'],
['Rest', 'Remaining'], ['Rest', 'Remaining'],
['Schnell finden', 'Quick find'], ['Schnell finden', 'Quick find'],
@@ -67,7 +74,7 @@
['Verbindungen', 'Connections'], ['Verbindungen', 'Connections'],
['Verbleibend', 'Remaining'], ['Verbleibend', 'Remaining'],
['Läuft', 'Running'], ['Läuft', 'Running'],
['Suche Aktualisierungen', 'Checking for updates'], ['Suche Aktualisierungen', 'Check for updates'],
['Verbleibend:', 'Remaining:'], ['Verbleibend:', 'Remaining:'],
['Zuletzt erzeugte Upload-Links', 'Recently generated upload links'], ['Zuletzt erzeugte Upload-Links', 'Recently generated upload links'],
['User-ID, role:ROLLEN-ID, @here oder @everyone', 'User ID, role:ROLE-ID, @here, or @everyone'], ['User-ID, role:ROLLEN-ID, @here oder @everyone', 'User ID, role:ROLE-ID, @here, or @everyone'],
@@ -77,6 +84,7 @@
['Datei', 'File'], ['Datei', 'File'],
['Dateien', 'Files'], ['Dateien', 'Files'],
['Dateien hinzufügen', 'Add files'], ['Dateien hinzufügen', 'Add files'],
['Dateien oder Ordner hinzufügen', 'Add files or folders'],
['Ordner hinzufügen', 'Add folder'], ['Ordner hinzufügen', 'Add folder'],
['Online-Schlüssel erstellen', 'Create online key'], ['Online-Schlüssel erstellen', 'Create online key'],
['Online-Schlüssel importieren', 'Import online key'], ['Online-Schlüssel importieren', 'Import online key'],
@@ -428,6 +436,32 @@
['Verschlüssele und speichere Einstellungen…', 'Encrypting and saving settings…'], ['Verschlüssele und speichere Einstellungen…', 'Encrypting and saving settings…'],
['Zeit', 'Time'], ['Zeit', 'Time'],
['Wenn das Backup mit der alten Passwort-Option (vor v3.0) erstellt wurde, hier eingeben.', 'If the backup was created with the legacy password option before v3.0, enter it here.'], ['Wenn das Backup mit der alten Passwort-Option (vor v3.0) erstellt wurde, hier eingeben.', 'If the backup was created with the legacy password option before v3.0, enter it here.'],
['Wartet', 'Waiting'],
['Abgebrochen', 'Canceled'],
['Fehlgeschlagen', 'Failed'],
['Fehlgeschlagen: Verbindung verloren', 'Failed: Connection lost'],
['Retry 2/3 · Primär nicht verfügbar', 'Retry 2/3 · Primary unavailable'],
['Link kopiert', 'Link copied'],
['Kopiert!', 'Copied!'],
['Sende…', 'Sending…'],
['Lade…', 'Loading…'],
['Upload-Statistiken', 'Upload statistics'],
['Keine Uploads entsprechen diesem Filter.', 'No uploads match this filter.'],
['Keine Accounts entsprechen diesem Filter.', 'No accounts match this filter.'],
['Keine Verlaufseinträge entsprechen diesem Filter.', 'No history entries match this filter.'],
['Verlauf konnte nicht geladen werden.', 'History could not be loaded.'],
['Wähle das Dateiformat für den Export.', 'Choose the file format for the export.'],
['CSV exportieren', 'Export CSV'],
['JSON exportieren', 'Export JSON'],
['Bestätigen', 'Confirm'],
['Alle Links entfernen?', 'Remove all links?'],
['Uploads entfernen?', 'Remove uploads?'],
['Alle Uploads entfernen?', 'Remove all uploads?'],
['Alle Uploads abbrechen?', 'Cancel all uploads?'],
['Alle laufenden Uploads werden abgebrochen und in die Warteschlange zurückgesetzt.', 'All running uploads will be canceled and returned to the queue.'],
['Alle abbrechen', 'Cancel all'],
['Hoster entfernen?', 'Remove host?'],
['Unbekannt', 'Unknown'],
['Verlauf', 'History'] ['Verlauf', 'History']
]; ];
const deToEn = new Map(pairs); const deToEn = new Map(pairs);
@@ -448,6 +482,24 @@
const patterns = target === 'en' const patterns = target === 'en'
? [ ? [
[/^Update v(.+) verfügbar$/, 'Update v$1 available'], [/^Update v(.+) verfügbar$/, 'Update v$1 available'],
[/^(\d+) Links kopiert$/, '$1 links copied'],
[/^(\d+) Link kopiert$/, '$1 link copied'],
[/^Wirklich alle (\d+) Links aus diesem Panel entfernen\?$/, 'Remove all $1 links from this panel?'],
[/^Ein ausgewählter Eintrag wird aus diesem Panel entfernt\.$/, 'One selected entry will be removed from this panel.'],
[/^(\d+) ausgewählte Einträge werden aus diesem Panel entfernt\.$/, '$1 selected entries will be removed from this panel.'],
[/^Ein ausgewählter Upload wird aus der Liste entfernt\.$/, 'One selected upload will be removed from the list.'],
[/^(\d+) ausgewählte Uploads werden aus der Liste entfernt\.$/, '$1 selected uploads will be removed from the list.'],
[/^(\d+) Upload wird aus der Liste entfernt\.$/, '$1 upload will be removed from the list.'],
[/^(\d+) Uploads werden aus der Liste entfernt\.$/, '$1 uploads will be removed from the list.'],
[/^(\d+) Upload wird für (.+) entfernt\.$/, '$1 upload for $2 will be removed.'],
[/^(\d+) Uploads werden für (.+) entfernt\.$/, '$1 uploads for $2 will be removed.'],
[/^Ruhezustand in (\d+)s\.\.\.$/, 'Sleep in $1s...'],
[/^Herunterfahren in (\d+)s\.\.\.$/, 'Shut down in $1s...'],
[/^Neustart in (\d+)s\.\.\.$/, 'Restart in $1s...'],
[/^(\d+) Job zum erneuten Upload zurückgesetzt$/, '$1 job reset for upload'],
[/^(\d+) Jobs zum erneuten Upload zurückgesetzt$/, '$1 jobs reset for upload'],
[/^(\d+) Verlaufseintrag wird dauerhaft entfernt\.$/, '$1 history entry will be permanently removed.'],
[/^(\d+) Verlaufseinträge werden dauerhaft entfernt\.$/, '$1 history entries will be permanently removed.'],
[/^Aktives Ziel: (.+)$/, 'Active destination: $1'], [/^Aktives Ziel: (.+)$/, 'Active destination: $1'],
[/^(\d+) Fehler$/, '$1 errors'], [/^(\d+) Fehler$/, '$1 errors'],
[/^Gesamt (\d+)$/, 'Total $1'], [/^Gesamt (\d+)$/, 'Total $1'],
@@ -467,6 +519,8 @@
[/^Account "(.+)" wirklich löschen\?$/, 'Are you sure you want to delete account "$1"?'], [/^Account "(.+)" wirklich löschen\?$/, 'Are you sure you want to delete account "$1"?'],
[/^Aktiv auf (.+) \((.+)\) — (\d+) Client\(s\) — Letzter Zugriff: (.+)$/, 'Active at $1 ($2) — $3 client(s) — Last access: $4'], [/^Aktiv auf (.+) \((.+)\) — (\d+) Client\(s\) — Letzter Zugriff: (.+)$/, 'Active at $1 ($2) — $3 client(s) — Last access: $4'],
[/^Aktiv auf Port (\d+) — (\d+) Client\(s\) verbunden$/, 'Active on port $1 — $2 client(s) connected'], [/^Aktiv auf Port (\d+) — (\d+) Client\(s\) verbunden$/, 'Active on port $1 — $2 client(s) connected'],
[/^Aktiv auf Port (\d+) — 1 Client verbunden$/, 'Active on port $1 — 1 client connected'],
[/^Aktiv auf Port (\d+) — (\d+) Clients verbunden$/, 'Active on port $1 — $2 clients connected'],
[/^Alle Fehler erneut versuchen \((\d+)\)$/, 'Retry all failed uploads ($1)'], [/^Alle Fehler erneut versuchen \((\d+)\)$/, 'Retry all failed uploads ($1)'],
[/^API: (.+)$/, 'API: $1'], [/^API: (.+)$/, 'API: $1'],
[/^Ausgewählte starten \((\d+)\)$/, 'Start selected ($1)'], [/^Ausgewählte starten \((\d+)\)$/, 'Start selected ($1)'],
@@ -502,6 +556,15 @@
] ]
: [ : [
[/^Update v(.+) available$/, 'Update v$1 verfügbar'], [/^Update v(.+) available$/, 'Update v$1 verfügbar'],
[/^Sleep in (\d+)s\.\.\.$/, 'Ruhezustand in $1s...'],
[/^Shut down in (\d+)s\.\.\.$/, 'Herunterfahren in $1s...'],
[/^Restart in (\d+)s\.\.\.$/, 'Neustart in $1s...'],
[/^(\d+) job reset for upload$/, '$1 Job zum erneuten Upload zurückgesetzt'],
[/^(\d+) jobs reset for upload$/, '$1 Jobs zum erneuten Upload zurückgesetzt'],
[/^(\d+) history entry will be permanently removed\.$/, '$1 Verlaufseintrag wird dauerhaft entfernt.'],
[/^(\d+) history entries will be permanently removed\.$/, '$1 Verlaufseinträge werden dauerhaft entfernt.'],
[/^Active on port (\d+) — 1 client connected$/, 'Aktiv auf Port $1 — 1 Client verbunden'],
[/^Active on port (\d+) — (\d+) clients connected$/, 'Aktiv auf Port $1 — $2 Clients verbunden'],
[/^Active destination: (.+)$/, 'Aktives Ziel: $1'], [/^Active destination: (.+)$/, 'Aktives Ziel: $1'],
[/^Running (\d+)$/, 'Läuft $1'], [/^Running (\d+)$/, 'Läuft $1'],
[/^Failed (\d+)$/, 'Fehler $1'], [/^Failed (\d+)$/, 'Fehler $1'],
+41 -40
View File
@@ -27,9 +27,9 @@
<header class="app-header"> <header class="app-header">
<div class="header-cluster header-primary"> <div class="header-cluster header-primary">
<div class="app-brand" aria-label="Multi-Hoster Upload"> <div class="app-brand" aria-label="Multi Hoster Uploader">
<img src="../assets/app_icon.png" alt="" class="app-brand-icon" width="24" height="24"> <img src="../assets/app_icon.png" alt="" class="app-brand-icon" width="24" height="24">
<span class="app-brand-name">MULTI-HOSTER UPLOAD</span> <span class="app-brand-name">MULTI HOSTER UPLOADER</span>
</div> </div>
<span class="header-divider" aria-hidden="true"></span> <span class="header-divider" aria-hidden="true"></span>
<nav class="tab-bar" role="tablist" aria-label="Hauptbereiche"> <nav class="tab-bar" role="tablist" aria-label="Hauptbereiche">
@@ -74,8 +74,8 @@
<button class="menu-dropdown-item" data-menu-action="add-folder"><span>Ordner hinzufügen</span></button> <button class="menu-dropdown-item" data-menu-action="add-folder"><span>Ordner hinzufügen</span></button>
<div class="menu-separator"></div> <div class="menu-separator"></div>
<div class="menu-submenu" data-submenu="sicherung"> <div class="menu-submenu" data-submenu="sicherung">
<button class="menu-submenu-trigger">Sicherung</button> <button class="menu-submenu-trigger" aria-haspopup="menu" aria-expanded="false">Sicherung</button>
<div class="menu-submenu-dropdown" style="display:none"> <div class="menu-submenu-dropdown" role="menu" style="display:none">
<button class="menu-dropdown-item" data-menu-action="backup-export"><span>Exportieren</span></button> <button class="menu-dropdown-item" data-menu-action="backup-export"><span>Exportieren</span></button>
<button class="menu-dropdown-item" data-menu-action="backup-import"><span>Importieren</span></button> <button class="menu-dropdown-item" data-menu-action="backup-import"><span>Importieren</span></button>
<button class="menu-dropdown-item" data-menu-action="online-backup-create"><span>Online-Schlüssel erstellen</span></button> <button class="menu-dropdown-item" data-menu-action="online-backup-create"><span>Online-Schlüssel erstellen</span></button>
@@ -235,7 +235,7 @@
</div> </div>
<div class="upload-workspace"> <div class="upload-workspace">
<div class="drop-zone" id="dropZone"> <div class="drop-zone" id="dropZone" role="button" tabindex="0" aria-label="Dateien oder Ordner hinzufügen">
<div class="drop-icon" aria-hidden="true"><svg><use href="#icon-cloud"></use></svg></div> <div class="drop-icon" aria-hidden="true"><svg><use href="#icon-cloud"></use></svg></div>
<p>Dateien hierher ziehen oder klicken</p> <p>Dateien hierher ziehen oder klicken</p>
<span>Dateien und Ordner werden vor dem Upload geprüft.</span> <span>Dateien und Ordner werden vor dem Upload geprüft.</span>
@@ -280,14 +280,14 @@
<table class="queue-table" id="queueTable"> <table class="queue-table" id="queueTable">
<thead> <thead>
<tr> <tr>
<th class="col-filename sortable" data-col="filename" data-sort="filename">Dateiname<span class="col-resizer"></span></th> <th class="col-filename sortable" data-col="filename" data-sort="filename" tabindex="0" aria-sort="ascending">Dateiname<span class="col-resizer"></span></th>
<th class="col-size sortable" data-col="size" data-sort="size">Hochgeladen / Größe<span class="col-resizer"></span></th> <th class="col-size sortable" data-col="size" data-sort="size" tabindex="0" aria-sort="none">Hochgeladen / Größe<span class="col-resizer"></span></th>
<th class="col-host sortable" data-col="host" data-sort="host">Hoster<span class="col-resizer"></span></th> <th class="col-host sortable" data-col="host" data-sort="host" tabindex="0" aria-sort="none">Hoster<span class="col-resizer"></span></th>
<th class="col-status sortable" data-col="status" data-sort="status">Status<span class="col-resizer"></span></th> <th class="col-status sortable" data-col="status" data-sort="status" tabindex="0" aria-sort="none">Status<span class="col-resizer"></span></th>
<th class="col-elapsed" data-col="elapsed">Zeit<span class="col-resizer"></span></th> <th class="col-elapsed" data-col="elapsed">Zeit<span class="col-resizer"></span></th>
<th class="col-remaining" data-col="remaining">Rest<span class="col-resizer"></span></th> <th class="col-remaining" data-col="remaining">Rest<span class="col-resizer"></span></th>
<th class="col-speed sortable" data-col="speed" data-sort="speed">Geschwindigkeit<span class="col-resizer"></span></th> <th class="col-speed sortable" data-col="speed" data-sort="speed" tabindex="0" aria-sort="none">Geschwindigkeit<span class="col-resizer"></span></th>
<th class="col-progress sortable" data-col="progress" data-sort="progress">Fortschritt</th> <th class="col-progress sortable" data-col="progress" data-sort="progress" tabindex="0" aria-sort="none">Fortschritt</th>
</tr> </tr>
</thead> </thead>
<tbody id="queueBody"></tbody> <tbody id="queueBody"></tbody>
@@ -324,10 +324,10 @@
<table class="recent-files-table"> <table class="recent-files-table">
<thead id="recentFilesHead"> <thead id="recentFilesHead">
<tr> <tr>
<th class="col-date sortable" data-recent-sort="date">Datum<span class="sort-indicator"></span></th> <th class="col-date sortable" data-recent-sort="date" tabindex="0" aria-sort="descending">Datum<span class="sort-indicator"></span></th>
<th class="col-filename sortable" data-recent-sort="filename">Dateiname<span class="sort-indicator"></span></th> <th class="col-filename sortable" data-recent-sort="filename" tabindex="0" aria-sort="none">Dateiname<span class="sort-indicator"></span></th>
<th class="col-host sortable" data-recent-sort="host">Hoster<span class="sort-indicator"></span></th> <th class="col-host sortable" data-recent-sort="host" tabindex="0" aria-sort="none">Hoster<span class="sort-indicator"></span></th>
<th class="col-link sortable" data-recent-sort="link">Link<span class="sort-indicator"></span></th> <th class="col-link sortable" data-recent-sort="link" tabindex="0" aria-sort="none">Link<span class="sort-indicator"></span></th>
</tr> </tr>
</thead> </thead>
<tbody id="recentFilesBody"></tbody> <tbody id="recentFilesBody"></tbody>
@@ -451,7 +451,7 @@
</div> </div>
<div class="modal-overlay" id="jobLogModal" style="display:none"> <div class="modal-overlay" id="jobLogModal" style="display:none">
<div class="modal-card" style="width:min(820px,96%);max-height:80vh;display:flex;flex-direction:column"> <div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="jobLogTitle" style="width:min(820px,96%);max-height:80vh;display:flex;flex-direction:column">
<div class="modal-header"> <div class="modal-header">
<div><h3 id="jobLogTitle">Upload-Log</h3></div> <div><h3 id="jobLogTitle">Upload-Log</h3></div>
<button class="icon-btn" id="closeJobLogBtn" aria-label="Schließen">&times;</button> <button class="icon-btn" id="closeJobLogBtn" aria-label="Schließen">&times;</button>
@@ -467,9 +467,9 @@
</div> </div>
<div class="modal-overlay" id="deleteAccountModal" style="display:none"> <div class="modal-overlay" id="deleteAccountModal" style="display:none">
<div class="modal-card" style="width:min(400px,100%)"> <div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="deleteAccountTitle" aria-describedby="deleteAccountMessage" style="width:min(400px,100%)">
<div class="modal-header"> <div class="modal-header">
<div><h3>Account löschen?</h3></div> <div><h3 id="deleteAccountTitle">Account löschen?</h3></div>
<button class="icon-btn" id="closeDeleteModalBtn" aria-label="Schließen">&times;</button> <button class="icon-btn" id="closeDeleteModalBtn" aria-label="Schließen">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -492,7 +492,8 @@
<p id="appAlertMessage"></p> <p id="appAlertMessage"></p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<span></span> <button class="btn btn-secondary" id="appAlertCancelBtn" hidden>Abbrechen</button>
<button class="btn btn-secondary" id="appAlertAlternateBtn" hidden>Alternative</button>
<button class="btn btn-primary" id="appAlertConfirmBtn">OK</button> <button class="btn btn-primary" id="appAlertConfirmBtn">OK</button>
</div> </div>
</div> </div>
@@ -577,8 +578,8 @@
<button type="button" class="history-retention-option" role="option" data-history-retention="7d" aria-selected="false">Letzte 7 Tage</button> <button type="button" class="history-retention-option" role="option" data-history-retention="7d" aria-selected="false">Letzte 7 Tage</button>
<button type="button" class="history-retention-option" role="option" data-history-retention="30d" aria-selected="false">Letzte 30 Tage</button> <button type="button" class="history-retention-option" role="option" data-history-retention="30d" aria-selected="false">Letzte 30 Tage</button>
<button type="button" class="history-retention-option" role="option" data-history-retention="90d" aria-selected="false">Letzte 90 Tage</button> <button type="button" class="history-retention-option" role="option" data-history-retention="90d" aria-selected="false">Letzte 90 Tage</button>
<button type="button" class="history-retention-option" role="option" data-history-retention="1000" aria-selected="false">Letzte 1000 Uploads</button>
<button type="button" class="history-retention-option" role="option" data-history-retention="100" aria-selected="false">Letzte 100 Uploads</button> <button type="button" class="history-retention-option" role="option" data-history-retention="100" aria-selected="false">Letzte 100 Uploads</button>
<button type="button" class="history-retention-option" role="option" data-history-retention="1000" aria-selected="false">Letzte 1000 Uploads</button>
</div> </div>
</div> </div>
<select id="historyRetentionSelect" class="history-retention-select" hidden aria-hidden="true" tabindex="-1"> <select id="historyRetentionSelect" class="history-retention-select" hidden aria-hidden="true" tabindex="-1">
@@ -586,8 +587,8 @@
<option value="7d">Letzte 7 Tage</option> <option value="7d">Letzte 7 Tage</option>
<option value="30d">Letzte 30 Tage</option> <option value="30d">Letzte 30 Tage</option>
<option value="90d">Letzte 90 Tage</option> <option value="90d">Letzte 90 Tage</option>
<option value="1000">Letzte 1000 Uploads</option>
<option value="100">Letzte 100 Uploads</option> <option value="100">Letzte 100 Uploads</option>
<option value="1000">Letzte 1000 Uploads</option>
</select> </select>
<button class="btn btn-secondary" id="exportHistoryBtn">Verlauf exportieren</button> <button class="btn btn-secondary" id="exportHistoryBtn">Verlauf exportieren</button>
<button class="btn btn-danger" id="clearHistoryBtn" disabled>Verlauf löschen</button> <button class="btn btn-danger" id="clearHistoryBtn" disabled>Verlauf löschen</button>
@@ -598,42 +599,42 @@
</main> </main>
</div> </div>
<div class="context-menu" id="contextMenu" style="display:none"> <div class="context-menu" id="contextMenu" role="menu" style="display:none">
<div class="ctx-item" data-action="start-selected">Ausgewählte starten</div> <div class="ctx-item" role="menuitem" tabindex="-1" data-action="start-selected">Ausgewählte starten</div>
<div class="ctx-item" data-action="retry-selected">Erneut versuchen</div> <div class="ctx-item" role="menuitem" tabindex="-1" data-action="retry-selected">Erneut versuchen</div>
<div class="ctx-item" data-action="show-log">Log anzeigen</div> <div class="ctx-item" role="menuitem" tabindex="-1" data-action="show-log">Log anzeigen</div>
<div class="ctx-separator"></div> <div class="ctx-separator"></div>
<div class="ctx-item" data-action="copy-links">Links kopieren</div> <div class="ctx-item" role="menuitem" tabindex="-1" data-action="copy-links">Links kopieren</div>
<div class="ctx-item" data-action="copy-all-links">Alle Links kopieren</div> <div class="ctx-item" role="menuitem" tabindex="-1" data-action="copy-all-links">Alle Links kopieren</div>
<div class="ctx-separator"></div> <div class="ctx-separator"></div>
<div class="ctx-item" data-action="delete-selected">Entfernen</div> <div class="ctx-item" role="menuitem" tabindex="-1" data-action="delete-selected">Entfernen</div>
<div class="ctx-item" data-action="delete-all">Alle entfernen</div> <div class="ctx-item" role="menuitem" tabindex="-1" data-action="delete-all">Alle entfernen</div>
<div class="ctx-submenu ctx-hoster-delete-submenu" style="display:none"> <div class="ctx-submenu ctx-hoster-delete-submenu" style="display:none">
<div class="ctx-item ctx-item-danger">Hoster entfernen &#9656;</div> <div class="ctx-item ctx-item-danger" role="menuitem" tabindex="-1">Hoster entfernen &#9656;</div>
<div class="ctx-submenu-items ctx-hoster-delete-items"></div> <div class="ctx-submenu-items ctx-hoster-delete-items"></div>
</div> </div>
</div> </div>
<div class="context-menu" id="recentContextMenu" style="display:none"> <div class="context-menu" id="recentContextMenu" role="menu" style="display:none">
<div class="ctx-item" data-action="recent-copy-links">Links kopieren</div> <div class="ctx-item" role="menuitem" tabindex="-1" data-action="recent-copy-links">Links kopieren</div>
<div class="ctx-item" data-action="recent-delete">Entfernen</div> <div class="ctx-item" role="menuitem" tabindex="-1" data-action="recent-delete">Entfernen</div>
</div> </div>
<div class="copy-toast" id="copyToast"></div> <div class="copy-toast" id="copyToast" role="status" aria-live="polite" aria-atomic="true"></div>
<div class="shutdown-overlay" id="shutdownOverlay" style="display:none"> <div class="shutdown-overlay" id="shutdownOverlay" style="display:none" aria-hidden="true">
<div class="shutdown-box"> <div class="shutdown-box" role="dialog" aria-modal="true" aria-labelledby="shutdownMessage">
<p id="shutdownMessage">System wird heruntergefahren in <span id="shutdownSeconds">60</span>s...</p> <p id="shutdownMessage">System wird heruntergefahren in <span id="shutdownSeconds">60</span>s...</p>
<button class="btn btn-danger" id="cancelShutdownBtn">Abbrechen</button> <button class="btn btn-danger" id="cancelShutdownBtn">Abbrechen</button>
</div> </div>
</div> </div>
<div class="modal-overlay" id="hosterModal" style="display:none"> <div class="modal-overlay" id="hosterModal" style="display:none" aria-hidden="true">
<div class="modal-card"> <div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="hosterModalTitle" aria-describedby="hosterModalDescription">
<div class="modal-header"> <div class="modal-header">
<div> <div>
<h3>Upload-Ziele auswählen</h3> <h3 id="hosterModalTitle">Upload-Ziele auswählen</h3>
<p>Dateien wurden hinzugefügt. Wähle jetzt die Hoster für den Upload.</p> <p id="hosterModalDescription">Dateien wurden hinzugefügt. Wähle jetzt die Hoster für den Upload.</p>
</div> </div>
<button class="icon-btn" id="closeHosterModalBtn" aria-label="Schließen">&times;</button> <button class="icon-btn" id="closeHosterModalBtn" aria-label="Schließen">&times;</button>
</div> </div>
+216 -20
View File
@@ -32,11 +32,18 @@ body {
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
color: var(--text); color: var(--text);
user-select: none;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
:where(button, select, option, label, .app-header, .menu-bar, .view-sidebar, .settings-sidebar, .queue-table th, .recent-files-table th, .history-table th, .resize-handle, .context-menu, .modal-header, .modal-footer, .account-card-drag-handle, .account-hoster-group-header, .account-hoster-settings-header) {
user-select: none;
}
:where(.status-badge, .health-badge, .account-status, .panel-status, .account-modal-status, .online-backup-status, .health-check-results, .account-hoster-group-meta.error, .recent-file-row.error, .history-row.error, .log-file-path-row .key-input, .log-paths-list, .log-paths-list *, #jobLogBody, .update-release-notes, .update-release-notes *, .history-link-text, .copy-toast) {
user-select: text;
}
/* Menu Bar (Datei / Einstellungen / Hilfe) */ /* Menu Bar (Datei / Einstellungen / Hilfe) */
.menu-bar { .menu-bar {
display: flex; display: flex;
@@ -3181,23 +3188,26 @@ input[type="checkbox"] {
} }
.log-file-path-row { .log-file-path-row {
display: grid !important; display: flex !important;
grid-template-columns: 190px minmax(180px, 1fr) auto auto;
align-items: center !important; align-items: center !important;
column-gap: 8px !important; flex-wrap: wrap !important;
flex-wrap: nowrap !important; gap: 8px !important;
} }
.log-file-path-row > label { .log-file-path-row > label {
min-width: 0 !important; min-width: 140px !important;
flex: 1 1 190px;
} }
.log-file-path-row > .key-input { .log-file-path-row > .key-input {
width: 100%; width: auto;
min-width: 0 !important; min-width: 180px !important;
max-width: 100%;
flex: 999 1 260px;
} }
.log-file-path-row .btn { .log-file-path-row .btn {
flex: 0 0 auto;
white-space: nowrap; white-space: nowrap;
} }
@@ -3344,6 +3354,28 @@ input[type="checkbox"] {
background: var(--bg-secondary); background: var(--bg-secondary);
} }
.account-hoster-group {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.account-card {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto auto;
}
.account-otp-action {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
}
.account-otp-input {
width: 100%;
min-width: 0;
max-width: none;
}
.account-hoster-group-header { .account-hoster-group-header {
min-height: 40px; min-height: 40px;
padding: 8px 12px; padding: 8px 12px;
@@ -3460,13 +3492,37 @@ input[type="checkbox"] {
} }
.copy-toast { .copy-toast {
bottom: 44px; top: 60px;
right: 12px;
bottom: auto;
left: auto;
width: max-content;
max-width: min(420px, calc(100vw - 24px));
overflow-wrap: anywhere;
white-space: normal;
transform: translateY(-8px);
border-radius: 6px; border-radius: 6px;
background: rgba(35, 35, 35, .96); background: rgba(35, 35, 35, .96);
color: var(--success); color: var(--success);
box-shadow: 0 12px 30px rgba(0, 0, 0, .38); box-shadow: 0 12px 30px rgba(0, 0, 0, .38);
} }
.copy-toast.show {
transform: translateY(0);
}
.status-badge {
min-width: 0;
max-width: 100%;
}
.status-badge-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px; height: 8px;
@@ -3534,6 +3590,90 @@ input[type="checkbox"] {
display: none; display: none;
} }
#upload-view.active {
grid-template-columns: clamp(196px, 24vw, 224px) minmax(0, 1fr);
}
#upload-view .view-sidebar {
padding: 14px 10px 10px;
overflow-y: auto;
}
#upload-view .view-sidebar-section,
#upload-view .upload-sidebar-lower {
display: grid;
}
#upload-view .view-sidebar-copy,
#upload-view .view-sidebar-badge {
display: block;
}
#upload-view .upload-sidebar-lower {
min-width: 0;
gap: 10px;
margin-top: 10px;
flex: 0 0 auto;
}
#upload-view .upload-sidebar-lower .view-sidebar-section {
padding-top: 10px;
}
#upload-view .upload-telemetry {
gap: 4px;
padding-top: 8px;
}
#upload-view .view-sidebar-navigation {
gap: 4px;
}
#upload-view .view-sidebar-item {
min-height: 34px;
grid-template-columns: 20px minmax(0, 1fr) auto;
place-items: initial;
align-items: center;
padding: 6px 8px;
}
#upload-view .view-sidebar-icon {
width: 18px;
height: 18px;
}
#upload-view .upload-telemetry-row,
#upload-view .upload-availability,
#upload-view .hoster-summary {
min-width: 0;
}
#upload-view .upload-telemetry-row {
gap: 8px;
}
#upload-view .upload-telemetry-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#upload-view .hoster-summary {
overflow-wrap: anywhere;
}
.account-card {
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: start;
}
.account-card-actions {
grid-column: 2 / -1;
justify-content: flex-end;
flex-wrap: wrap;
}
.upload-speed-sparkline { .upload-speed-sparkline {
width: 126px; width: 126px;
grid-template-columns: minmax(0, 1fr) 56px; grid-template-columns: minmax(0, 1fr) 56px;
@@ -3646,38 +3786,57 @@ input[type="checkbox"] {
} }
.settings-layout { .settings-layout {
height: 100%; height: auto;
grid-template-columns: 176px minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
overflow: hidden; overflow: hidden;
} }
.settings-container { .settings-container {
overflow: hidden; overflow-y: auto;
}
.settings-hosters {
flex: 0 0 auto;
min-width: 0;
} }
.settings-sidebar { .settings-sidebar {
overflow-y: auto; min-width: 0;
border-right: 1px solid var(--border); overflow: visible;
border-bottom: 0; border-right: 0;
border-bottom: 1px solid var(--border);
} }
.settings-navigation { .settings-navigation {
display: flex; display: grid;
grid-template-columns: none; grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.settings-content { .settings-content {
overflow-y: auto; min-width: 0;
overflow: visible;
} }
.settings-search-wrap > label { .settings-search-wrap > label {
display: none; display: block;
} }
.settings-content { .settings-content {
padding: 18px 16px 28px; padding: 18px 16px 28px;
} }
.settings-search-wrap,
.settings-search-control,
#settingsSearchInput,
.settings-nav-button {
min-width: 0;
max-width: 100%;
}
.settings-sidebar-status {
margin-top: 0;
}
.settings-subpage .settings-row > label { .settings-subpage .settings-row > label {
min-width: 100%; min-width: 100%;
} }
@@ -3690,6 +3849,12 @@ input[type="checkbox"] {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.log-file-path-row > label,
.log-file-path-row > .key-input {
min-width: 0 !important;
flex-basis: 100%;
}
.upload-toolbar, .upload-toolbar,
.accounts-header, .accounts-header,
.history-header, .history-header,
@@ -3711,7 +3876,38 @@ input[type="checkbox"] {
} }
.accounts-auto-check { .accounts-auto-check {
display: none; display: flex;
}
.accounts-header {
flex-wrap: wrap;
}
.accounts-header-actions {
width: 100%;
justify-content: flex-start;
}
.account-card {
grid-template-columns: auto minmax(0, 1fr);
}
.account-status,
.account-card-actions {
grid-column: 2;
justify-self: start;
}
.account-card-actions {
justify-content: flex-start;
}
.account-otp-action {
grid-template-columns: minmax(0, 1fr);
}
.account-otp-action .btn {
width: 100%;
} }
.history-retention-select { .history-retention-select {
+36
View File
@@ -25,3 +25,39 @@ test('sidebar hierarchy uses distinct English and German kicker labels', () => {
assert.equal(translateText('Manage accounts', 'de'), 'Accounts verwalten'); assert.equal(translateText('Manage accounts', 'de'), 'Accounts verwalten');
assert.equal(translateText('Archive', 'de'), 'Archiv'); assert.equal(translateText('Archive', 'de'), 'Archiv');
}); });
test('runtime queue, account, toast, and shutdown copy translates completely', () => {
const cases = [
['Wartet', 'Waiting'],
['Abgebrochen', 'Canceled'],
['Fehlgeschlagen: Verbindung verloren', 'Failed: Connection lost'],
['Retry 2/3 · Primär nicht verfügbar', 'Retry 2/3 · Primary unavailable'],
['Link kopiert', 'Link copied'],
['Kopiert!', 'Copied!'],
['Sende…', 'Sending…'],
['Lade…', 'Loading…'],
['Upload-Statistiken', 'Upload statistics'],
['Ruhezustand in 30s...', 'Sleep in 30s...'],
['Herunterfahren in 30s...', 'Shut down in 30s...']
];
for (const [german, english] of cases) {
assert.equal(translateText(german, 'en'), english);
assert.equal(translateText(english, 'de'), german);
}
});
test('English copy uses complete actions and correct singular plurals', () => {
assert.equal(translateText('Fehlgeschlagene erneut', 'en'), 'Retry failed uploads');
assert.equal(translateText('Suche Aktualisierungen', 'en'), 'Check for updates');
assert.equal(translateText('1 Job zum erneuten Upload zurückgesetzt', 'en'), '1 job reset for upload');
assert.equal(translateText('2 Jobs zum erneuten Upload zurückgesetzt', 'en'), '2 jobs reset for upload');
assert.equal(translateText('1 Verlaufseintrag wird dauerhaft entfernt.', 'en'), '1 history entry will be permanently removed.');
assert.equal(translateText('2 Verlaufseinträge werden dauerhaft entfernt.', 'en'), '2 history entries will be permanently removed.');
assert.equal(translateText('Wiederholungen', 'en'), 'Retries');
assert.equal(translateText('Maximale Geschwindigkeit (MB/s)', 'en'), 'Maximum speed (MB/s)');
assert.equal(translateText('Neustart unter (kB/s)', 'en'), 'Restart below (kB/s)');
assert.equal(translateText('Maximale Größe (MB)', 'en'), 'Maximum size (MB)');
assert.equal(translateText('Aktiv auf Port 9100 — 1 Client verbunden', 'en'), 'Active on port 9100 — 1 client connected');
assert.equal(translateText('Aktiv auf Port 9100 — 2 Clients verbunden', 'en'), 'Active on port 9100 — 2 clients connected');
});
+152 -3
View File
@@ -164,7 +164,7 @@ setTimeout(async () => {
await captureVisual('00-language-picker.png'); await captureVisual('00-language-picker.png');
await wc.executeJavaScript('document.getElementById("upload-tab").click()'); await wc.executeJavaScript('document.getElementById("upload-tab").click()');
const unchangedValues = await wc.executeJavaScript('(() => { setUiLanguage("de"); const nodes = []; const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); let node = walker.nextNode(); while (node) { if (node.nodeValue.trim()) nodes.push({ node, source: node.nodeValue.trim() }); node = walker.nextNode(); } const attributes = [...document.querySelectorAll("[title],[aria-label],[placeholder],[data-tooltip]")].flatMap(element => ["title", "aria-label", "placeholder", "data-tooltip"].filter(name => element.hasAttribute(name)).map(name => ({ element, name, source: element.getAttribute(name).trim() }))); setUiLanguage("en"); const unchanged = nodes.filter(entry => entry.source === entry.node.nodeValue.trim()).map(entry => entry.source); unchanged.push(...attributes.filter(entry => entry.source === entry.element.getAttribute(entry.name).trim()).map(entry => entry.source)); return [...new Set(unchanged.filter(value => /[A-Za-zÄÖÜäöüß]{2}/.test(value)))].sort(); })()'); const unchangedValues = await wc.executeJavaScript('(() => { setUiLanguage("de"); const nodes = []; const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); let node = walker.nextNode(); while (node) { if (node.nodeValue.trim()) nodes.push({ node, source: node.nodeValue.trim() }); node = walker.nextNode(); } const attributes = [...document.querySelectorAll("[title],[aria-label],[placeholder],[data-tooltip]")].flatMap(element => ["title", "aria-label", "placeholder", "data-tooltip"].filter(name => element.hasAttribute(name)).map(name => ({ element, name, source: element.getAttribute(name).trim() }))); setUiLanguage("en"); const unchanged = nodes.filter(entry => entry.source === entry.node.nodeValue.trim()).map(entry => entry.source); unchanged.push(...attributes.filter(entry => entry.source === entry.element.getAttribute(entry.name).trim()).map(entry => entry.source)); return [...new Set(unchanged.filter(value => /[A-Za-zÄÖÜäöüß]{2}/.test(value)))].sort(); })()');
const neutralUiValues = new Set(['0 kB/s', 'Accounts', 'BBCode', 'CSV', 'Changelog', 'ETA', 'ETA --:--', 'FileUploader Log', 'HTML', 'JSON', 'Label (optional)', 'Link', 'Log', 'Logs & Support', 'MB/s', 'MHU2-…', 'MULTI-HOSTER UPLOAD', 'Markdown', 'Multi-Hoster Upload', 'OK', 'Plaintext', 'Port', 'Server', 'Status', 'Update', 'Upload', 'Uploads', 'Verbose Logging', 'Webhook', 'account-rotation.log', 'debug.log', 'doodstream-debug.log', 'fileuploader.log', 'mp4,mkv,avi']); const neutralUiValues = new Set(['0 kB/s', 'Accounts', 'BBCode', 'CSV', 'Changelog', 'ETA', 'ETA --:--', 'FileUploader Log', 'HTML', 'JSON', 'Label (optional)', 'Link', 'Log', 'Logs & Support', 'MB/s', 'MHU2-…', 'MULTI HOSTER UPLOADER', 'Markdown', 'Multi Hoster Uploader', 'OK', 'Plaintext', 'Port', 'Server', 'Status', 'Update', 'Upload', 'Uploads', 'Verbose Logging', 'Webhook', 'account-rotation.log', 'debug.log', 'doodstream-debug.log', 'fileuploader.log', 'upload-debug.log', 'mp4,mkv,avi']);
const unexpectedUnchangedValues = unchangedValues.filter(value => !neutralUiValues.has(value) && !value.includes('Multi-Hoster-Uploader')); const unexpectedUnchangedValues = unchangedValues.filter(value => !neutralUiValues.has(value) && !value.includes('Multi-Hoster-Uploader'));
if (process.env.AUDIT_I18N_UNCHANGED === '1' || unexpectedUnchangedValues.length) console.log('Unchanged i18n values: ' + JSON.stringify(unchangedValues, null, 2)); if (process.env.AUDIT_I18N_UNCHANGED === '1' || unexpectedUnchangedValues.length) console.log('Unchanged i18n values: ' + JSON.stringify(unchangedValues, null, 2));
check('Every mounted human-facing value is translated or explicitly language-neutral', unexpectedUnchangedValues.length === 0); check('Every mounted human-facing value is translated or explicitly language-neutral', unexpectedUnchangedValues.length === 0);
@@ -207,7 +207,7 @@ setTimeout(async () => {
check('App shell exposes the primary header', appHeaderExists); check('App shell exposes the primary header', appHeaderExists);
const appBrandText = await wc.executeJavaScript('document.querySelector(".app-brand-name")?.textContent?.trim()'); const appBrandText = await wc.executeJavaScript('document.querySelector(".app-brand-name")?.textContent?.trim()');
check('App header shows the Multi-Hoster Upload brand', appBrandText === 'MULTI-HOSTER UPLOAD'); check('App header shows the Multi Hoster Uploader brand', appBrandText === 'MULTI HOSTER UPLOADER');
const topbarIconCount = await wc.executeJavaScript('document.querySelectorAll(".app-header .tab .top-nav-icon").length'); const topbarIconCount = await wc.executeJavaScript('document.querySelectorAll(".app-header .tab .top-nav-icon").length');
check('App header exposes exactly four topbar icons', topbarIconCount === 4); check('App header exposes exactly four topbar icons', topbarIconCount === 4);
@@ -897,6 +897,8 @@ setTimeout(async () => {
await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'diagnose\\']")?.click()'); await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'diagnose\\']")?.click()');
const diagnoseSettingsSpacing = await wc.executeJavaScript('(() => { const grid = document.querySelector("[data-subpage=diagnose] .settings-grid-mini")?.getBoundingClientRect(); const port = document.getElementById("diagPortInput")?.closest(".settings-row")?.getBoundingClientRect(); return grid && port ? Math.round(port.top - grid.bottom) : -1; })()'); const diagnoseSettingsSpacing = await wc.executeJavaScript('(() => { const grid = document.querySelector("[data-subpage=diagnose] .settings-grid-mini")?.getBoundingClientRect(); const port = document.getElementById("diagPortInput")?.closest(".settings-row")?.getBoundingClientRect(); return grid && port ? Math.round(port.top - grid.bottom) : -1; })()');
check('Diagnose settings keep space before Port', diagnoseSettingsSpacing >= 8); check('Diagnose settings keep space before Port', diagnoseSettingsSpacing >= 8);
const diagnosticsDirtyTracking = await wc.executeJavaScript('(async () => { const original = await window.api.diagnosticsGetSettings(); const input = document.getElementById("diagPublicHostInput"); establishSettingsBaseline(); input.value = "ui-diagnostics-save.invalid"; input.dispatchEvent(new Event("input", { bubbles: true })); const button = document.getElementById("saveSettingsBtn"); const enabled = button.disabled === false && button.classList.contains("btn-success"); if (enabled) await saveSettings({ feedbackText: "Gespeichert" }); const persisted = await window.api.diagnosticsGetSettings(); await saveDiagnosticsSettingsTracked(original); input.value = original.publicHost || ""; establishSettingsBaseline(); return { enabled, persisted: persisted.publicHost }; })()');
check('Diagnostics changes enable Save and persist with the full settings form', diagnosticsDirtyTracking.enabled === true && diagnosticsDirtyTracking.persisted === 'ui-diagnostics-save.invalid');
await captureVisual('03-settings.png'); await captureVisual('03-settings.png');
@@ -1382,7 +1384,7 @@ setTimeout(async () => {
await new Promise(resolve => setTimeout(resolve, 160)); await new Promise(resolve => setTimeout(resolve, 160));
const historyClearAction = await wc.executeJavaScript('(() => { const button = document.getElementById("clearHistoryBtn"); window.__historyOriginalConfirm = window.confirm; window.__historyNativeConfirmCalls = 0; window.confirm = () => { window.__historyNativeConfirmCalls++; return false; }; button?.click(); const modal = document.getElementById("historyClearModal"); return [button?.classList.contains("btn-danger"), button?.disabled, window.__historyNativeConfirmCalls, modal?.style.display, modal?.getAttribute("aria-hidden"), document.getElementById("historyClearModalTitle")?.textContent?.trim(), document.activeElement?.id].join("|"); })()'); const historyClearAction = await wc.executeJavaScript('(() => { const button = document.getElementById("clearHistoryBtn"); window.__historyOriginalConfirm = window.confirm; window.__historyNativeConfirmCalls = 0; window.confirm = () => { window.__historyNativeConfirmCalls++; return false; }; button?.click(); const modal = document.getElementById("historyClearModal"); return [button?.classList.contains("btn-danger"), button?.disabled, window.__historyNativeConfirmCalls, modal?.style.display, modal?.getAttribute("aria-hidden"), document.getElementById("historyClearModalTitle")?.textContent?.trim(), document.activeElement?.id].join("|"); })()');
check('History clear uses a red enabled action and opens the styled confirmation dialog', historyClearAction === 'true|false|0|flex|false|Verlauf löschen?|confirmHistoryClearBtn'); check('History clear uses a red enabled action and opens the styled confirmation dialog with safe default focus', historyClearAction === 'true|false|0|flex|false|Verlauf löschen?|cancelHistoryClearBtn');
const historyClearMessage = await wc.executeJavaScript('document.getElementById("historyClearModalMessage")?.textContent?.trim()'); const historyClearMessage = await wc.executeJavaScript('document.getElementById("historyClearModalMessage")?.textContent?.trim()');
check('History clear dialog explains that deletion is permanent', historyClearMessage === 'Alle Verlaufseinträge werden dauerhaft gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden.'); check('History clear dialog explains that deletion is permanent', historyClearMessage === 'Alle Verlaufseinträge werden dauerhaft gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden.');
await captureVisual('04-history-clear-modal.png'); await captureVisual('04-history-clear-modal.png');
@@ -1469,6 +1471,54 @@ setTimeout(async () => {
const dynamicEnglishResidue = dynamicEnglishValues.filter(containsGermanTerm); const dynamicEnglishResidue = dynamicEnglishValues.filter(containsGermanTerm);
if (dynamicEnglishResidue.length) console.log('Dynamic English residue: ' + JSON.stringify(dynamicEnglishResidue, null, 2)); if (dynamicEnglishResidue.length) console.log('Dynamic English residue: ' + JSON.stringify(dynamicEnglishResidue, null, 2));
check('English translation covers dynamically rendered interface states', dynamicEnglishResidue.length === 0); check('English translation covers dynamically rendered interface states', dynamicEnglishResidue.length === 0);
const explicitDynamicEnglish = await wc.executeJavaScript(\`(() => {
queueJobs = [
{ id: 'ui-waiting', file: 'C:/ui/waiting.bin', fileName: 'waiting.bin', hoster: 'byse.sx', status: 'queued', bytesUploaded: 0, bytesTotal: 100, progress: 0 },
{ id: 'ui-aborted', file: 'C:/ui/aborted.bin', fileName: 'aborted.bin', hoster: 'byse.sx', status: 'aborted', error: 'Abgebrochen', bytesUploaded: 0, bytesTotal: 100, progress: 0 },
{ id: 'ui-failed', file: 'C:/ui/failed.bin', fileName: 'failed.bin', hoster: 'byse.sx', status: 'error', error: 'Fehlgeschlagen: Verbindung verloren', bytesUploaded: 0, bytesTotal: 100, progress: 0 }
];
rebuildJobIndex();
setUploadSidebarFilter('all');
updateUploadView();
renderQueueTable();
showCopyToast('Link kopiert', 5000);
handleShutdownCountdown({ mode: 'sleep', seconds: 30 });
const values = [...document.querySelectorAll('#queueBody .col-status')].map(cell => cell.textContent.trim());
const titles = [...document.querySelectorAll('#queueBody .col-status')].map(cell => cell.title);
const result = { values, titles, toast: document.getElementById('copyToast').textContent.trim(), shutdown: document.getElementById('shutdownMessage').textContent.trim() };
clearInterval(shutdownCountdownInterval);
document.getElementById('shutdownOverlay').style.display = 'none';
document.getElementById('copyToast').classList.remove('show');
return result;
})()\`);
const expectedDynamicEnglish = ['Waiting', 'Canceled', 'Failed: Connection lost'].sort().join('|');
const explicitDynamicEnglishValid = [...explicitDynamicEnglish.values].sort().join('|') === expectedDynamicEnglish && [...explicitDynamicEnglish.titles].sort().join('|') === expectedDynamicEnglish && explicitDynamicEnglish.toast === 'Link copied' && explicitDynamicEnglish.shutdown === 'Sleep in 30s...';
if (!explicitDynamicEnglishValid) console.log('Explicit dynamic English: ' + JSON.stringify(explicitDynamicEnglish));
check('English runtime statuses, status tooltips, toast, and shutdown copy are localized', explicitDynamicEnglishValid);
const englishSettingsSearch = await wc.executeJavaScript(\`(() => {
document.querySelector('.tab[data-view="settings"]').click();
const search = document.getElementById('settingsSearchInput');
const inspect = value => {
search.value = value;
search.dispatchEvent(new Event('input', { bubbles: true }));
return [...document.querySelectorAll('.settings-nav-button')].filter(button => !button.hidden).map(button => button.dataset.settingsPage);
};
const notifications = inspect('notifications');
const windowSettings = inspect('window');
search.value = '';
search.dispatchEvent(new Event('input', { bubbles: true }));
return { notifications, windowSettings };
})()\`);
check('English settings search finds localized concepts', englishSettingsSearch.notifications.includes('benachrichtigungen') && englishSettingsSearch.windowSettings.includes('allgemein'));
const productNaming = await wc.executeJavaScript(\`(() => ({
title: document.title,
brand: document.querySelector('.app-brand-name')?.textContent.trim(),
label: document.querySelector('.app-brand')?.getAttribute('aria-label')
}))()\`);
check('User-visible product naming is consistent', productNaming.title === 'Multi Hoster Uploader' && productNaming.brand === 'MULTI HOSTER UPLOADER' && productNaming.label === 'Multi Hoster Uploader');
await wc.executeJavaScript('setUiLanguage("de")'); await wc.executeJavaScript('setUiLanguage("de")');
console.log('\\n=== Global UI ==='); console.log('\\n=== Global UI ===');
@@ -1479,6 +1529,77 @@ setTimeout(async () => {
const toastHidden = await wc.executeJavaScript('!document.getElementById("copyToast")?.classList.contains("show")'); const toastHidden = await wc.executeJavaScript('!document.getElementById("copyToast")?.classList.contains("show")');
check('Copy toast hidden', toastHidden); check('Copy toast hidden', toastHidden);
const toastSemantics = await wc.executeJavaScript('document.getElementById("copyToast")?.getAttribute("role") + "|" + document.getElementById("copyToast")?.getAttribute("aria-live")');
check('Copy toast exposes polite status semantics', toastSemantics === 'status|polite');
const emptyActionState = await wc.executeJavaScript(\`(() => {
queueJobs = [];
sessionFilesData = [];
historyRowsData = [];
rebuildJobIndex();
renderQueueTable();
renderRecentUploadsPanel();
syncHistoryClearAction();
return [
document.getElementById('copyAllLinksBtn')?.disabled,
document.getElementById('clearRecentFilesBtn')?.disabled,
document.getElementById('exportRecentFilesBtn')?.disabled,
document.getElementById('exportHistoryBtn')?.disabled,
document.getElementById('clearHistoryBtn')?.disabled
].join('|');
})()\`);
check('Unavailable recent, queue, and history actions are disabled', emptyActionState === 'true|true|true|true|true');
const keyboardInteractionContract = await wc.executeJavaScript(\`(() => {
queueJobs = [{ id: 'ui-keyboard-row', file: 'C:/ui/keyboard.bin', fileName: 'keyboard.bin', hoster: 'byse.sx', status: 'queued', bytesUploaded: 0, bytesTotal: 100, progress: 0 }];
selectedJobIds.clear();
rebuildJobIndex();
setUploadSidebarFilter('all');
updateUploadView();
renderQueueTable();
const row = document.querySelector('#queueBody .queue-row');
row?.focus();
row?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }));
const sortable = [...document.querySelectorAll('#queueTable th.sortable')];
row?.dispatchEvent(new KeyboardEvent('keydown', { key: 'F10', shiftKey: true, bubbles: true, cancelable: true }));
const queueMenuKeyboard = document.getElementById('contextMenu')?.style.display === 'block' && document.activeElement?.closest('#contextMenu') !== null;
document.getElementById('contextMenu')?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
const recentSortable = [...document.querySelectorAll('#recentFilesHead th.sortable')];
const historySortable = [...document.querySelectorAll('#historyContainer th.sortable')];
const accountAccordions = [...document.querySelectorAll('[data-hoster-toggle], [data-hoster-settings-toggle]')];
const priorityHandles = [...document.querySelectorAll('.account-card-drag-handle')];
const submenuTrigger = document.querySelector('.menu-submenu-trigger');
return {
rowFocusable: row?.tabIndex === 0,
rowSelected: selectedJobIds.has('ui-keyboard-row'),
sortableKeyboard: sortable.every(header => header.tabIndex === 0 && header.hasAttribute('aria-sort')),
dropzoneButton: document.getElementById('dropZone')?.getAttribute('role') === 'button' && document.getElementById('dropZone')?.tabIndex === 0,
menus: document.getElementById('contextMenu')?.getAttribute('role') === 'menu' && [...document.querySelectorAll('#contextMenu .ctx-item')].every(item => item.getAttribute('role') === 'menuitem' && item.tabIndex === -1),
queueMenuKeyboard,
secondarySortKeyboard: [...recentSortable, ...historySortable].every(header => header.tabIndex === 0 && header.hasAttribute('aria-sort')),
accountKeyboard: accountAccordions.length > 0 && accountAccordions.every(header => header.getAttribute('role') === 'button' && header.tabIndex === 0) && priorityHandles.length > 0 && priorityHandles.every(handle => handle.getAttribute('role') === 'button' && handle.tabIndex === 0),
submenuExpanded: submenuTrigger?.hasAttribute('aria-expanded') && submenuTrigger?.getAttribute('aria-haspopup') === 'menu'
};
})()\`);
check('Queue, sorting, dropzone, context menus, and backup submenu expose keyboard interaction', Object.values(keyboardInteractionContract).every(Boolean));
const dialogContract = await wc.executeJavaScript(\`showAppConfirm({ title: 'Prüfung', message: 'Wirklich fortfahren?', confirmText: 'Fortfahren', danger: true }).then(result => window.__appDialogResult = result); (() => {
const modal = document.getElementById('appAlertModal');
const dialog = modal?.querySelector('[role="dialog"]');
const backgroundInert = document.querySelector('.app-header')?.inert === true && document.querySelector('.view.active')?.inert === true;
return [modal?.style.display, dialog?.getAttribute('aria-modal'), document.activeElement?.id, backgroundInert].join('|');
})()\`);
check('Styled app dialog focuses the safe action and makes background inert', dialogContract === 'flex|true|appAlertCancelBtn|true');
await wc.executeJavaScript('document.getElementById("appAlertCancelBtn")?.click()');
const modalSemantics = await wc.executeJavaScript(\`(() => [
document.querySelector('#hosterModal .modal-card')?.getAttribute('role'),
document.querySelector('#jobLogModal .modal-card')?.getAttribute('role'),
document.querySelector('#deleteAccountModal .modal-card')?.getAttribute('role'),
document.querySelector('#shutdownOverlay .shutdown-box')?.getAttribute('role')
].join('|'))()\`);
check('Hoster, job log, delete-account, and shutdown surfaces expose dialog semantics', modalSemantics === 'dialog|dialog|dialog|dialog');
const updateHidden = await wc.executeJavaScript('document.getElementById("updateBanner")?.style.display'); const updateHidden = await wc.executeJavaScript('document.getElementById("updateBanner")?.style.display');
check('Update banner hidden', updateHidden === 'none'); check('Update banner hidden', updateHidden === 'none');
@@ -1514,6 +1635,30 @@ setTimeout(async () => {
})()\`); })()\`);
} }
const compactSettingsHeader = await wc.executeJavaScript('document.querySelector(".tab[data-view=settings]").click(); document.querySelector(".settings-header")?.getBoundingClientRect().height'); const compactSettingsHeader = await wc.executeJavaScript('document.querySelector(".tab[data-view=settings]").click(); document.querySelector(".settings-header")?.getBoundingClientRect().height');
const minimumResponsiveContract = await wc.executeJavaScript(\`(() => {
const fits = element => !element || element.scrollWidth <= element.clientWidth + 1;
const settingsSidebar = document.querySelector('.settings-sidebar');
const settingsSearch = document.querySelector('.settings-search-control');
document.querySelector('[data-settings-page="logs"]')?.click();
const logRowsFit = [...document.querySelectorAll('[data-subpage="logs"] .settings-row')].every(fits);
document.querySelector('.tab[data-view="accounts"]').click();
const autoCheck = document.querySelector('.accounts-auto-check');
const autoCheckVisible = Boolean(autoCheck && getComputedStyle(autoCheck).display !== 'none' && autoCheck.getBoundingClientRect().width > 0);
const accountsMain = document.querySelector('#accounts-view .view-main');
const accountsMainFits = fits(accountsMain);
document.querySelector('.tab[data-view="upload"]').click();
const telemetry = document.getElementById('uploadTelemetry');
const availability = document.getElementById('uploadAvailability');
return {
settingsSidebarFits: fits(settingsSidebar),
settingsSearchFits: fits(settingsSearch),
logRowsFit,
autoCheckVisible,
accountsMainFits,
telemetryVisible: Boolean(telemetry && getComputedStyle(telemetry).display !== 'none'),
availabilityVisible: Boolean(availability && getComputedStyle(availability).display !== 'none')
};
})()\`);
win.setBounds(originalBounds); win.setBounds(originalBounds);
await new Promise(resolve => setTimeout(resolve, 150)); await new Promise(resolve => setTimeout(resolve, 150));
await wc.executeJavaScript('queueJobs = []; rebuildJobIndex(); updateUploadView(); renderQueueTable(); updateStatusBar();'); await wc.executeJavaScript('queueJobs = []; rebuildJobIndex(); updateUploadView(); renderQueueTable(); updateStatusBar();');
@@ -1524,6 +1669,10 @@ setTimeout(async () => {
check('Sidebar indicator stays aligned at the standard window size', queueProgressVisibility.standard.sidebarIndicatorAligned); check('Sidebar indicator stays aligned at the standard window size', queueProgressVisibility.standard.sidebarIndicatorAligned);
check('Sidebar indicator stays aligned at the minimum window size', queueProgressVisibility.minimum.sidebarIndicatorAligned); check('Sidebar indicator stays aligned at the minimum window size', queueProgressVisibility.minimum.sidebarIndicatorAligned);
check('Minimum window keeps the settings header compact', compactSettingsHeader <= 58); check('Minimum window keeps the settings header compact', compactSettingsHeader <= 58);
check('Minimum settings sidebar, search, and log rows stay contained', minimumResponsiveContract.settingsSidebarFits && minimumResponsiveContract.settingsSearchFits && minimumResponsiveContract.logRowsFit);
if (!(minimumResponsiveContract.autoCheckVisible && minimumResponsiveContract.accountsMainFits)) console.log('Minimum responsive contract: ' + JSON.stringify(minimumResponsiveContract));
check('Minimum Accounts keeps auto-check reachable and content contained', minimumResponsiveContract.autoCheckVisible && minimumResponsiveContract.accountsMainFits);
check('Minimum Uploads preserves availability and telemetry information', minimumResponsiveContract.telemetryVisible && minimumResponsiveContract.availabilityVisible);
const updateOverlayState = await wc.executeJavaScript('_knownUpdateInfo = { available: true, remoteVersion: "9.9.9" }; _syncHeaderUpdateState(); document.getElementById("headerUpdateBtn").focus(); showUpdateBanner({ remoteVersion: "9.9.9", releaseNotes: "\\\\n\\\\n\\\\n## New in this version\\\\n\\\\n\\\\n### Menus and navigation\\\\n\\\\n- Added live language switching.\\\\n- Improved settings layout.\\\\n\\\\n\\\\n" }); (() => { const overlay = document.getElementById("updateBanner"); const dialog = overlay?.querySelector(".update-dialog"); const button = document.getElementById("headerUpdateBtn"); return [overlay?.classList.contains("update-overlay"), overlay?.style.display, dialog?.getAttribute("role"), dialog?.getAttribute("aria-modal"), button?.hidden, getComputedStyle(button).display].join("|"); })()'); const updateOverlayState = await wc.executeJavaScript('_knownUpdateInfo = { available: true, remoteVersion: "9.9.9" }; _syncHeaderUpdateState(); document.getElementById("headerUpdateBtn").focus(); showUpdateBanner({ remoteVersion: "9.9.9", releaseNotes: "\\\\n\\\\n\\\\n## New in this version\\\\n\\\\n\\\\n### Menus and navigation\\\\n\\\\n- Added live language switching.\\\\n- Improved settings layout.\\\\n\\\\n\\\\n" }); (() => { const overlay = document.getElementById("updateBanner"); const dialog = overlay?.querySelector(".update-dialog"); const button = document.getElementById("headerUpdateBtn"); return [overlay?.classList.contains("update-overlay"), overlay?.style.display, dialog?.getAttribute("role"), dialog?.getAttribute("aria-modal"), button?.hidden, getComputedStyle(button).display].join("|"); })()');
check('Available update opens an accessible update dialog', updateOverlayState === 'true|flex|dialog|true|false|flex'); check('Available update opens an accessible update dialog', updateOverlayState === 'true|flex|dialog|true|false|flex');