diff --git a/CHANGELOG.md b/CHANGELOG.md index e4e9a31..b5ee9dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to Multi-Debrid Downloader are documented in this file. +## [2.0.19] - 2026-08-11 + +### Interface fixes + +- Restored smooth sidebar selection movement in development builds when React StrictMode restarts layout effects. +- Kept the active sidebar entry visible immediately while the shared selection marker moves to its new position. +- Disabled the service filter when fewer than two concrete services are available, preventing an empty or redundant dropdown from opening. + +### Reliability and testing + +- Added regression coverage for StrictMode transition initialization and service-filter availability states. + ## [2.0.18] - 2026-08-11 ### Downloads and status handling diff --git a/package-lock.json b/package-lock.json index 2413c10..59a0acc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "2.0.18", + "version": "2.0.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "2.0.18", + "version": "2.0.19", "license": "MIT", "dependencies": { "adm-zip": "0.6.0", diff --git a/package.json b/package.json index 2679e04..6abe910 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "2.0.18", + "version": "2.0.19", "description": "Desktop downloader", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/renderer/ui/SlidingSelection.tsx b/src/renderer/ui/SlidingSelection.tsx index 436c46c..5773703 100644 --- a/src/renderer/ui/SlidingSelection.tsx +++ b/src/renderer/ui/SlidingSelection.tsx @@ -25,9 +25,22 @@ export function scheduleSelectionLayout( return requestFrame(apply); } +export function scheduleSelectionTransitions( + enabled: boolean, + frame: number, + enable: () => void, + requestFrame: (callback: FrameRequestCallback) => number = requestAnimationFrame, + cancelFrame: (frame: number) => void = cancelAnimationFrame +): number { + if (enabled) return frame; + if (frame) cancelFrame(frame); + return requestFrame(enable); +} + export function SlidingSelection({ activeKey, as = "div", axis, children, className = "", ...attributes }: SlidingSelectionProps): ReactElement { const ref = useRef(null); const hasPositionRef = useRef(false); + const transitionsEnabledRef = useRef(false); useSelectionLayoutEffect(() => { const element = ref.current; @@ -50,11 +63,12 @@ export function SlidingSelection({ activeKey, as = "div", axis, children, classN hasPositionRef.current = true; }; const sync = (): void => { - if (!hasPositionRef.current) { - transitionFrame = scheduleSelectionLayout(false, transitionFrame, applyLayout, requestAnimationFrame, cancelAnimationFrame, () => { - if (hasPositionRef.current) { - element.style.setProperty("--ui-sliding-selection-duration", "420ms"); - } + if (!hasPositionRef.current || !transitionsEnabledRef.current) { + applyLayout(); + transitionFrame = scheduleSelectionTransitions(transitionsEnabledRef.current, transitionFrame, () => { + if (!hasPositionRef.current) return; + transitionsEnabledRef.current = true; + element.style.setProperty("--ui-sliding-selection-duration", "420ms"); }); return; } diff --git a/src/renderer/views/downloads/DownloadsView.tsx b/src/renderer/views/downloads/DownloadsView.tsx index eecfb2f..fc21939 100644 --- a/src/renderer/views/downloads/DownloadsView.tsx +++ b/src/renderer/views/downloads/DownloadsView.tsx @@ -88,7 +88,7 @@ export function DownloadsSidebar({ actions, model }: { actions: DownloadsViewAct {filters.map((filter) => )} - +
diff --git a/tests/downloads-view.test.tsx b/tests/downloads-view.test.tsx index 9d4eef3..b139fc5 100644 --- a/tests/downloads-view.test.tsx +++ b/tests/downloads-view.test.tsx @@ -486,6 +486,16 @@ describe("downloads view", () => { expect(html.match(/data-sliding-selection-active="true"/g)).toHaveLength(1); }); + it("disables the service filter until more than one concrete service is available", () => { + const none = renderToStaticMarkup(); + const one = renderToStaticMarkup(); + const multiple = renderToStaticMarkup(); + + expect(none).toMatch(/]*aria-label="Service filtern"[^>]*disabled=""/); + expect(one).toMatch(/]*aria-label="Service filtern"[^>]*disabled=""/); + expect(multiple).not.toMatch(/]*aria-label="Service filtern"[^>]*disabled=""/); + }); + it("shows only the package mode while the file mode remains hidden", () => { const html = renderToStaticMarkup(); const css = fs.readFileSync(path.join(process.cwd(), "src/renderer/views/downloads/downloads.css"), "utf8"); diff --git a/tests/sliding-selection.test.ts b/tests/sliding-selection.test.ts index 4079940..db72118 100644 --- a/tests/sliding-selection.test.ts +++ b/tests/sliding-selection.test.ts @@ -1,6 +1,34 @@ import { describe, expect, it } from "vitest"; describe("sliding selection scheduling", () => { + it("re-arms transitions when a StrictMode effect cleanup cancels the first frame", async () => { + const selectionModule = await import("../src/renderer/ui/SlidingSelection"); + const schedule = Reflect.get(selectionModule, "scheduleSelectionTransitions"); + + expect(schedule).toBeTypeOf("function"); + + let transitionsEnabled = false; + let nextFrame = 0; + const pending = new Map(); + const requestFrame = (callback: FrameRequestCallback): number => { + nextFrame += 1; + pending.set(nextFrame, callback); + return nextFrame; + }; + const cancelFrame = (frame: number): void => { + pending.delete(frame); + }; + + const firstFrame = schedule(false, 0, () => { transitionsEnabled = true; }, requestFrame, cancelFrame); + cancelFrame(firstFrame); + const secondFrame = schedule(false, 0, () => { transitionsEnabled = true; }, requestFrame, cancelFrame); + + expect(transitionsEnabled).toBe(false); + expect(pending.has(firstFrame)).toBe(false); + pending.get(secondFrame)?.(0); + expect(transitionsEnabled).toBe(true); + }); + it("applies the first mounted selection before scheduling later movements", async () => { const selectionModule = await import("../src/renderer/ui/SlidingSelection"); const schedule = Reflect.get(selectionModule, "scheduleSelectionLayout");