feat(ui): downloader-style top menu bar (Datei / Einstellungen / Hilfe)

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) <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-06-15 00:45:39 +02:00
parent cafe777377
commit e5d7917cf3
5 changed files with 393 additions and 0 deletions

View File

@ -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();

View File

@ -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));
},

View File

@ -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);

View File

@ -7,6 +7,65 @@
<link rel="stylesheet" href="styles.css">
</head>
<body>
<nav class="menu-bar" id="menuBar">
<div class="menu-bar-item" data-menu="datei">
<button class="menu-bar-trigger" data-menu-trigger="datei">Datei</button>
<div class="menu-dropdown" data-menu-dropdown="datei" style="display:none">
<button class="menu-dropdown-item" data-menu-action="add-files"><span>Dateien hinzufügen</span></button>
<button class="menu-dropdown-item" data-menu-action="add-folder"><span>Ordner hinzufügen</span></button>
<div class="menu-separator"></div>
<div class="menu-submenu" data-submenu="sicherung">
<button class="menu-submenu-trigger">Sicherung</button>
<div class="menu-submenu-dropdown" 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>
</div>
</div>
<div class="menu-separator"></div>
<button class="menu-dropdown-item" data-menu-action="restart"><span>Neustart</span></button>
<button class="menu-dropdown-item" data-menu-action="quit"><span>Beenden</span></button>
</div>
</div>
<div class="menu-bar-item" data-menu="einstellungen">
<button class="menu-bar-trigger" data-menu-trigger="einstellungen">Einstellungen</button>
<div class="menu-dropdown" data-menu-dropdown="einstellungen" style="display:none">
<button class="menu-dropdown-item" data-menu-action="open-settings"><span>Einstellungen öffnen</span></button>
<div class="menu-separator"></div>
<div class="menu-settings-grid" id="menuSettingsGrid">
<span>Max. parallele Uploads</span>
<span></span>
<div class="menu-spinner">
<input type="text" inputmode="numeric" id="menuParallelInput">
<div class="menu-spinner-arrows">
<button data-spin="parallel-up">&#9650;</button>
<button data-spin="parallel-down">&#9660;</button>
</div>
</div>
<span></span>
<span>Geschwindigkeitslimit</span>
<input type="checkbox" id="menuSpeedLimitCheck">
<div class="menu-spinner" id="menuSpeedSpinner">
<input type="text" inputmode="decimal" id="menuSpeedInput">
<div class="menu-spinner-arrows">
<button data-spin="speed-up">&#9650;</button>
<button data-spin="speed-down">&#9660;</button>
</div>
</div>
<span class="menu-speed-unit">MB/s</span>
</div>
</div>
</div>
<div class="menu-bar-item" data-menu="hilfe">
<button class="menu-bar-trigger" data-menu-trigger="hilfe">Hilfe</button>
<div class="menu-dropdown" data-menu-dropdown="hilfe" style="display:none">
<button class="menu-dropdown-item" data-menu-action="open-log-folder"><span>Log-Ordner öffnen</span></button>
<button class="menu-dropdown-item" data-menu-action="support-bundle"><span>Diagnose-Paket exportieren</span></button>
<div class="menu-separator"></div>
<button class="menu-dropdown-item" data-menu-action="check-updates"><span>Suche Aktualisierungen</span></button>
</div>
</div>
</nav>
<nav class="tab-bar">
<button class="tab active" data-view="upload">Upload</button>
<button class="tab" data-view="accounts">Accounts</button>

View File

@ -34,6 +34,165 @@ body {
flex-direction: column;
}
/* Menu Bar (Datei / Einstellungen / Hilfe) */
.menu-bar {
display: flex;
align-items: center;
gap: 0;
user-select: none;
font-size: 13px;
font-weight: 600;
padding: 2px 10px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0.06));
border-bottom: 1px solid var(--border);
flex-shrink: 0;
position: relative;
z-index: 200;
}
.menu-bar-item { position: relative; }
.menu-bar-trigger {
background: none;
border: none;
color: var(--text);
padding: 5px 12px;
cursor: pointer;
font: inherit;
font-weight: 600;
border-radius: 6px;
transition: background 0.1s;
}
.menu-bar-trigger:hover,
.menu-bar-trigger.open { background: var(--bg-card-hover); }
.menu-dropdown {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
min-width: max-content;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 4px 0;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
white-space: nowrap;
}
.menu-dropdown-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
background: none;
border: none;
color: var(--text);
padding: 7px 16px;
cursor: pointer;
font: inherit;
font-size: 13px;
font-weight: 500;
text-align: left;
transition: background 0.1s;
}
.menu-dropdown-item:hover { background: var(--bg-card-hover); }
.menu-dropdown-item .shortcut {
color: var(--text-dim);
font-size: 12px;
margin-left: 24px;
white-space: nowrap;
}
.menu-separator { height: 1px; background: var(--border); margin: 4px 8px; }
.menu-submenu { position: relative; }
.menu-submenu-trigger {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
background: none;
border: none;
color: var(--text);
padding: 7px 16px;
cursor: pointer;
font: inherit;
font-size: 13px;
font-weight: 500;
text-align: left;
transition: background 0.1s;
}
.menu-submenu-trigger:hover { background: var(--bg-card-hover); }
.menu-submenu-trigger::after {
content: "\25B6";
font-size: 8px;
color: var(--text-dim);
margin-left: 12px;
}
.menu-submenu-dropdown {
position: absolute;
top: -4px;
left: 100%;
z-index: 1001;
min-width: max-content;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 4px 0;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.menu-settings-grid {
display: grid;
grid-template-columns: 1fr auto auto auto;
align-items: center;
gap: 6px 8px;
padding: 6px 16px;
font-size: 13px;
font-weight: 500;
color: var(--text);
}
.menu-spinner {
display: flex;
align-items: stretch;
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
height: 28px;
}
.menu-spinner.disabled { opacity: 0.45; pointer-events: none; }
.menu-spinner input[type="text"] {
width: 46px;
background: var(--bg-input);
color: var(--text);
border: none;
padding: 0 6px;
font: inherit;
font-size: 13px;
text-align: center;
outline: none;
}
.menu-spinner-arrows { display: flex; flex-direction: column; border-left: 1px solid var(--border); }
.menu-spinner-arrows button {
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
border: none;
color: var(--text-dim);
cursor: pointer;
padding: 0;
width: 18px;
flex: 1;
font-size: 8px;
line-height: 1;
transition: background 0.1s;
}
.menu-spinner-arrows button:hover { background: var(--bg-card-hover); color: var(--text); }
.menu-spinner-arrows button:first-child { border-bottom: 1px solid var(--border); }
.menu-settings-grid input[type="checkbox"] {
width: 15px;
height: 15px;
accent-color: var(--accent);
cursor: pointer;
flex-shrink: 0;
}
.menu-speed-unit { color: var(--text-dim); font-size: 12px; white-space: nowrap; }
/* Tab Bar */
.tab-bar {
display: flex;