diff --git a/package-lock.json b/package-lock.json index 1fbb787..5f88ad2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "multi-hoster-uploader", - "version": "2.0.4", + "version": "2.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "multi-hoster-uploader", - "version": "2.0.4", + "version": "2.0.5", "dependencies": { "chokidar": "^3.6.0", "undici": "^7.29.0", diff --git a/package.json b/package.json index bc06e88..7565aa1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multi-hoster-uploader", - "version": "2.0.4", + "version": "2.0.5", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "main": "main.js", "scripts": { diff --git a/renderer/app.js b/renderer/app.js index 6bf3b16..75cf869 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -1902,7 +1902,12 @@ async function doOnlineBackupCreate() { function openOnlineBackupView(focusRestore = false) { document.querySelector('.tab[data-view="settings"]')?.click(); - document.querySelector('[data-subtab="backup"]')?.click(); + const searchInput = document.getElementById('settingsSearchInput'); + if (searchInput && searchInput.value) { + searchInput.value = ''; + searchInput.dispatchEvent(new window.Event('input', { bubbles: true })); + } + document.querySelector('[data-settings-page="backup"]')?.click(); if (focusRestore) document.getElementById('onlineBackupKeyInput')?.focus(); } @@ -3183,92 +3188,122 @@ function renderSettings() { const fm = globalSettings.folderMonitor || {}; const remoteSettings = globalSettings.remote || {}; - const subtabBar = document.createElement('div'); - subtabBar.className = 'settings-subtabs'; - subtabBar.innerHTML = ` - - - - - - - `; - container.appendChild(subtabBar); + const pageDefinitions = [ + { id: 'allgemein', label: 'Allgemein', search: 'fenster vordergrund drop target oberfläche updates aktualisierung version' }, + { id: 'uploads', label: 'Uploads', search: 'upload queue warteschlange fertig abschluss entfernen parallel geschwindigkeit speed limit fortsetzen wiederherstellen hoster' }, + { id: 'automatik', label: 'Automatik', search: 'automatisch retry wiederholen ordner überwachen dateierweiterungen unterordner duplikate' }, + { id: 'benachrichtigungen', label: 'Benachrichtigungen', search: 'webhook discord meldung ping erwähnung batch fertig' }, + { id: 'logs', label: 'Logs & Support', search: 'log protokoll debug verbose diagnose support paket datei ordner' }, + { id: 'remote', label: 'Fernsteuerung', search: 'remote fernsteuerung server input port api token verbindung' }, + { id: 'diagnose', label: 'Diagnose-Zugriff', search: 'diagnose zugriff lesen allowlist netzwerk lokal verbindung code support' }, + { id: 'backup', label: 'Backup & Übertragen', search: 'backup sichern export import online schlüssel einstellungen accounts übertragen' } + ]; + const pageHeader = (title, description) => ` +
+

${title}

+

${description}

+
`; + const layout = document.createElement('div'); + layout.className = 'settings-layout'; + layout.innerHTML = ` + +
`; + container.appendChild(layout); + + const navigation = layout.querySelector('.settings-navigation'); + const content = layout.querySelector('.settings-content'); const pages = {}; - ['allgemein', 'automatik', 'logs', 'remote', 'diagnose', 'backup'].forEach((id) => { + pageDefinitions.forEach(({ id }) => { const page = document.createElement('div'); page.className = id === 'allgemein' ? 'settings-subpage active' : 'settings-subpage'; page.dataset.subpage = id; pages[id] = page; - container.appendChild(page); + content.appendChild(page); }); pages.allgemein.innerHTML = ` -
Uploads
-
- - - 0 = nur pro Hoster -
-
- - - 0 = unbegrenzt -
-
Verhalten
+ ${pageHeader('Allgemein', 'Fensterverhalten, Drop-Target und Programmupdates.')} +
Oberfläche
- +
- - -
-
- - -
-
- - -
-
- +
-
Updates
+
Programmupdate
- +
-
Die Upload-Einstellungen pro Hoster (Retries, Speed, Parallel, Max-Größe, Log) sind jetzt im Accounts-Tab — direkt bei den jeweiligen Hostern unter „Upload-Einstellungen".
+ `; + + pages.uploads.innerHTML = ` + ${pageHeader('Upload-Verhalten', 'Globale Leistung, Warteschlange und Verhalten nach einem erfolgreichen Upload.')} +
Leistung
+
+ + + 0 = nur die Einstellung des jeweiligen Hosters verwenden +
+
+ + + MB/s · 0 = unbegrenzt +
+
+
+ + Verteilt die globale Parallelität auf vorhandene Accounts eines Hosters. +
+ +
+
Warteschlange
+
+
+ + Erfolgreich hochgeladene Dateien verschwinden automatisch aus der Upload-Liste. +
+ +
+
+
+ + Noch nicht abgeschlossene Uploads werden beim nächsten Programmstart erneut angezeigt. +
+ +
+
Einstellungen einzelner Hoster wie Wiederholungen, Geschwindigkeit, Parallelität, Dateigröße und Logging findest du im Accounts-Tab direkt beim jeweiligen Hoster.
`; pages.automatik.innerHTML = ` + ${pageHeader('Automatik', 'Wiederholungen und überwachte Ordner für unbeaufsichtigte Uploads.')}
Unbeaufsichtigter Betrieb
- + 0 = aus. Nach Batch-Ende werden transiente Fehler (Netzwerk, Hoster-Flake) automatisch bis zu N Runden neu versucht.
- + - Basis-Wartezeit; Runde N wartet N × diesen Wert (lineares Backoff). -
-
- - - - Bei Batch-Ende wird eine Zusammenfassung gepostet (Discord wird automatisch erkannt, sonst generisches JSON). -
-
- - - Damit Discord dich wirklich benachrichtigt (Push). User-ID: in Discord Entwicklermodus an → Rechtsklick auf deinen Namen → 'User-ID kopieren'. Leer = nur posten, kein Ping. + Minuten · jede weitere Runde wartet entsprechend länger
Ordnerüberwachung ${fm.enabled && fm.folderPath ? 'Aktiv' : 'Inaktiv'}
@@ -3319,7 +3354,24 @@ function renderSettings() { ${configuredAccounts.length === 0 ? '

Erst Accounts anlegen, dann hier auswählen.

' : '

Keine Auswahl = Hoster-Modal bei jeder Datei.

'} `; + pages.benachrichtigungen.innerHTML = ` + ${pageHeader('Benachrichtigungen', 'Meldungen nach einem abgeschlossenen Upload-Batch versenden.')} +
Webhook
+
+ + + + Nach Batch-Ende wird eine Zusammenfassung versendet. Discord wird automatisch erkannt, andere Ziele erhalten JSON. +
+
+ + + Optional · leer lassen, wenn die Nachricht ohne Ping gesendet werden soll +
+ `; + pages.logs.innerHTML = ` + ${pageHeader('Logs & Support', 'Protokollierung verwalten, Log-Dateien öffnen und ein bereinigtes Support-Paket erstellen.')}
Log
@@ -3358,6 +3410,7 @@ function renderSettings() { `; pages.remote.innerHTML = ` + ${pageHeader('Fernsteuerung', 'Upload-Funktionen über einen verbundenen Client steuern.')}
@@ -3386,9 +3439,10 @@ function renderSettings() { `; pages.diagnose.innerHTML = ` + ${pageHeader('Diagnose-Zugriff', 'Zeitlich kontrollierter Lesezugriff für Fehleranalyse und Support.')}

- Erlaubt Claude nur lesenden Zugriff auf Logs, Queue-Status und sanitierte Config (Passwörter/API-Keys/Token werden maskiert). Kein Bildschirm, keine Eingabe-Steuerung. Der Verbindungs-Code ist ein Zugangsschlüssel — nur mit vertrauenswürdigen Stellen teilen; bei Verdacht „Neu" klicken. Standard-Bindung ist 127.0.0.1 (nur über SSH-/VPN-Tunnel erreichbar). + Erlaubt nur lesenden Zugriff auf Logs, Queue-Status und bereinigte Einstellungen. Passwörter, API-Keys und Tokens werden maskiert. Bildschirm und Eingabesteuerung bleiben gesperrt. Der Verbindungs-Code ist ein Zugangsschlüssel — nur mit vertrauenswürdigen Stellen teilen; bei Verdacht „Neu" klicken. Standard-Bindung ist 127.0.0.1 und damit nur über einen SSH- oder VPN-Tunnel erreichbar.

@@ -3434,7 +3488,8 @@ function renderSettings() { `; pages.backup.innerHTML = ` -

Alle Accounts und Einstellungen exportieren oder importieren. Der Upload-Verlauf wird nicht übertragen und bleibt auf diesem Gerät.

+ ${pageHeader('Backup & Übertragen', 'Accounts und Einstellungen sichern oder auf ein anderes Gerät übernehmen.')} +

Der Upload-Verlauf wird nicht übertragen und bleibt auf diesem Gerät.

Verschlüsseltes Online-Backup

@@ -3464,16 +3519,59 @@ function renderSettings() {
`; - subtabBar.addEventListener('click', (e) => { - const btn = e.target.closest('[data-subtab]'); - if (!btn) return; - const target = btn.dataset.subtab; - subtabBar.querySelectorAll('.settings-subtab').forEach((b) => { - b.classList.toggle('active', b === btn); + const activateSettingsPage = (target, focus = false) => { + const activeButton = navigation.querySelector(`[data-settings-page="${target}"]`); + if (!activeButton || activeButton.hidden) return; + navigation.querySelectorAll('.settings-nav-button').forEach((button) => { + const active = button === activeButton; + button.classList.toggle('active', active); + button.setAttribute('aria-current', active ? 'page' : 'false'); }); - Object.values(pages).forEach((p) => { - p.classList.toggle('active', p.dataset.subpage === target); + Object.values(pages).forEach((page) => { + page.classList.toggle('active', page.dataset.subpage === target); }); + if (focus) activeButton.focus(); + content.scrollTop = 0; + }; + + navigation.addEventListener('click', (event) => { + const button = event.target.closest('[data-settings-page]'); + if (button) activateSettingsPage(button.dataset.settingsPage); + }); + + navigation.addEventListener('keydown', (event) => { + const button = event.target.closest('[data-settings-page]'); + if (!button || !['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return; + const visibleButtons = [...navigation.querySelectorAll('.settings-nav-button')].filter((item) => !item.hidden); + const currentIndex = visibleButtons.indexOf(button); + let nextIndex = currentIndex; + if (event.key === 'ArrowDown') nextIndex = (currentIndex + 1) % visibleButtons.length; + if (event.key === 'ArrowUp') nextIndex = (currentIndex - 1 + visibleButtons.length) % visibleButtons.length; + if (event.key === 'Home') nextIndex = 0; + if (event.key === 'End') nextIndex = visibleButtons.length - 1; + event.preventDefault(); + activateSettingsPage(visibleButtons[nextIndex].dataset.settingsPage, true); + }); + + const searchInput = layout.querySelector('#settingsSearchInput'); + const searchEmpty = layout.querySelector('#settingsSearchEmpty'); + searchInput.addEventListener('input', () => { + const query = searchInput.value.trim().toLocaleLowerCase('de-DE'); + const visibleButtons = []; + navigation.querySelectorAll('.settings-nav-button').forEach((button) => { + const visible = !query || button.dataset.search.includes(query); + button.hidden = !visible; + if (visible) visibleButtons.push(button); + }); + searchEmpty.hidden = visibleButtons.length > 0; + const activeButton = navigation.querySelector('.settings-nav-button.active'); + if (visibleButtons.length === 0) { + Object.values(pages).forEach((page) => page.classList.remove('active')); + return; + } + if (!activeButton || activeButton.hidden || !content.querySelector('.settings-subpage.active')) { + activateSettingsPage((activeButton && !activeButton.hidden ? activeButton : visibleButtons[0]).dataset.settingsPage); + } }); _renderLogPathsList(document.getElementById('logPathsList')); diff --git a/renderer/index.html b/renderer/index.html index 8121e09..fb113dc 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -307,13 +307,17 @@
-

Upload-Einstellungen

-

Hoster-Einstellungen erscheinen erst, sobald ein Account hinterlegt ist. Änderungen werden automatisch gespeichert.

-
-
- Änderungen werden automatisch gespeichert. - +
+
+

Einstellungen

+

Alle Optionen nach Aufgaben sortiert. Änderungen werden automatisch gespeichert.

+
+
+ Automatisch gespeichert + +
+
diff --git a/renderer/styles.css b/renderer/styles.css index 9a53794..b512572 100644 --- a/renderer/styles.css +++ b/renderer/styles.css @@ -816,10 +816,116 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel .ctx-submenu:hover .ctx-submenu-items { display: block; } /* Settings View */ -.settings-container { padding: 16px; overflow: auto; flex: 1; } -.settings-container { background: linear-gradient(180deg, rgba(255,255,255,0.015), transparent 24%); } -.settings-container h2 { margin-bottom: 4px; font-size: 18px; } -.settings-hint { color: var(--text-muted); font-size: 12px; margin-bottom: 12px; } +.settings-container { + padding: 12px 14px 14px; + overflow: hidden; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + gap: 10px; + background: linear-gradient(180deg, rgba(255,255,255,0.018), transparent 26%); +} +.settings-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + padding: 12px 14px; + border: 1px solid var(--border); + border-radius: 10px; + background: color-mix(in srgb, var(--bg-card) 94%, var(--accent) 6%); +} +.settings-header h2 { margin: 0 0 3px; font-size: 17px; } +.settings-hint { color: var(--text-muted); font-size: 11px; margin: 0; line-height: 1.45; } +.settings-hosters { flex: 1; min-height: 0; } +.settings-layout { + height: 100%; + min-height: 0; + display: grid; + grid-template-columns: 220px minmax(0, 1fr); + overflow: hidden; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--bg-card); +} +.settings-sidebar { + min-height: 0; + display: flex; + flex-direction: column; + gap: 10px; + padding: 14px 10px; + overflow-y: auto; + border-right: 1px solid var(--border); + background: color-mix(in srgb, var(--bg-card) 88%, var(--bg-main) 12%); +} +.settings-search-wrap { display: grid; gap: 6px; padding: 0 4px 4px; } +.settings-search-wrap > label { + color: var(--text-dim); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.07em; + text-transform: uppercase; +} +.settings-search-control { position: relative; } +.settings-search-icon { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-52%); + color: var(--text-dim); + font-size: 17px; + pointer-events: none; +} +#settingsSearchInput { + width: 100%; + min-height: 34px; + padding: 7px 9px 7px 31px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-input); + color: var(--text); + font: inherit; + font-size: 11px; +} +#settingsSearchInput::placeholder { color: var(--text-dim); } +#settingsSearchInput:focus { border-color: var(--accent); outline: none; } +.settings-navigation { display: flex; flex-direction: column; gap: 3px; } +.settings-nav-button { + min-height: 35px; + padding: 8px 11px; + border: 0; + border-left: 3px solid transparent; + border-radius: 0 8px 8px 0; + background: transparent; + color: var(--text-muted); + cursor: pointer; + font: inherit; + font-size: 12px; + font-weight: 600; + text-align: left; + transition: background 0.14s, color 0.14s, border-color 0.14s; +} +.settings-nav-button:hover { background: var(--bg-card-hover); color: var(--text); } +.settings-nav-button.active { + border-left-color: var(--accent); + background: color-mix(in srgb, var(--accent) 13%, transparent); + color: var(--text); +} +.settings-nav-button[hidden] { display: none; } +.settings-search-empty { margin: 4px; color: var(--text-dim); font-size: 11px; line-height: 1.45; } +.settings-content { + min-width: 0; + min-height: 0; + padding: 18px 20px 24px; + overflow-y: auto; + background: linear-gradient(145deg, rgba(255,255,255,0.012), transparent 42%); +} +.settings-subpage { display: none; max-width: 1180px; margin: 0 auto; } +.settings-subpage.active { display: block; } +.settings-page-header { margin-bottom: 18px; } +.settings-page-header h3 { margin: 0 0 5px; color: var(--text); font-size: 18px; } +.settings-page-header p { margin: 0; color: var(--text-muted); font-size: 12px; line-height: 1.5; } .hoster-settings-panel { background: var(--bg-card); @@ -847,37 +953,6 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel .settings-divider { height: 1px; background: var(--border); margin: 12px 0; } .hoster-panel-body h4 { font-size: 12px; color: var(--text-muted); margin-bottom: 8px; font-weight: 500; } -.settings-subtabs { - display: flex; - gap: 4px; - margin-bottom: 14px; - border-bottom: 1px solid var(--border); - flex-wrap: wrap; -} -.settings-subtab { - background: none; - border: none; - color: var(--text-muted); - font-size: 12px; - font-weight: 600; - letter-spacing: 0.03em; - text-transform: uppercase; - padding: 8px 14px; - cursor: pointer; - border-bottom: 2px solid transparent; - transition: color 0.15s, border-color 0.15s; -} -.settings-subtab:hover { color: var(--text); } -.settings-subtab.active { color: var(--text); border-bottom-color: var(--accent); } -.settings-subpage { - display: none; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: 8px; - padding: 4px 14px 14px; -} -.settings-subpage.active { display: block; } - .settings-row { display: flex; align-items: center; @@ -924,17 +999,49 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel .hs-input { max-width: 100px; } select.hs-input { max-width: none; width: auto; min-width: 140px; } .hint { font-size: 10px; color: var(--text-dim); } +.settings-subpage .settings-row { + min-height: 44px; + margin: 0 0 8px; + padding: 9px 11px; + border: 1px solid color-mix(in srgb, var(--border) 90%, transparent); + border-radius: 9px; + background: color-mix(in srgb, var(--bg-input) 62%, transparent); +} +.settings-subpage .settings-row > label { min-width: 215px; color: var(--text); font-weight: 600; } +.settings-subpage .settings-row > .key-input { max-width: none; min-width: 180px; } +.settings-subpage .settings-row > .hint { flex: 1 1 220px; line-height: 1.45; } +.settings-subpage .settings-row-wide { flex-wrap: wrap; } +.settings-subpage .settings-row-wide > .hint { flex-basis: calc(100% - 223px); margin-left: 223px; } .settings-section-label { + display: flex; + align-items: center; + gap: 8px; font-size: 10px; - font-weight: 600; + font-weight: 700; text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--text-dim); - margin: 10px 0 6px; - padding-bottom: 3px; + letter-spacing: 0.08em; + color: var(--text-muted); + margin: 20px 0 8px; + padding-bottom: 7px; border-bottom: 1px solid var(--border); } -.settings-section-label:first-child { margin-top: 0; } +.settings-page-header + .settings-section-label { margin-top: 0; } +.settings-option { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 18px; + align-items: center; + min-height: 56px; + margin-bottom: 8px; + padding: 10px 12px; + border: 1px solid color-mix(in srgb, var(--border) 90%, transparent); + border-radius: 9px; + background: color-mix(in srgb, var(--bg-input) 62%, transparent); +} +.settings-option-copy { display: grid; gap: 4px; } +.settings-option-copy label { color: var(--text); font-size: 12px; font-weight: 600; cursor: pointer; } +.settings-option-description { color: var(--text-dim); font-size: 10px; line-height: 1.45; } +.settings-option > input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; } .toggle-vis { background: transparent; @@ -947,10 +1054,10 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; } } .toggle-vis:hover { border-color: var(--border-hover); } -.settings-save-row { display: flex; justify-content: flex-end; align-items: center; gap: 8px; margin-top: 12px; } +.settings-save-row { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin: 0; } .settings-backup-section { margin-top: 24px; border-top: 1px solid var(--border); padding-top: 16px; } .settings-backup-buttons { display: flex; gap: 8px; } -.save-feedback { font-size: 12px; color: var(--success); } +.save-feedback { font-size: 11px; color: var(--text-dim); } .settings-empty { padding: 28px 16px; border: 1px dashed var(--border); @@ -963,7 +1070,26 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; } .settings-grid-mini { display: grid; grid-template-columns: 1fr 1fr; - gap: 4px 16px; + gap: 8px; +} +.settings-subpage .settings-grid-mini .settings-row.checkbox-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 14px; + min-height: 48px; + margin: 0; +} +.settings-subpage .settings-grid-mini .checkbox-row label { min-width: 0; cursor: pointer; } +.settings-subpage .settings-grid-mini .checkbox-row input[type="checkbox"] { order: 2; width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; } +.settings-hoster-pointer { + margin-top: 16px; + padding: 11px 13px; + border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--border)); + border-radius: 9px; + background: color-mix(in srgb, var(--accent) 8%, transparent); + color: var(--text-muted); + font-size: 11px; + line-height: 1.5; } /* Accounts View */ @@ -1341,6 +1467,11 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; } .shutdown-box p { margin-bottom: 16px; font-size: 16px; } @media (max-width: 900px) { + .settings-layout { grid-template-columns: 172px minmax(0, 1fr); } + .settings-content { padding: 16px; } + .settings-subpage .settings-row { align-items: flex-start; flex-wrap: wrap; } + .settings-subpage .settings-row > label { min-width: 100%; } + .settings-subpage .settings-row-wide > .hint { flex-basis: 100%; margin-left: 0; } .recent-files-panel { max-height: 220px; } @@ -1464,6 +1595,13 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; } } @media (max-width: 820px) { + .settings-container { overflow-y: auto; } + .settings-header { align-items: flex-start; flex-direction: column; } + .settings-layout { height: auto; grid-template-columns: 1fr; overflow: visible; } + .settings-sidebar { overflow: visible; border-right: 0; border-bottom: 1px solid var(--border); } + .settings-navigation { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } + .settings-content { overflow: visible; } + .settings-grid-mini { grid-template-columns: 1fr; } .online-backup-key-row { grid-template-columns: 1fr; } diff --git a/tests/ui-smoke.js b/tests/ui-smoke.js index 77891bd..03e6d05 100644 --- a/tests/ui-smoke.js +++ b/tests/ui-smoke.js @@ -16,6 +16,7 @@ const fs = require('fs'); // Create a temp script that the real Electron app will execute via --eval const testScript = ` const { app, BrowserWindow } = require('electron'); +const fs = require('fs'); // Monkey-patch: after the real window loads, run tests const origReady = app.whenReady; @@ -213,8 +214,29 @@ setTimeout(async () => { const settingsActive = await wc.executeJavaScript('document.getElementById("settings-view")?.classList.contains("active")'); check('Settings tab active', settingsActive); - const settingsSubtabs = await wc.executeJavaScript('document.querySelectorAll(".settings-subtab").length'); - check('6 settings subtabs exist', settingsSubtabs === 6); + const settingsNavigation = await wc.executeJavaScript('(() => { const buttons = [...document.querySelectorAll(".settings-nav-button")]; return [buttons.length, buttons.map(button => button.textContent.trim()).join("|"), document.querySelector(".settings-nav-button.active")?.dataset.settingsPage, document.getElementById("settingsSearchInput")?.placeholder].join("::"); })()'); + check('Settings use the task-based sidebar navigation', settingsNavigation === '8::Allgemein|Uploads|Automatik|Benachrichtigungen|Logs & Support|Fernsteuerung|Diagnose-Zugriff|Backup & Übertragen::allgemein::Einstellungen durchsuchen'); + + await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'uploads\\']")?.click()'); + const uploadSettingsState = await wc.executeJavaScript('(() => { const activePage = document.querySelector(".settings-subpage.active"); return [activePage?.dataset.subpage, activePage?.querySelector("h3")?.textContent.trim(), document.querySelector("label[for=removeFromQueueOnDoneInput]")?.textContent.trim(), document.getElementById("removeFromQueueOnDoneInput")?.closest(".settings-option")?.querySelector(".settings-option-description")?.textContent.trim()].join("|"); })()'); + check('Upload completion behavior is immediately findable', uploadSettingsState === 'uploads|Upload-Verhalten|Nach Abschluss aus der Liste entfernen|Erfolgreich hochgeladene Dateien verschwinden automatisch aus der Upload-Liste.'); + + if (process.env.MHU_SETTINGS_SCREENSHOT) { + const screenshotPage = process.env.MHU_SETTINGS_SCREENSHOT_PAGE; + if (screenshotPage) await wc.executeJavaScript('document.querySelector("[data-settings-page=" + ' + JSON.stringify(screenshotPage) + ' + "]")?.click()'); + await new Promise(resolve => setTimeout(resolve, 150)); + const screenshot = await wc.capturePage(); + fs.writeFileSync(process.env.MHU_SETTINGS_SCREENSHOT, screenshot.toPNG()); + } + + const settingsSearchState = await wc.executeJavaScript('(() => { const search = document.getElementById("settingsSearchInput"); if (!search) return "missing"; search.value = "fertig"; search.dispatchEvent(new Event("input", { bubbles: true })); const visible = [...document.querySelectorAll(".settings-nav-button")].filter(button => !button.hidden); return [visible.map(button => button.dataset.settingsPage).join(","), document.querySelector(".settings-nav-button.active")?.dataset.settingsPage].join("|"); })()'); + check('Settings search routes completion terms to Uploads first', settingsSearchState === 'uploads,benachrichtigungen|uploads'); + + const settingsSearchRecovery = await wc.executeJavaScript('(() => { const search = document.getElementById("settingsSearchInput"); search.value = "kein-passender-treffer"; search.dispatchEvent(new Event("input", { bubbles: true })); const emptyVisible = !document.getElementById("settingsSearchEmpty").hidden; search.value = ""; search.dispatchEvent(new Event("input", { bubbles: true })); return [emptyVisible, document.querySelector(".settings-subpage.active")?.dataset.subpage, document.getElementById("settingsSearchEmpty").hidden].join("|"); })()'); + check('Clearing an empty settings search restores the current page', settingsSearchRecovery === 'true|uploads|true'); + + const filteredOnlineRestoreNavigation = await wc.executeJavaScript('(() => { const search = document.getElementById("settingsSearchInput"); search.value = "fertig"; search.dispatchEvent(new Event("input", { bubbles: true })); _handleMenuAction("online-backup-restore"); return [search.value, document.querySelector(".settings-nav-button.active")?.dataset.settingsPage, document.activeElement?.id].join("|"); })()'); + check('Online restore navigation clears filters and opens Backup', filteredOnlineRestoreNavigation === '|backup|onlineBackupKeyInput'); const accountSettingsPointer = await wc.executeJavaScript('document.querySelector(".settings-hoster-pointer")?.textContent'); check('Hoster settings point to Accounts tab', accountSettingsPointer && accountSettingsPointer.includes('Accounts')); @@ -222,7 +244,7 @@ setTimeout(async () => { const parallel = await wc.executeJavaScript('document.getElementById("parallelUploadCountInput")?.value'); check('Global parallel uploads default 0', parallel === '0'); - await wc.executeJavaScript('document.querySelector("[data-subtab=\\'backup\\']").click()'); + await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'backup\\']")?.click()'); const onlineBackupControls = await wc.executeJavaScript('["createOnlineBackupBtn", "onlineBackupKeyOutput", "copyOnlineBackupKeyBtn", "onlineBackupKeyInput", "restoreOnlineBackupBtn", "onlineBackupStatus"].every(id => Boolean(document.getElementById(id)))'); check('Online backup controls exist', onlineBackupControls); @@ -238,13 +260,17 @@ setTimeout(async () => { const validOnlineBackup = await wc.executeJavaScript('document.getElementById("onlineBackupKeyInput").value = "MHU2-" + "A".repeat(70); document.getElementById("onlineBackupKeyInput").dispatchEvent(new Event("input", { bubbles: true })); document.getElementById("restoreOnlineBackupBtn").disabled'); check('Valid 75-character online backup keys enable restore', validOnlineBackup === false); - const onlineRestoreNavigation = await wc.executeJavaScript('_handleMenuAction("online-backup-restore"); document.activeElement?.id + "|" + document.querySelector(".settings-subtab.active")?.dataset.subtab'); + const onlineRestoreNavigation = await wc.executeJavaScript('_handleMenuAction("online-backup-restore"); document.activeElement?.id + "|" + document.querySelector(".settings-nav-button.active")?.dataset.settingsPage'); check('Online restore menu opens the backup page and focuses the key', onlineRestoreNavigation === 'onlineBackupKeyInput|backup'); // Test save await wc.executeJavaScript('document.getElementById("saveSettingsBtn").click()'); - await new Promise(r => setTimeout(r, 500)); - const feedback = await wc.executeJavaScript('document.getElementById("saveFeedback")?.textContent'); + let feedback = ''; + for (let attempt = 0; attempt < 50; attempt++) { + feedback = await wc.executeJavaScript('document.getElementById("saveFeedback")?.textContent'); + if (feedback === 'Gespeichert!') break; + await new Promise(r => setTimeout(r, 50)); + } check('Save shows Gespeichert!', feedback === 'Gespeichert!'); console.log('\\n=== History View ===');