diff --git a/src/renderer/views/downloads/VirtualizedDownloadsBody.tsx b/src/renderer/views/downloads/VirtualizedDownloadsBody.tsx index 747441e..041a7d9 100644 --- a/src/renderer/views/downloads/VirtualizedDownloadsBody.tsx +++ b/src/renderer/views/downloads/VirtualizedDownloadsBody.tsx @@ -5,6 +5,7 @@ import { DOWNLOAD_DISCLOSURE_DURATION_MS, mergeDownloadDisclosureRows, prepareDownloadDisclosureTransition, + scheduleDownloadDisclosureActivation, type DownloadDisclosureRow } from "./download-disclosure-transition"; import type { DownloadsViewActions, DownloadsViewModel } from "./DownloadsView"; @@ -114,7 +115,7 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D transitionRowsRef.current = prepared.rows; setTransitionRows(prepared.rows); let settleTimer = 0; - const animationFrame = window.requestAnimationFrame(() => { + const cancelActivation = scheduleDownloadDisclosureActivation(window.requestAnimationFrame.bind(window), window.cancelAnimationFrame.bind(window), () => { const active = activateDownloadDisclosureTransition(transitionRowsRef.current ?? prepared.rows); transitionRowsRef.current = active; setTransitionRows(active); @@ -125,7 +126,7 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D }, DOWNLOAD_DISCLOSURE_DURATION_MS); }); return () => { - window.cancelAnimationFrame(animationFrame); + cancelActivation(); if (settleTimer) window.clearTimeout(settleTimer); }; }, [model.disclosureRevision, model.displayMode]); diff --git a/src/renderer/views/downloads/download-disclosure-transition.ts b/src/renderer/views/downloads/download-disclosure-transition.ts index dfc107f..95db89d 100644 --- a/src/renderer/views/downloads/download-disclosure-transition.ts +++ b/src/renderer/views/downloads/download-disclosure-transition.ts @@ -4,6 +4,22 @@ import { DOWNLOAD_FILE_ROW_HEIGHT } from "./download-virtualizer"; export const DOWNLOAD_DISCLOSURE_DURATION_MS = 300; export const DOWNLOAD_DISCLOSURE_MAX_ANIMATED_ITEMS = 64; +export function scheduleDownloadDisclosureActivation( + requestFrame: (callback: FrameRequestCallback) => number, + cancelFrame: (frame: number) => void, + activate: () => void +): () => void { + let paintFrame = 0; + let activationFrame = 0; + paintFrame = requestFrame(() => { + activationFrame = requestFrame(activate); + }); + return () => { + cancelFrame(paintFrame); + if (activationFrame) cancelFrame(activationFrame); + }; +} + export type DownloadDisclosurePhase = "stable" | "entering" | "leaving"; export type DownloadDisclosureRow = DownloadLogicalRow & { diff --git a/src/renderer/views/downloads/downloads.css b/src/renderer/views/downloads/downloads.css index 749f066..56706cb 100644 --- a/src/renderer/views/downloads/downloads.css +++ b/src/renderer/views/downloads/downloads.css @@ -583,6 +583,14 @@ } @media (prefers-reduced-motion: reduce) { + .downloads-virtual-spacer { + transition-duration: 300ms !important; + } + + .downloads-virtual-row { + transition-duration: 300ms, 300ms, 300ms !important; + } + .downloads-table.is-column-drag-active [data-download-column], .downloads-table.is-column-drag-settling [data-download-column] { transition-duration: 220ms !important; diff --git a/tests/downloads-view.test.tsx b/tests/downloads-view.test.tsx index 8cdbbd7..3bd23af 100644 --- a/tests/downloads-view.test.tsx +++ b/tests/downloads-view.test.tsx @@ -23,6 +23,7 @@ import { activateDownloadDisclosureTransition, mergeDownloadDisclosureRows, prepareDownloadDisclosureTransition, + scheduleDownloadDisclosureActivation, stableDownloadDisclosureRows } from "../src/renderer/views/downloads/download-disclosure-transition"; import { @@ -224,6 +225,32 @@ describe("virtualisierte Paketanimation", () => { renderLimit: 100 })); + it("zeichnet den Startzustand in einem eigenen Frame vor der Aktivierung", () => { + const frames: FrameRequestCallback[] = []; + const cancelled: number[] = []; + const activated = vi.fn(); + let frameId = 0; + const requestFrame = vi.fn((callback: FrameRequestCallback) => { + frames.push(callback); + frameId += 1; + return frameId; + }); + const cancelFrame = vi.fn((frame: number) => cancelled.push(frame)); + + const cancel = scheduleDownloadDisclosureActivation(requestFrame, cancelFrame, activated); + + expect(activated).not.toHaveBeenCalled(); + expect(frames).toHaveLength(1); + frames.shift()?.(16); + expect(activated).not.toHaveBeenCalled(); + expect(frames).toHaveLength(1); + frames.shift()?.(32); + expect(activated).toHaveBeenCalledTimes(1); + + cancel(); + expect(cancelled).toContain(2); + }); + it("fährt neue Unterzeilen von null auf ihre feste Höhe aus und verschiebt Folgepakete unter stabilen IDs", () => { const collapsed = logicalRows([packageA.id]); const expanded = logicalRows([]); @@ -246,6 +273,13 @@ describe("virtualisierte Paketanimation", () => { ]); }); + it("behält die ausdrücklich gewünschte Paketbewegung auch bei reduzierten Systemanimationen bei", () => { + const css = fs.readFileSync(path.join(process.cwd(), "src/renderer/views/downloads/downloads.css"), "utf8"); + + expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.downloads-virtual-spacer\s*\{[^}]*transition-duration:\s*300ms !important;/s); + expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.downloads-virtual-row\s*\{[^}]*transition-duration:\s*300ms, 300ms, 300ms !important;/s); + }); + it("behält ausfahrende Unterzeilen bis zum Animationsende und reduziert ihre Höhe auf null", () => { const expanded = logicalRows([]); const collapsed = logicalRows([packageA.id]); diff --git a/tests/visual/index.html b/tests/visual/index.html index 042f4c2..c7b14fe 100644 --- a/tests/visual/index.html +++ b/tests/visual/index.html @@ -4,10 +4,13 @@ Multi Debrid Downloader Visual Harness +