fix(downloads): make column reorder frames atomic

Commit the target grid immediately, derive track definitions from the same column order as the cells, and animate only cell contents with FLIP. Disable grid-track interpolation to avoid Chromium reduced-motion mix frames, honor the application animation setting, and retain serialized persistence and import authority.
This commit is contained in:
Sucukdeluxe
2026-08-20 09:16:06 +02:00
parent 32fc1e7321
commit 0dfb57525a
7 changed files with 205 additions and 67 deletions
+41 -16
View File
@@ -95,9 +95,9 @@ import {
type StatisticsViewActions
} from "./views/statistics/StatisticsView";
import { buildDownloadsViewModel, formatRemainingDownloadBytes, getDownloadQueueTotalBytes, getDownloadSpeedBps, getPendingDownloadItemCount, getRemainingDownloadBytes, type DownloadDisplayMode, type DownloadSidebarFilter } from "./views/downloads/downloads-model";
import { downloadColumnDefinitions, type DownloadSortColumn } from "./views/downloads/DownloadsTable";
import { applyDownloadGridTemplate, downloadColumnDefinitions, type DownloadSortColumn } from "./views/downloads/DownloadsTable";
import { DeleteConfirmationDialog } from "./views/downloads/DeleteConfirmationDialog";
import { beginDownloadColumnDrag, clearDownloadColumnDrag, commitDownloadColumnDrag, createDownloadColumnOrderPersistence, settleDownloadColumnDrag, updateDownloadColumnDrag, type DownloadColumnDragSession, type DownloadColumnOrderPersistence } from "./views/downloads/column-drag";
import { beginDownloadColumnDrag, clearDownloadColumnDrag, commitDownloadColumnDrag, createDownloadColumnOrderPersistence, DOWNLOAD_COLUMN_MOVE_DURATION_MS, updateDownloadColumnDrag, type DownloadColumnDragSession, type DownloadColumnOrderPersistence } from "./views/downloads/column-drag";
import {
DownloadsContent,
DownloadsFooter,
@@ -1645,11 +1645,17 @@ export function App(): ReactElement {
}
const columnDragSessionRef = useRef<DownloadColumnDragSession | null>(null);
const columnDragSettleTimerRef = useRef<number | null>(null);
const columnDragAnimationsRef = useRef<Animation[]>([]);
const suppressColumnSortRef = useRef(false);
const cancelColumnDragAnimations = useCallback((): void => {
columnDragAnimationsRef.current.forEach((animation) => animation.cancel());
columnDragAnimationsRef.current = [];
}, []);
useEffect(() => () => {
if (columnDragSettleTimerRef.current !== null) window.clearTimeout(columnDragSettleTimerRef.current);
cancelColumnDragAnimations();
if (columnDragSessionRef.current) clearDownloadColumnDrag(columnDragSessionRef.current);
}, []);
}, [cancelColumnDragAnimations]);
const [colHeaderCtx, setColHeaderCtx] = useState<{ x: number; y: number } | null>(null);
const colHeaderCtxRef = useRef<HTMLDivElement>(null);
const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([]);
@@ -4890,8 +4896,9 @@ export function App(): ReactElement {
window.clearTimeout(columnDragSettleTimerRef.current);
columnDragSettleTimerRef.current = null;
}
cancelColumnDragAnimations();
if (columnDragSessionRef.current) clearDownloadColumnDrag(columnDragSessionRef.current);
columnDragSessionRef.current = beginDownloadColumnDrag(event.currentTarget, column, event.pointerId, event.clientX);
columnDragSessionRef.current = beginDownloadColumnDrag(event.currentTarget, column, event.pointerId, event.clientX, snapshot.settings.animatePackageDisclosure);
},
onColumnPointerMove: (_column, event) => {
const session = columnDragSessionRef.current;
@@ -4901,34 +4908,52 @@ export function App(): ReactElement {
onColumnPointerUp: (_column, event) => {
const session = columnDragSessionRef.current;
if (!session || session.pointerId !== event.pointerId) return;
const next = settleDownloadColumnDrag(session, false);
if (!next) {
if (!session.active) {
clearDownloadColumnDrag(session);
columnDragSessionRef.current = null;
return;
}
const next = session.preview.order;
suppressColumnSortRef.current = true;
window.setTimeout(() => { suppressColumnSortRef.current = false; }, 0);
const changed = next.join("|") !== session.measurements.map((measurement) => measurement.id).join("|");
if (changed) persistColumnOrder(next);
const animationsEnabled = snapshot.settings.animatePackageDisclosure;
columnDragAnimationsRef.current = commitDownloadColumnDrag(session, next, (order) => {
if (changed) flushSync(() => setColumnOrder(order));
}, () => applyDownloadGridTemplate(session.root, next), animationsEnabled);
if (!animationsEnabled) {
if (columnDragSessionRef.current === session) columnDragSessionRef.current = null;
return;
}
columnDragSettleTimerRef.current = window.setTimeout(() => {
if (changed) {
persistColumnOrder(next);
commitDownloadColumnDrag(session, next, (order) => flushSync(() => setColumnOrder(order)));
} else {
clearDownloadColumnDrag(session);
}
cancelColumnDragAnimations();
session.root.classList.remove("is-column-drag-settling");
if (columnDragSessionRef.current === session) columnDragSessionRef.current = null;
columnDragSettleTimerRef.current = null;
}, 220);
}, DOWNLOAD_COLUMN_MOVE_DURATION_MS);
},
onColumnPointerCancel: (_column, event) => {
const session = columnDragSessionRef.current;
if (!session || session.pointerId !== event.pointerId) return;
settleDownloadColumnDrag(session, true);
columnDragSettleTimerRef.current = window.setTimeout(() => {
if (!session.active) {
clearDownloadColumnDrag(session);
columnDragSessionRef.current = null;
return;
}
const originalOrder = session.measurements.map((measurement) => measurement.id);
const animationsEnabled = snapshot.settings.animatePackageDisclosure;
columnDragAnimationsRef.current = commitDownloadColumnDrag(session, originalOrder, () => {}, () => {}, animationsEnabled);
if (!animationsEnabled) {
if (columnDragSessionRef.current === session) columnDragSessionRef.current = null;
return;
}
columnDragSettleTimerRef.current = window.setTimeout(() => {
cancelColumnDragAnimations();
session.root.classList.remove("is-column-drag-settling");
if (columnDragSessionRef.current === session) columnDragSessionRef.current = null;
columnDragSettleTimerRef.current = null;
}, 220);
}, DOWNLOAD_COLUMN_MOVE_DURATION_MS);
},
onColumnContextMenu: (_column, x, y) => setColHeaderCtx({ x, y })
};