fix: polish download status and application menus

Animate nested application menus while keeping hidden actions out of keyboard navigation and respecting reduced-motion preferences.

Normalize Mega-Debrid service labels, simplify active download and extraction statuses, and preserve full diagnostics in tooltips.

Brand the Windows development runtime and isolate concurrent launchers with PID-aware cleanup and regression coverage.
This commit is contained in:
Sucukdeluxe
2026-08-10 23:33:34 +02:00
parent 7dce19119f
commit 9b1eeada2e
11 changed files with 338 additions and 44 deletions
+33 -25
View File
@@ -5679,15 +5679,17 @@ export function App(): ReactElement {
onMouseLeave={() => setOpenSubmenu(null)}
>
<button className="menu-submenu-trigger">Sicherung</button>
{openSubmenu === "sicherung" && (
<div className="menu-submenu-dropdown">
<div
aria-hidden={openSubmenu !== "sicherung"}
{...(openSubmenu !== "sicherung" ? { inert: "" } : {})}
className={`menu-submenu-dropdown${openSubmenu === "sicherung" ? " is-open" : ""}`}
>
<button className="menu-dropdown-item" onClick={() => { void onExportBackup(); }}>Exportieren</button>
<button className="menu-dropdown-item" onClick={() => { void onImportBackup(); }}>Importieren</button>
<div className="menu-separator" />
<button className="menu-dropdown-item" onClick={() => { void onCreateOnlineBackup(); }}>Online-Schlüssel exportieren</button>
<button className="menu-dropdown-item" onClick={onOpenOnlineBackupImport}>Online-Schlüssel importieren</button>
</div>
)}
</div>
</div>
<div className="menu-separator" />
<button className="menu-dropdown-item" onClick={onMenuRestart}>
@@ -5827,15 +5829,17 @@ export function App(): ReactElement {
onMouseLeave={() => setOpenSubmenu(null)}
>
<button className="menu-submenu-trigger">Logs öffnen</button>
{openSubmenu === "hilfe-log" && (
<div className="menu-submenu-dropdown">
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openLog().catch(() => {}); }}><span>Haupt-Log</span></button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openAuditLog().catch(() => {}); }}><span>Audit-Log</span></button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openRenameLog().catch(() => {}); }}><span>Rename-Log</span></button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openSessionLog().catch(() => {}); }}><span>Session-Log</span></button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openTraceLog().catch(() => {}); }}><span>Trace-Log</span></button>
</div>
)}
<div
aria-hidden={openSubmenu !== "hilfe-log"}
{...(openSubmenu !== "hilfe-log" ? { inert: "" } : {})}
className={`menu-submenu-dropdown${openSubmenu === "hilfe-log" ? " is-open" : ""}`}
>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openLog().catch(() => {}); }}><span>Haupt-Log</span></button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openAuditLog().catch(() => {}); }}><span>Audit-Log</span></button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openRenameLog().catch(() => {}); }}><span>Rename-Log</span></button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openSessionLog().catch(() => {}); }}><span>Session-Log</span></button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openTraceLog().catch(() => {}); }}><span>Trace-Log</span></button>
</div>
</div>
<div
className="menu-submenu"
@@ -5843,13 +5847,15 @@ export function App(): ReactElement {
onMouseLeave={() => setOpenSubmenu(null)}
>
<button className="menu-submenu-trigger">Remote-Support</button>
{openSubmenu === "hilfe-remote" && (
<div className="menu-submenu-dropdown">
<div
aria-hidden={openSubmenu !== "hilfe-remote"}
{...(openSubmenu !== "hilfe-remote" ? { inert: "" } : {})}
className={`menu-submenu-dropdown${openSubmenu === "hilfe-remote" ? " is-open" : ""}`}
>
<button className="menu-dropdown-item" onClick={() => { void onOpenRemoteDiagnostics(); }}><span>Ferndiagnose </span></button>
<button className="menu-dropdown-item" onClick={() => { void onExportSupportBundle(); }}><span>Support-Bundle exportieren</span></button>
<button className="menu-dropdown-item" onClick={() => { void onToggleSupportTrace(); }}><span>{supportTraceEnabled ? "Support-Trace deaktivieren" : "Support-Trace aktivieren"}</span></button>
</div>
)}
<button className="menu-dropdown-item" onClick={() => { void onExportSupportBundle(); }}><span>Support-Bundle exportieren</span></button>
<button className="menu-dropdown-item" onClick={() => { void onToggleSupportTrace(); }}><span>{supportTraceEnabled ? "Support-Trace deaktivieren" : "Support-Trace aktivieren"}</span></button>
</div>
</div>
<div className="menu-separator" />
<button className="menu-dropdown-item" onClick={() => { void onShowRecentErrors(); }}>
@@ -5861,12 +5867,14 @@ export function App(): ReactElement {
onMouseLeave={() => setOpenSubmenu(null)}
>
<button className="menu-submenu-trigger">Diagnose</button>
{openSubmenu === "hilfe-diagnose" && (
<div className="menu-submenu-dropdown">
<button className="menu-dropdown-item" onClick={() => { void onRunDebugSetupCheck(); }}><span>Debug-Setup prüfen</span></button>
<button className="menu-dropdown-item" onClick={() => { void onRotateDebugToken(); }}><span>Debug-Token rotieren</span></button>
</div>
)}
<div
aria-hidden={openSubmenu !== "hilfe-diagnose"}
{...(openSubmenu !== "hilfe-diagnose" ? { inert: "" } : {})}
className={`menu-submenu-dropdown${openSubmenu === "hilfe-diagnose" ? " is-open" : ""}`}
>
<button className="menu-dropdown-item" onClick={() => { void onRunDebugSetupCheck(); }}><span>Debug-Setup prüfen</span></button>
<button className="menu-dropdown-item" onClick={() => { void onRotateDebugToken(); }}><span>Debug-Token rotieren</span></button>
</div>
</div>
<div className="menu-separator" />
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void onCheckUpdates(); }}>
+1 -1
View File
@@ -30,7 +30,7 @@ export function compactProviderLabels(labels: string[]): string {
}
export function normalizeDownloadServiceLabel(label: string): string {
return [...new Set(label.split(",").map((entry) => entry.trim().replace(/\s+(Web|API)\s+\(\1 Account\)$/i, " $1")).filter(Boolean))].join(", ");
return [...new Set(label.split(",").map((entry) => entry.trim().replace(/^(Mega-Debrid)\s+(Web|API)(?:\s+\([^)]*\))?$/i, "$1 $2")).filter(Boolean))].join(", ");
}
export function compactDownloadServiceLabel(label: string): string {
+25
View File
@@ -612,6 +612,22 @@
.md-application-menu-tree .menu-submenu-dropdown {
right: 100%;
left: auto;
opacity: 0;
transform: translateX(8px);
visibility: hidden;
pointer-events: none;
transition:
opacity 160ms ease,
transform 220ms cubic-bezier(0.22, 0.76, 0.22, 1),
visibility 0s linear 220ms;
}
.md-application-menu-tree .menu-submenu-dropdown.is-open {
opacity: 1;
transform: translateX(0);
visibility: visible;
pointer-events: auto;
transition-delay: 0s;
}
.md-overlay-host {
@@ -909,4 +925,13 @@
.md-application-menu-tree .menu-dropdown.is-open {
transition-delay: 0s !important;
}
.md-application-menu-tree .menu-submenu-dropdown {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
.md-application-menu-tree .menu-submenu-dropdown.is-open {
transition-delay: 0s !important;
}
}
@@ -122,9 +122,13 @@ function progress(value: number): number {
export function compactDownloadStatus(value: string): string {
const status = value.trim();
if (/Link wird umgewandelt/i.test(status)) return "Umwandeln";
if (/Download läuft\b/i.test(status)) return "DL läuft";
const extracting = status.match(/(Entpacken\s+\d+%)/i);
return extracting?.[1] ?? status;
if (/Download läuft\b/i.test(status)) return "Download läuft";
if (/Download running\b/i.test(status)) return "Download running";
const extracting = status.match(/Entpacken\s+(\d+)%/i);
if (extracting) return `Entpacken - ${extracting[1]}%`;
const extractingEnglish = status.match(/Extracting\s+(\d+)%/i);
if (extractingEnglish) return `Extracting - ${extractingEnglish[1]}%`;
return status;
}
function DownloadMeter({ value, text }: { value: number; text: string }): ReactElement {
@@ -140,10 +144,11 @@ function DownloadMeter({ value, text }: { value: number; text: string }): ReactE
}
function DownloadStatusCell({ status, title }: { status: string; title?: string }): ReactElement {
const visibleStatus = compactDownloadStatus(status);
return (
<span aria-label={status} className="downloads-cell downloads-status-cell" title={title || status}>
<span aria-hidden="true" className="downloads-status-full">{status}</span>
<span aria-hidden="true" className="downloads-status-compact">{compactDownloadStatus(status)}</span>
<span aria-label={visibleStatus} className="downloads-cell downloads-status-cell" title={title || status}>
<span aria-hidden="true" className="downloads-status-full">{visibleStatus}</span>
<span aria-hidden="true" className="downloads-status-compact">{visibleStatus}</span>
</span>
);
}
@@ -410,8 +415,12 @@ function packageCell(row: DownloadPackageRow, column: string, packageSpeedBps: n
if (column === "prio") return <span className="downloads-cell">{entry.priority === "high" ? "Hoch" : entry.priority === "low" ? "Niedrig" : ""}</span>;
if (column === "status") {
const audio = entry.audioStripSummary ? formatAudioStripSummary(entry.audioStripSummary) : null;
const status = `${stats.done}/${stats.total}${stats.failed > 0 ? ` · ${stats.failed} Fehler` : ""}${stats.cancelled > 0 ? ` · ${stats.cancelled} abgebrochen` : ""}${entry.postProcessLabel ? ` · ${entry.postProcessLabel}` : ""}${audio ? ` · ${audio.text}` : ""}`;
const title = audio?.tooltip ? `${status}\n${audio.tooltip}` : status;
const details = `${stats.done}/${stats.total}${stats.failed > 0 ? ` · ${stats.failed} Fehler` : ""}${stats.cancelled > 0 ? ` · ${stats.cancelled} abgebrochen` : ""}${entry.postProcessLabel ? ` · ${entry.postProcessLabel}` : ""}${audio ? ` · ${audio.text}` : ""}`;
const downloading = entry.status === "downloading" || entry.status === "validating" || row.items.some((item) => item.status === "downloading" || item.status === "validating");
const status = entry.postProcessLabel && /Entpacken\s+\d+%/i.test(entry.postProcessLabel)
? entry.postProcessLabel
: downloading ? "Download läuft" : details;
const title = audio?.tooltip ? `${details}\n${audio.tooltip}` : details;
return <DownloadStatusCell status={status} title={title} />;
}
if (column === "speed") return <span className="downloads-cell">{packageSpeedBps > 0 ? formatSpeedMbps(packageSpeedBps) : ""}</span>;