Disable row and height animations when removing downloads
This commit is contained in:
@@ -10,6 +10,7 @@ All notable changes to Multi-Debrid Downloader are documented in this file.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove queue entries and reposition the remaining rows immediately, without row or table-height animations, for both whole-package and archive-set cleanup.
|
||||
- Disable CSS row transitions as well as JavaScript animations during column sorting, so packages immediately jump to their sorted positions.
|
||||
- Keep the chosen package order stable while live snapshots and reorder confirmations arrive, preventing a brief jump back to the previous order when reversing a sort. Ignore failures from superseded reorder requests.
|
||||
|
||||
|
||||
@@ -235,6 +235,10 @@ Diese Datei hält den verifizierten technischen Arbeitsstand fest. Sie enthält
|
||||
|
||||
### Unveröffentlichte Offline-Paketbereinigung vom 4. September 2026
|
||||
|
||||
- Entfernen ohne Animation: Die Downloadansicht erkennt entfernte IDs im tatsächlichen Queue-Itembestand. CSS-Zeilen-/Höhentransitionen und laufende Reihenfolgeanimationen werden für diesen Render ausgesetzt; eventuell laufende Aufklappübergänge werden beendet. Das gilt auch für einzelne Archivsätze, deren Oberpaket erhalten bleibt. Normales Auf-/Zuklappen wird durch die bloße Sichtbarkeit einzelner Zeilen nicht als Löschung behandelt.
|
||||
- Verifiziert: 189 fokussierte Tests erfolgreich und TypeScript fehlerfrei. Browserprobe mit aktivierten Animationen unter `?motion=on&offline-cleanup=multipart&check-removal-motion` meldet für Paketmodus 12 Prüfungen/0 Bewegungen, für Archivsatzmodus 10 Prüfungen/0 Bewegungen; Folge 4 bleibt beim Entfernen von Folge 3 erhalten. Änderung per Hot-Reload geladen.
|
||||
- Release-Status: Sascha hat die Vorbereitung von v2.0.88 unterbrochen, um zuerst das animationslose Entfernen umzusetzen. Version bleibt 2.0.87; kein Release veröffentlicht. Release bleibt bis zur Fortsetzung pausiert.
|
||||
|
||||
- Aktuelle Bedienvorgabe: „Ganze Pakete“ steht im Bestätigungsdialog oben und wird bei jedem Öffnen vorausgewählt. „Nur betroffene Archivsätze“ folgt darunter. Beide Modi bleiben wählbar; der gewählte Scope wird ausdrücklich an das Backend übergeben. Browserprüfung bestätigt Reihenfolge `package`, `archive` und ausschließlich `package` als vorausgewählt; TypeScript fehlerfrei. Änderung per Hot-Reload in der Dev-App geladen.
|
||||
|
||||
- Erweiterung nach Saschas Rückmeldung: Die Bestätigung enthält jetzt zwei Radiooptionen. Standard bei jedem Öffnen ist „Nur betroffene Archivsätze“; alternativ „Ganze Pakete“. Im Archivsatz-Modus entfernt ein Offline-Part alle zusammengehörigen Parts derselben Folge, einschließlich bereits abgeschlossener Queue-Einträge; andere Folgen bleiben im Oberpaket. Leere Oberpakete verschwinden automatisch. Dateien bleiben in beiden Modi erhalten. Der Scope wird separat vom automatischen Offline-Überspringen übergeben und verändert dessen Einstellung nicht.
|
||||
|
||||
@@ -5426,6 +5426,7 @@ export function App(): ReactElement {
|
||||
sortColumn: downloadsSortColumn,
|
||||
sortDirection: downloadsSortDescending ? "desc" : "asc",
|
||||
packageOrderSortRevision: downloadsSortRevision,
|
||||
queueItems: snapshot.session.items,
|
||||
disclosureRevision: downloadDisclosureRevision,
|
||||
animationsEnabled: snapshot.settings.animatePackageDisclosure,
|
||||
status: {
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface DownloadsViewModel extends DownloadsViewModelCore {
|
||||
sortColumn?: DownloadSortColumn;
|
||||
sortDirection?: "asc" | "desc";
|
||||
packageOrderSortRevision?: number;
|
||||
queueItems?: Readonly<Record<string, unknown>>;
|
||||
disclosureRevision: number;
|
||||
animationsEnabled: boolean;
|
||||
status: DownloadsStatusModel;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
captureDownloadOrderRowTops,
|
||||
getDownloadOrderTransitionPinnedIds,
|
||||
shouldAnimateDownloadOrderChange,
|
||||
hasRemovedDownloadItems,
|
||||
getDownloadPackageOrder,
|
||||
isDownloadPackageOrderChange
|
||||
} from "./download-order-transition";
|
||||
@@ -140,8 +141,10 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D
|
||||
const packageOrderChanged = isDownloadPackageOrderChange(previousPackageOrderRef.current, packageOrder);
|
||||
const packageOrderSortRevision = model.packageOrderSortRevision ?? 0;
|
||||
const appliedSortRevisionRef = useRef(packageOrderSortRevision);
|
||||
const previousQueueItemsRef = useRef(model.queueItems);
|
||||
const itemsRemoved = hasRemovedDownloadItems(previousQueueItemsRef.current, model.queueItems);
|
||||
const orderAnimationsEnabled = shouldAnimateDownloadOrderChange({
|
||||
animationsEnabled: model.animationsEnabled,
|
||||
animationsEnabled: model.animationsEnabled && !itemsRemoved,
|
||||
sortRevision: packageOrderSortRevision,
|
||||
appliedSortRevision: appliedSortRevisionRef.current
|
||||
});
|
||||
@@ -161,7 +164,7 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D
|
||||
const prepared = prepareDownloadDisclosureTransition(
|
||||
transitionRowsRef.current ?? previousRowsRef.current,
|
||||
desiredRowsRef.current,
|
||||
model.animationsEnabled
|
||||
model.animationsEnabled && !itemsRemoved
|
||||
);
|
||||
if (!prepared.animated) {
|
||||
transitionRowsRef.current = null;
|
||||
@@ -193,9 +196,9 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D
|
||||
cancelActivation();
|
||||
if (settleTimer) window.clearTimeout(settleTimer);
|
||||
};
|
||||
}, [model.animationsEnabled, model.disclosureRevision, model.displayMode]);
|
||||
}, [itemsRemoved, model.animationsEnabled, model.disclosureRevision, model.displayMode]);
|
||||
|
||||
const renderedRows = useMemo<DownloadDisclosureRow[]>(() => transitionRows ? mergeDownloadDisclosureRows(transitionRows, desiredRows) : stableDownloadDisclosureRows(desiredRows), [desiredRows, transitionRows]);
|
||||
const renderedRows = useMemo<DownloadDisclosureRow[]>(() => transitionRows && !itemsRemoved ? mergeDownloadDisclosureRows(transitionRows, desiredRows) : stableDownloadDisclosureRows(desiredRows), [desiredRows, itemsRemoved, transitionRows]);
|
||||
const virtualWindow = useMemo(() => calculateDownloadVirtualWindow(renderedRows, {
|
||||
scrollTop: viewport.scrollTop,
|
||||
viewportHeight: viewport.viewportHeight,
|
||||
@@ -207,6 +210,7 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D
|
||||
previousPackageOrderRef.current = packageOrder;
|
||||
previousVisibleIdsRef.current = virtualWindow.rows.map((entry) => entry.id);
|
||||
appliedSortRevisionRef.current = packageOrderSortRevision;
|
||||
previousQueueItemsRef.current = model.queueItems;
|
||||
const body = bodyRef.current;
|
||||
if (!body) return;
|
||||
if (!orderAnimationsEnabled) {
|
||||
@@ -237,7 +241,7 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D
|
||||
orderTransitionTimerRef.current = 0;
|
||||
setOrderTransitionRevision((revision) => revision + 1);
|
||||
}, DOWNLOAD_ORDER_TRANSITION_DURATION_MS);
|
||||
}, [orderAnimationsEnabled, orderTransitionPinnedIds, packageOrder, packageOrderChanged, packageOrderSortRevision, virtualWindow.rows]);
|
||||
}, [model.queueItems, orderAnimationsEnabled, orderTransitionPinnedIds, packageOrder, packageOrderChanged, packageOrderSortRevision, virtualWindow.rows]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (orderTransitionTimerRef.current) window.clearTimeout(orderTransitionTimerRef.current);
|
||||
|
||||
@@ -24,6 +24,10 @@ export function shouldAnimateDownloadOrderChange(input: { animationsEnabled: boo
|
||||
return input.animationsEnabled && input.sortRevision === input.appliedSortRevision;
|
||||
}
|
||||
|
||||
export function hasRemovedDownloadItems(previous: Readonly<Record<string, unknown>> | undefined, current: Readonly<Record<string, unknown>> | undefined): boolean {
|
||||
return Boolean(previous && current && previous !== current && Object.keys(previous).some((id) => !Object.hasOwn(current, id)));
|
||||
}
|
||||
|
||||
export function getDownloadOrderTransitionPinnedIds(input: {
|
||||
enabled: boolean;
|
||||
previousOrder: readonly string[];
|
||||
|
||||
@@ -38,7 +38,8 @@ import {
|
||||
getDownloadOrderTransitionPinnedIds,
|
||||
getDownloadOrderTransformKeyframes,
|
||||
isDownloadPackageOrderChange,
|
||||
shouldAnimateDownloadOrderChange
|
||||
shouldAnimateDownloadOrderChange,
|
||||
hasRemovedDownloadItems
|
||||
} from "../src/renderer/views/downloads/download-order-transition";
|
||||
import {
|
||||
DownloadsContent,
|
||||
@@ -386,6 +387,14 @@ describe("virtualisierte Paketanimation", () => {
|
||||
expect(shouldAnimateDownloadOrderChange({ animationsEnabled: false, sortRevision: 3, appliedSortRevision: 3 })).toBe(false);
|
||||
});
|
||||
|
||||
it("detects removed queue entries even when another entry is added in the same update", () => {
|
||||
expect(hasRemovedDownloadItems({ first: {}, second: {} }, { second: {} })).toBe(true);
|
||||
expect(hasRemovedDownloadItems({ first: {}, second: {} }, { second: {}, third: {} })).toBe(true);
|
||||
expect(hasRemovedDownloadItems({ first: {} }, {})).toBe(true);
|
||||
expect(hasRemovedDownloadItems({ first: {} }, { first: { status: "completed" }, second: {} })).toBe(false);
|
||||
expect(hasRemovedDownloadItems(undefined, { first: {} })).toBe(false);
|
||||
});
|
||||
|
||||
it("pins only previously visible rows for a real priority reorder", () => {
|
||||
expect(DOWNLOAD_ORDER_TRANSITION_DURATION_MS).toBe(3000);
|
||||
expect(isDownloadPackageOrderChange(["a", "b", "c"], ["b", "c", "a"])).toBe(true);
|
||||
|
||||
@@ -205,5 +205,6 @@ export async function startVisualHarness(
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
||||
if (new URLSearchParams(window.location.search).has("check-removal-motion")) void import("./removal-motion-probe");
|
||||
startVisualHarness();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
let checks = 0;
|
||||
let movingChecks = 0;
|
||||
const observer = new MutationObserver(() => {
|
||||
const body = document.querySelector(".downloads-table-body");
|
||||
if (!body) return;
|
||||
checks++;
|
||||
const moving = [...body.querySelectorAll<HTMLElement>(".downloads-virtual-row")].some((row) => {
|
||||
const target = Number.parseFloat(row.style.getPropertyValue("--downloads-virtual-row-top"));
|
||||
return Math.abs(new DOMMatrixReadOnly(getComputedStyle(row).transform).m42 - target) > 0.5;
|
||||
});
|
||||
const spacer = body.querySelector<HTMLElement>(".downloads-virtual-spacer");
|
||||
const resizing = spacer && Math.abs(spacer.getBoundingClientRect().height - Number.parseFloat(spacer.style.getPropertyValue("--downloads-virtual-total-height"))) > 0.5;
|
||||
if (moving || resizing) movingChecks++;
|
||||
document.documentElement.dataset.removalMotion = JSON.stringify({ checks, movingChecks });
|
||||
});
|
||||
observer.observe(document.body, { subtree: true, childList: true, attributes: true, attributeFilter: ["style", "class"] });
|
||||
|
||||
export {};
|
||||
Reference in New Issue
Block a user