From e5d7917cf3a9de711165d2eb8dd97b4e47e2b0b3 Mon Sep 17 00:00:00 2001 From: Administrator Date: Mon, 15 Jun 2026 00:45:39 +0200 Subject: [PATCH] feat(ui): downloader-style top menu bar (Datei / Einstellungen / Hilfe) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a custom HTML menu bar above the tab bar, replicating the Real-Debrid Downloader's look: click-to-open triggers, hover-switch between open menus, click-outside / Escape to close, right-flyout submenus. - Datei: Dateien hinzufügen, Ordner hinzufügen, Sicherung submenu (Export/Import backup), Neustart, Beenden. - Einstellungen: "Einstellungen öffnen" (jumps to the settings tab) plus an inline live grid with the max-parallel-uploads spinner and a speed-limit checkbox + MB/s spinner, both two-way-synced with the Settings tab fields via saveGlobalSettings (parallelUploadCount / globalMaxSpeedKbs). - Hilfe: Log-Ordner öffnen, Diagnose-Paket exportieren, Suche Aktualisierungen. Wiring reuses existing handlers (addFiles/addFolder buttons, doBackupExport/ Import, openLogFolder, createSupportBundle, checkForUpdate) and two new tiny IPC handlers app:restart (app.relaunch+quit) / app:quit. initMenuBar() is wrapped in try/catch in setupListeners so a menu fault can never break core app init. CSS maps the downloader's menu styles onto this app's theme vars. Co-Authored-By: Claude Opus 4.8 (1M context) --- main.js | 9 +++ preload.js | 2 + renderer/app.js | 164 ++++++++++++++++++++++++++++++++++++++++++++ renderer/index.html | 59 ++++++++++++++++ renderer/styles.css | 159 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 393 insertions(+) diff --git a/main.js b/main.js index 4d38bf0..0d32cc0 100644 --- a/main.js +++ b/main.js @@ -2180,6 +2180,15 @@ ipcMain.handle('app:get-version', () => { return app.getVersion(); }); +ipcMain.handle('app:restart', () => { + app.relaunch(); + app.quit(); +}); + +ipcMain.handle('app:quit', () => { + app.quit(); +}); + // --- Hoster settings --- ipcMain.handle('get-hoster-settings', () => { const config = configStore.load(); diff --git a/preload.js b/preload.js index 1aed2c7..398acba 100644 --- a/preload.js +++ b/preload.js @@ -56,6 +56,8 @@ contextBridge.exposeInMainWorld('api', { installUpdate: () => ipcRenderer.invoke('app:install-update'), abortUpdate: () => ipcRenderer.invoke('app:abort-update'), getVersion: () => ipcRenderer.invoke('app:get-version'), + restartApp: () => ipcRenderer.invoke('app:restart'), + quitApp: () => ipcRenderer.invoke('app:quit'), onUpdateAvailable: (callback) => { ipcRenderer.on('app:update-available', (_event, data) => callback(data)); }, diff --git a/renderer/app.js b/renderer/app.js index 195adb8..696133a 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -291,6 +291,169 @@ function _isHistoryTabActive() { } })(); +// --- Top menu bar (Datei / Einstellungen / Hilfe) --- +function initMenuBar() { + const menuBar = document.getElementById('menuBar'); + if (!menuBar) return; + let openMenu = null; + + const dropdowns = {}; + menuBar.querySelectorAll('[data-menu-dropdown]').forEach(d => { dropdowns[d.dataset.menuDropdown] = d; }); + const triggers = {}; + menuBar.querySelectorAll('[data-menu-trigger]').forEach(t => { triggers[t.dataset.menuTrigger] = t; }); + + function closeMenus() { + openMenu = null; + for (const k in dropdowns) dropdowns[k].style.display = 'none'; + for (const k in triggers) triggers[k].classList.remove('open'); + menuBar.querySelectorAll('.menu-submenu-dropdown').forEach(s => { s.style.display = 'none'; }); + } + + function openMenuNamed(name) { + if (openMenu === name) return; + closeMenus(); + openMenu = name; + if (dropdowns[name]) dropdowns[name].style.display = ''; + if (triggers[name]) triggers[name].classList.add('open'); + if (name === 'einstellungen') _syncMenuSettings(); + } + + menuBar.querySelectorAll('[data-menu-trigger]').forEach(trigger => { + const name = trigger.dataset.menuTrigger; + trigger.addEventListener('click', (e) => { + e.stopPropagation(); + if (openMenu === name) closeMenus(); else openMenuNamed(name); + }); + trigger.addEventListener('mouseenter', () => { if (openMenu && openMenu !== name) openMenuNamed(name); }); + }); + + menuBar.querySelectorAll('.menu-submenu').forEach(sm => { + const sub = sm.querySelector('.menu-submenu-dropdown'); + sm.addEventListener('mouseenter', () => { if (sub) sub.style.display = ''; }); + sm.addEventListener('mouseleave', () => { if (sub) sub.style.display = 'none'; }); + }); + + menuBar.querySelectorAll('[data-menu-action]').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + const action = item.dataset.menuAction; + closeMenus(); + _handleMenuAction(action); + }); + }); + + document.addEventListener('mousedown', (e) => { if (openMenu && !e.target.closest('.menu-bar')) closeMenus(); }); + document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && openMenu) closeMenus(); }); + + _initMenuSettingsControls(); +} + +async function _handleMenuAction(action) { + switch (action) { + case 'add-files': document.getElementById('addFilesBtn')?.click(); break; + case 'add-folder': document.getElementById('addFolderBtn')?.click(); break; + case 'backup-export': doBackupExport(); break; + case 'backup-import': doBackupImport(); break; + case 'restart': if (confirm('Anwendung neu starten?')) window.api.restartApp(); break; + case 'quit': window.api.quitApp(); break; + case 'open-settings': document.querySelector('.tab[data-view="settings"]')?.click(); break; + case 'open-log-folder': window.api.openLogFolder(); break; + case 'support-bundle': { + showCopyToast('Diagnose-Paket wird erstellt…'); + try { + const res = await window.api.createSupportBundle(); + if (res && res.ok) showCopyToast(`Diagnose-Paket gespeichert (${(res.bytes / 1024).toFixed(1)} KB)`); + else if (res && res.canceled) showCopyToast('Abgebrochen'); + else showCopyToast(`Fehler: ${(res && res.error) || 'unbekannt'}`); + } catch (err) { showCopyToast(`Fehler: ${err.message || err}`); } + break; + } + case 'check-updates': { + showCopyToast('Suche nach Updates…'); + try { + const result = await window.api.checkForUpdate(); + if (result && result.available) { showUpdateBanner(result); showCopyToast('Update gefunden!'); } + else showCopyToast('Kein Update verfügbar'); + } catch { showCopyToast('Fehler beim Prüfen'); } + break; + } + } +} + +function _syncMenuSettings() { + const gs = config.globalSettings || {}; + const pInput = document.getElementById('menuParallelInput'); + if (pInput) pInput.value = String(gs.parallelUploadCount ?? 0); + const speedKbs = gs.globalMaxSpeedKbs || 0; + const enabled = speedKbs > 0; + const sCheck = document.getElementById('menuSpeedLimitCheck'); + const sInput = document.getElementById('menuSpeedInput'); + const sSpinner = document.getElementById('menuSpeedSpinner'); + if (sCheck) sCheck.checked = enabled; + if (sInput) sInput.value = enabled ? String(+(speedKbs / 1024).toFixed(2)) : '0'; + if (sSpinner) sSpinner.classList.toggle('disabled', !enabled); +} + +function _menuSaveParallel(v) { + const n = Math.max(0, Math.min(100, parseInt(v, 10) || 0)); + const gs = { ...(config.globalSettings || {}), parallelUploadCount: n }; + config.globalSettings = gs; + window.api.saveGlobalSettings(gs).catch(() => {}); + const mirror = document.getElementById('parallelUploadCountInput'); + if (mirror) mirror.value = String(n); + return n; +} + +function _menuSaveSpeedMbs(mbs, enabled) { + const kbs = enabled ? Math.max(0, Math.round((parseFloat(mbs) || 0) * 1024)) : 0; + const gs = { ...(config.globalSettings || {}), globalMaxSpeedKbs: kbs }; + config.globalSettings = gs; + window.api.saveGlobalSettings(gs).catch(() => {}); + const mirror = document.getElementById('globalMaxSpeedMbsInput'); + if (mirror) mirror.value = kbs > 0 ? String(+(kbs / 1024).toFixed(2)) : '0'; +} + +function _initMenuSettingsControls() { + const grid = document.getElementById('menuSettingsGrid'); + if (grid) grid.addEventListener('click', (e) => e.stopPropagation()); + + const pInput = document.getElementById('menuParallelInput'); + if (pInput) { + const save = () => { pInput.value = String(_menuSaveParallel(pInput.value)); }; + pInput.addEventListener('change', save); + pInput.addEventListener('blur', save); + } + + const sInput = document.getElementById('menuSpeedInput'); + const sCheck = document.getElementById('menuSpeedLimitCheck'); + const sSpinner = document.getElementById('menuSpeedSpinner'); + if (sCheck) sCheck.addEventListener('change', () => { + if (sSpinner) sSpinner.classList.toggle('disabled', !sCheck.checked); + _menuSaveSpeedMbs(sInput ? sInput.value : 0, sCheck.checked); + }); + if (sInput) { + const save = () => _menuSaveSpeedMbs(sInput.value, sCheck ? sCheck.checked : true); + sInput.addEventListener('change', save); + sInput.addEventListener('blur', save); + } + + document.querySelectorAll('#menuSettingsGrid [data-spin]').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const kind = btn.dataset.spin; + if (kind === 'parallel-up' || kind === 'parallel-down') { + const cur = parseInt(pInput.value, 10) || 0; + pInput.value = String(_menuSaveParallel(cur + (kind === 'parallel-up' ? 1 : -1))); + } else if (kind === 'speed-up' || kind === 'speed-down') { + const cur = parseFloat(sInput.value) || 0; + const next = Math.max(0, cur + (kind === 'speed-up' ? 1 : -1)); + sInput.value = String(next); + _menuSaveSpeedMbs(next, sCheck ? sCheck.checked : true); + } + }); + }); +} + // --- Hoster selection --- function accountHasCreds(name, account) { if (!account) return false; @@ -4464,6 +4627,7 @@ window.addEventListener('beforeunload', () => { // --- Setup Listeners --- function setupListeners() { + try { initMenuBar(); } catch (err) { console.error('menu bar init failed', err); } document.getElementById('addFilesBtn').addEventListener('click', pickFiles); document.getElementById('addFolderBtn').addEventListener('click', pickFolder); document.getElementById('startUploadBtn').addEventListener('click', startUpload); diff --git a/renderer/index.html b/renderer/index.html index baee95c..e824b25 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -7,6 +7,65 @@ + +