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:
parent
cafe777377
commit
e5d7917cf3
9
main.js
9
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();
|
||||
|
||||
@ -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));
|
||||
},
|
||||
|
||||
164
renderer/app.js
164
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);
|
||||
|
||||
@ -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">▲</button>
|
||||
<button data-spin="parallel-down">▼</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">▲</button>
|
||||
<button data-spin="speed-down">▼</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>
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user