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:
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
+1
-1
@@ -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": {
|
||||
|
||||
+485
-176
File diff suppressed because it is too large
Load Diff
+65
-2
@@ -4,6 +4,7 @@
|
||||
if (root) root.I18n = api;
|
||||
})(typeof window !== 'undefined' ? window : globalThis, function () {
|
||||
const pairs = [
|
||||
['Alternative', 'Alternate'],
|
||||
['Arbeitsbereich', 'Workspace'],
|
||||
['Accounts verwalten', 'Manage accounts'],
|
||||
['Archiv', 'Archive'],
|
||||
@@ -17,6 +18,10 @@
|
||||
['Ausgabe-Format der kopierten Links', 'Output format for copied links'],
|
||||
['Auto-Check vor Upload', 'Auto-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.'],
|
||||
['Bindet nur an', 'Binds only to'],
|
||||
['Bindet an', 'Binds to'],
|
||||
@@ -27,7 +32,7 @@
|
||||
['Erlaubt', 'Allowed'],
|
||||
['Erneut versuchen', 'Retry'],
|
||||
['Exportieren', 'Export'],
|
||||
['Fehlgeschlagene erneut', 'Retry failed'],
|
||||
['Fehlgeschlagene erneut', 'Retry failed uploads'],
|
||||
['Fensterverhalten, Drop-Target und Programmupdates.', 'Window behavior, drop target, and application updates.'],
|
||||
['Gesamt:', 'Total:'],
|
||||
['Globales Speed-Limit', 'Global speed limit'],
|
||||
@@ -52,6 +57,8 @@
|
||||
['Noch keine Accounts', 'No accounts yet'],
|
||||
['Nur lokal (127.0.0.1) — Tunnel/VPN', 'Local only (127.0.0.1) — tunnel/VPN'],
|
||||
['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:'],
|
||||
['Rest', 'Remaining'],
|
||||
['Schnell finden', 'Quick find'],
|
||||
@@ -67,7 +74,7 @@
|
||||
['Verbindungen', 'Connections'],
|
||||
['Verbleibend', 'Remaining'],
|
||||
['Läuft', 'Running'],
|
||||
['Suche Aktualisierungen', 'Checking for updates'],
|
||||
['Suche Aktualisierungen', 'Check for updates'],
|
||||
['Verbleibend:', 'Remaining:'],
|
||||
['Zuletzt erzeugte Upload-Links', 'Recently generated upload links'],
|
||||
['User-ID, role:ROLLEN-ID, @here oder @everyone', 'User ID, role:ROLE-ID, @here, or @everyone'],
|
||||
@@ -77,6 +84,7 @@
|
||||
['Datei', 'File'],
|
||||
['Dateien', 'Files'],
|
||||
['Dateien hinzufügen', 'Add files'],
|
||||
['Dateien oder Ordner hinzufügen', 'Add files or folders'],
|
||||
['Ordner hinzufügen', 'Add folder'],
|
||||
['Online-Schlüssel erstellen', 'Create online key'],
|
||||
['Online-Schlüssel importieren', 'Import online key'],
|
||||
@@ -428,6 +436,32 @@
|
||||
['Verschlüssele und speichere Einstellungen…', 'Encrypting and saving settings…'],
|
||||
['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.'],
|
||||
['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']
|
||||
];
|
||||
const deToEn = new Map(pairs);
|
||||
@@ -448,6 +482,24 @@
|
||||
const patterns = target === 'en'
|
||||
? [
|
||||
[/^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'],
|
||||
[/^(\d+) Fehler$/, '$1 errors'],
|
||||
[/^Gesamt (\d+)$/, 'Total $1'],
|
||||
@@ -467,6 +519,8 @@
|
||||
[/^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 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)'],
|
||||
[/^API: (.+)$/, 'API: $1'],
|
||||
[/^Ausgewählte starten \((\d+)\)$/, 'Start selected ($1)'],
|
||||
@@ -502,6 +556,15 @@
|
||||
]
|
||||
: [
|
||||
[/^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'],
|
||||
[/^Running (\d+)$/, 'Läuft $1'],
|
||||
[/^Failed (\d+)$/, 'Fehler $1'],
|
||||
|
||||
+41
-40
@@ -27,9 +27,9 @@
|
||||
|
||||
<header class="app-header">
|
||||
<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">
|
||||
<span class="app-brand-name">MULTI-HOSTER UPLOAD</span>
|
||||
<span class="app-brand-name">MULTI HOSTER UPLOADER</span>
|
||||
</div>
|
||||
<span class="header-divider" aria-hidden="true"></span>
|
||||
<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>
|
||||
<div class="menu-separator"></div>
|
||||
<div class="menu-submenu" data-submenu="sicherung">
|
||||
<button class="menu-submenu-trigger">Sicherung</button>
|
||||
<div class="menu-submenu-dropdown" style="display:none">
|
||||
<button class="menu-submenu-trigger" aria-haspopup="menu" aria-expanded="false">Sicherung</button>
|
||||
<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-import"><span>Importieren</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 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>
|
||||
<p>Dateien hierher ziehen oder klicken</p>
|
||||
<span>Dateien und Ordner werden vor dem Upload geprüft.</span>
|
||||
@@ -280,14 +280,14 @@
|
||||
<table class="queue-table" id="queueTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-filename sortable" data-col="filename" data-sort="filename">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-host sortable" data-col="host" data-sort="host">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-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" 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" tabindex="0" aria-sort="none">Hoster<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-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-progress sortable" data-col="progress" data-sort="progress">Fortschritt</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" tabindex="0" aria-sort="none">Fortschritt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="queueBody"></tbody>
|
||||
@@ -324,10 +324,10 @@
|
||||
<table class="recent-files-table">
|
||||
<thead id="recentFilesHead">
|
||||
<tr>
|
||||
<th class="col-date sortable" data-recent-sort="date">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-host sortable" data-recent-sort="host">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-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" tabindex="0" aria-sort="none">Dateiname<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" tabindex="0" aria-sort="none">Link<span class="sort-indicator">↕</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recentFilesBody"></tbody>
|
||||
@@ -451,7 +451,7 @@
|
||||
</div>
|
||||
|
||||
<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><h3 id="jobLogTitle">Upload-Log</h3></div>
|
||||
<button class="icon-btn" id="closeJobLogBtn" aria-label="Schließen">×</button>
|
||||
@@ -467,9 +467,9 @@
|
||||
</div>
|
||||
|
||||
<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><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">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -492,7 +492,8 @@
|
||||
<p id="appAlertMessage"></p>
|
||||
</div>
|
||||
<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>
|
||||
</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="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="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="1000" aria-selected="false">Letzte 1000 Uploads</button>
|
||||
</div>
|
||||
</div>
|
||||
<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="30d">Letzte 30 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="1000">Letzte 1000 Uploads</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary" id="exportHistoryBtn">Verlauf exportieren</button>
|
||||
<button class="btn btn-danger" id="clearHistoryBtn" disabled>Verlauf löschen</button>
|
||||
@@ -598,42 +599,42 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="context-menu" id="contextMenu" style="display:none">
|
||||
<div class="ctx-item" data-action="start-selected">Ausgewählte starten</div>
|
||||
<div class="ctx-item" data-action="retry-selected">Erneut versuchen</div>
|
||||
<div class="ctx-item" data-action="show-log">Log anzeigen</div>
|
||||
<div class="context-menu" id="contextMenu" role="menu" style="display:none">
|
||||
<div class="ctx-item" role="menuitem" tabindex="-1" data-action="start-selected">Ausgewählte starten</div>
|
||||
<div class="ctx-item" role="menuitem" tabindex="-1" data-action="retry-selected">Erneut versuchen</div>
|
||||
<div class="ctx-item" role="menuitem" tabindex="-1" data-action="show-log">Log anzeigen</div>
|
||||
<div class="ctx-separator"></div>
|
||||
<div class="ctx-item" 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-links">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-item" 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-selected">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-item ctx-item-danger">Hoster entfernen ▸</div>
|
||||
<div class="ctx-item ctx-item-danger" role="menuitem" tabindex="-1">Hoster entfernen ▸</div>
|
||||
<div class="ctx-submenu-items ctx-hoster-delete-items"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="context-menu" id="recentContextMenu" style="display:none">
|
||||
<div class="ctx-item" data-action="recent-copy-links">Links kopieren</div>
|
||||
<div class="ctx-item" data-action="recent-delete">Entfernen</div>
|
||||
<div class="context-menu" id="recentContextMenu" role="menu" style="display:none">
|
||||
<div class="ctx-item" role="menuitem" tabindex="-1" data-action="recent-copy-links">Links kopieren</div>
|
||||
<div class="ctx-item" role="menuitem" tabindex="-1" data-action="recent-delete">Entfernen</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-box">
|
||||
<div class="shutdown-overlay" id="shutdownOverlay" style="display:none" aria-hidden="true">
|
||||
<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>
|
||||
<button class="btn btn-danger" id="cancelShutdownBtn">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="hosterModal" style="display:none">
|
||||
<div class="modal-card">
|
||||
<div class="modal-overlay" id="hosterModal" style="display:none" aria-hidden="true">
|
||||
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="hosterModalTitle" aria-describedby="hosterModalDescription">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h3>Upload-Ziele auswählen</h3>
|
||||
<p>Dateien wurden hinzugefügt. Wähle jetzt die Hoster für den Upload.</p>
|
||||
<h3 id="hosterModalTitle">Upload-Ziele auswählen</h3>
|
||||
<p id="hosterModalDescription">Dateien wurden hinzugefügt. Wähle jetzt die Hoster für den Upload.</p>
|
||||
</div>
|
||||
<button class="icon-btn" id="closeHosterModalBtn" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
|
||||
+216
-20
@@ -32,11 +32,18 @@ body {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
color: var(--text);
|
||||
user-select: none;
|
||||
display: flex;
|
||||
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 {
|
||||
display: flex;
|
||||
@@ -3181,23 +3188,26 @@ input[type="checkbox"] {
|
||||
}
|
||||
|
||||
.log-file-path-row {
|
||||
display: grid !important;
|
||||
grid-template-columns: 190px minmax(180px, 1fr) auto auto;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
column-gap: 8px !important;
|
||||
flex-wrap: nowrap !important;
|
||||
flex-wrap: wrap !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.log-file-path-row > label {
|
||||
min-width: 0 !important;
|
||||
min-width: 140px !important;
|
||||
flex: 1 1 190px;
|
||||
}
|
||||
|
||||
.log-file-path-row > .key-input {
|
||||
width: 100%;
|
||||
min-width: 0 !important;
|
||||
width: auto;
|
||||
min-width: 180px !important;
|
||||
max-width: 100%;
|
||||
flex: 999 1 260px;
|
||||
}
|
||||
|
||||
.log-file-path-row .btn {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -3344,6 +3354,28 @@ input[type="checkbox"] {
|
||||
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 {
|
||||
min-height: 40px;
|
||||
padding: 8px 12px;
|
||||
@@ -3460,13 +3492,37 @@ input[type="checkbox"] {
|
||||
}
|
||||
|
||||
.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;
|
||||
background: rgba(35, 35, 35, .96);
|
||||
color: var(--success);
|
||||
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 {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@@ -3534,6 +3590,90 @@ input[type="checkbox"] {
|
||||
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 {
|
||||
width: 126px;
|
||||
grid-template-columns: minmax(0, 1fr) 56px;
|
||||
@@ -3646,38 +3786,57 @@ input[type="checkbox"] {
|
||||
}
|
||||
|
||||
.settings-layout {
|
||||
height: 100%;
|
||||
grid-template-columns: 176px minmax(0, 1fr);
|
||||
height: auto;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.settings-hosters {
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settings-sidebar {
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid var(--border);
|
||||
border-bottom: 0;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.settings-navigation {
|
||||
display: flex;
|
||||
grid-template-columns: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.settings-search-wrap > label {
|
||||
display: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
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 {
|
||||
min-width: 100%;
|
||||
}
|
||||
@@ -3690,6 +3849,12 @@ input[type="checkbox"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.log-file-path-row > label,
|
||||
.log-file-path-row > .key-input {
|
||||
min-width: 0 !important;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.upload-toolbar,
|
||||
.accounts-header,
|
||||
.history-header,
|
||||
@@ -3711,7 +3876,38 @@ input[type="checkbox"] {
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -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('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
@@ -164,7 +164,7 @@ setTimeout(async () => {
|
||||
await captureVisual('00-language-picker.png');
|
||||
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 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'));
|
||||
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);
|
||||
@@ -207,7 +207,7 @@ setTimeout(async () => {
|
||||
check('App shell exposes the primary header', appHeaderExists);
|
||||
|
||||
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');
|
||||
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()');
|
||||
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);
|
||||
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');
|
||||
|
||||
@@ -1382,7 +1384,7 @@ setTimeout(async () => {
|
||||
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("|"); })()');
|
||||
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()');
|
||||
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');
|
||||
@@ -1469,6 +1471,54 @@ setTimeout(async () => {
|
||||
const dynamicEnglishResidue = dynamicEnglishValues.filter(containsGermanTerm);
|
||||
if (dynamicEnglishResidue.length) console.log('Dynamic English residue: ' + JSON.stringify(dynamicEnglishResidue, null, 2));
|
||||
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")');
|
||||
|
||||
console.log('\\n=== Global UI ===');
|
||||
@@ -1479,6 +1529,77 @@ setTimeout(async () => {
|
||||
const toastHidden = await wc.executeJavaScript('!document.getElementById("copyToast")?.classList.contains("show")');
|
||||
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');
|
||||
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 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);
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
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 minimum window size', queueProgressVisibility.minimum.sidebarIndicatorAligned);
|
||||
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("|"); })()');
|
||||
check('Available update opens an accessible update dialog', updateOverlayState === 'true|flex|dialog|true|false|flex');
|
||||
|
||||
Reference in New Issue
Block a user