diff --git a/CHANGELOG.md b/CHANGELOG.md index 45daec9..2db1a25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to Multi-Debrid Downloader are documented in this file. +## [2.0.17] - 2026-08-10 + +### Interface fixes + +- Added smooth horizontal opening and closing transitions to the Backup, Logs, Remote Support, and Diagnostics submenus. +- Kept nested menus mounted during closing so their exit motion remains visible instead of disappearing immediately. +- Preserved left-side submenu placement in narrow windows without restoring horizontal overflow. +- Removed hidden submenu actions from keyboard navigation and respected reduced-motion preferences. +- Simplified Mega-Debrid API service labels by removing redundant access-mode suffixes while retaining the complete source label as a tooltip. +- Reduced download and extraction status cells to the active operation and percentage while keeping diagnostic details in tooltips. + +### Reliability and testing + +- Branded the development Electron executable with the application name, version metadata, and product icon for Windows system dialogs. +- Made development launches resilient to stale executable locks by isolating each runtime executable. +- Added regression coverage for persistent nested-menu rendering, hidden interaction states, and the shared submenu transition. +- Added a Windows regression check for development executable metadata. + ## [2.0.16] - 2026-08-10 ### Downloads and telemetry diff --git a/package-lock.json b/package-lock.json index 82421f8..befea0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "2.0.16", + "version": "2.0.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "2.0.16", + "version": "2.0.17", "license": "MIT", "dependencies": { "adm-zip": "0.6.0", diff --git a/package.json b/package.json index 1cb013b..f034d4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "2.0.16", + "version": "2.0.17", "description": "Desktop downloader", "main": "build/main/main/main.js", "author": "Sucukdeluxe", @@ -10,7 +10,7 @@ "dev:renderer": "vite --port 5180 --strictPort", "visual:dev": "vite --config tests/visual/vite.config.mts --host 127.0.0.1 --port 5174 --strictPort", "dev:main:watch": "tsup src/main/main.ts src/preload/preload.ts --out-dir build/main --format cjs --target node20 --external electron --sourcemap --watch", - "dev:electron": "wait-on tcp:5180 file:build/main/main/main.js && cross-env NODE_ENV=development DEV_SERVER_PORT=5180 electron .", + "dev:electron": "wait-on tcp:5180 file:build/main/main/main.js && cross-env NODE_ENV=development DEV_SERVER_PORT=5180 tsx scripts/run-dev-electron.ts", "build": "npm run build:main && npm run build:renderer", "build:main": "tsup src/main/main.ts src/preload/preload.ts --out-dir build/main --format cjs --target node20 --external electron --sourcemap", "build:renderer": "vite build", diff --git a/scripts/run-dev-electron.ts b/scripts/run-dev-electron.ts new file mode 100644 index 0000000..e5ee9bc --- /dev/null +++ b/scripts/run-dev-electron.ts @@ -0,0 +1,114 @@ +import { execFile, spawn } from "node:child_process"; +import { copyFile, mkdir, readFile, readdir, rename, rm } from "node:fs/promises"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { promisify } from "node:util"; + +const require = createRequire(import.meta.url); +const execFileAsync = promisify(execFile); +const productName = "Multi-Debrid-Downloader"; +const executableName = `${productName}.exe`; + +async function brandExecutable(executable: string, icon: string, version: string): Promise { + const editorName = process.arch === "x64" ? "rcedit-x64.exe" : "rcedit.exe"; + const editor = path.resolve("node_modules", "rcedit", "bin", editorName); + await execFileAsync(editor, [ + executable, + "--set-file-version", version, + "--set-product-version", version, + "--set-icon", icon, + "--set-version-string", "CompanyName", "Sucukdeluxe", + "--set-version-string", "FileDescription", productName, + "--set-version-string", "InternalName", productName, + "--set-version-string", "LegalCopyright", "Copyright © 2026 Sucukdeluxe", + "--set-version-string", "OriginalFilename", executableName, + "--set-version-string", "ProductName", productName + ]); +} + +export async function prepareDevElectron(source: string, target: string, icon: string, version: string): Promise { + await mkdir(path.dirname(target), { recursive: true }); + const temporaryTarget = `${target}.${process.pid}.tmp.exe`; + await rm(temporaryTarget, { force: true }); + await copyFile(source, temporaryTarget); + await brandExecutable(temporaryTarget, icon, version); + await rm(target, { force: true }); + await rename(temporaryTarget, target); + return target; +} + +async function readVersion(): Promise { + const packageJson = JSON.parse(await readFile(path.resolve("package.json"), "utf8")) as { version: string }; + return packageJson.version; +} + +function isProcessRunning(pid: number): boolean { + if (!Number.isInteger(pid) || pid <= 0) return false; + try { + process.kill(pid, 0); + return true; + } catch (error) { + return (error as NodeJS.ErrnoException).code === "EPERM"; + } +} + +async function removeStaleDevExecutables(directory: string, activeTarget: string): Promise { + const names = await readdir(directory).catch(() => []); + await Promise.all(names + .filter((name) => name === executableName || (name.startsWith(`${productName}-dev-`) && name.endsWith(".exe"))) + .map(async (name) => { + const candidate = path.join(directory, name); + const pid = Number(name.match(/^Multi-Debrid-Downloader-dev-(\d+)\.exe(?:\.\d+\.tmp\.exe)?$/)?.[1] || 0); + if (candidate !== activeTarget && !isProcessRunning(pid)) { + await rm(candidate, { force: true }).catch(() => undefined); + } + })); +} + +async function run(): Promise { + const args = process.argv.slice(2); + if (args[0] === "--prepare-only") { + const [, source, target, icon, version] = args; + if (!source || !target || !icon || !version) { + process.exitCode = 2; + return; + } + await prepareDevElectron(source, target, icon, version); + return; + } + if (args[0] === "--cleanup-only") { + const [, directory, activeTarget] = args; + if (!directory || !activeTarget) { + process.exitCode = 2; + return; + } + await removeStaleDevExecutables(directory, activeTarget); + return; + } + + const source = require("electron") as string; + const directory = path.dirname(source); + const target = path.join(directory, `${productName}-dev-${process.pid}.exe`); + const icon = path.resolve("assets/app_icon.ico"); + const version = await readVersion(); + await removeStaleDevExecutables(directory, target); + await prepareDevElectron(source, target, icon, version); + + const child = spawn(target, ["."], { stdio: "inherit", windowsHide: false }); + child.on("close", async (code, signal) => { + await rm(target, { force: true }).catch(() => undefined); + process.exitCode = code ?? (signal ? 1 : 0); + }); + for (const signal of ["SIGINT", "SIGTERM"] as const) { + process.on(signal, () => { + if (!child.killed) { + child.kill(signal); + } + }); + } +} + +void run().catch((error: unknown) => { + process.stderr.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`); + process.exitCode = 1; +}); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 0078be1..4d35df9 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -5679,15 +5679,17 @@ export function App(): ReactElement { onMouseLeave={() => setOpenSubmenu(null)} > - {openSubmenu === "sicherung" && ( -
+
-
- )} +
- {openSubmenu === "hilfe-log" && ( -
- - - - - -
- )} +
+ + + + + +
setOpenSubmenu(null)} > - {openSubmenu === "hilfe-remote" && ( -
+
- - -
- )} + + +
- {openSubmenu === "hilfe-diagnose" && ( -
- - -
- )} +
+ + +