feat: cycle availability sorting with part-count tie breakers

This commit is contained in:
Sucukdeluxe
2026-09-05 13:00:54 +02:00
parent 2eb4eb5769
commit dad645a6eb
8 changed files with 131 additions and 17 deletions
+16 -6
View File
@@ -39,7 +39,7 @@ import {
getProviderDailyUsageBytes,
getProviderUsageDayKey
} from "../shared/provider-daily-limits";
import { preservePackageOrderForDisplay, sortPackageOrderByAvailability, sortPackageOrderByName } from "./package-order";
import { createAvailabilitySortCycle, preservePackageOrderForDisplay, sortPackageOrderByAvailability, sortPackageOrderByName } from "./package-order";
import { createPackageOrderState } from "./package-order-state";
import { getPackagesWithOfflineLinks } from "../shared/offline-packages";
import type { OfflineSkipScope } from "../shared/types";
@@ -1894,7 +1894,8 @@ export function App(): ReactElement {
const [downloadDisplayMode, setDownloadDisplayMode] = useState<DownloadDisplayMode>("packages");
const [downloadFilter, setDownloadFilter] = useState<DownloadSidebarFilter>("all");
const [downloadProviderFilter, setDownloadProviderFilter] = useState("all");
const [downloadsSortColumn, setDownloadsSortColumn] = useState<DownloadSortColumn>("name");
const [downloadsSortColumn, setDownloadsSortColumn] = useState<DownloadSortColumn | undefined>("name");
const availabilitySortCycleRef = useRef(createAvailabilitySortCycle());
const [downloadsSortRevision, setDownloadsSortRevision] = useState(0);
const [downloadsSortDescending, setDownloadsSortDescending] = useState(false);
const [showAllPackages, setShowAllPackages] = useState(false);
@@ -5271,11 +5272,22 @@ export function App(): ReactElement {
}, [snapshot.packageSpeedBps]);
const sortDownloadsByColumn = useCallback((column: DownloadSortColumn): void => {
const baseOrder = packageOrderRef.current.length > 0 ? packageOrderRef.current : snapshot.session.packageOrder;
if (column === "availability") {
const step = availabilitySortCycleRef.current.next(baseOrder);
setDownloadsSortColumn(step.descending === null ? undefined : column);
setDownloadsSortDescending(step.descending === false);
setDownloadsSortRevision((revision) => revision + 1);
commitPackageOrder(step.descending === null ? step.order : sortPackageOrderByAvailability(
step.order, snapshot.session.packages, snapshot.session.items, step.descending
));
return;
}
availabilitySortCycleRef.current.reset();
const nextDescending = downloadsSortColumn === column ? !downloadsSortDescending : false;
setDownloadsSortColumn(column);
setDownloadsSortDescending(nextDescending);
setDownloadsSortRevision((revision) => revision + 1);
const baseOrder = packageOrderRef.current.length > 0 ? packageOrderRef.current : snapshot.session.packageOrder;
const sorted = column === "progress"
? sortPackageOrderByProgress(baseOrder, snapshot.session.packages, snapshot.session.items, nextDescending)
: column === "size"
@@ -5290,9 +5302,7 @@ export function App(): ReactElement {
nextDescending,
Object.fromEntries(downloadsViewCore.packageRows.map((row) => [row.package.id, row.items]))
)
: column === "availability"
? sortPackageOrderByAvailability(baseOrder, snapshot.session.packages, snapshot.session.items, nextDescending)
: sortPackageOrderByName(baseOrder, snapshot.session.packages, nextDescending);
: sortPackageOrderByName(baseOrder, snapshot.session.packages, nextDescending);
commitPackageOrder(sorted);
}, [commitPackageOrder, downloadsSortColumn, downloadsSortDescending, downloadsViewCore.packageRows, snapshot.session.items, snapshot.session.packageOrder, snapshot.session.packages]);
+31 -1
View File
@@ -37,7 +37,9 @@ export function sortPackageOrderByAvailability(
}));
const sorted = [...order];
sorted.sort((a, b) => {
const cmp = compareAvailabilitySummaries(summaries.get(a)!, summaries.get(b)!);
const summaryA = summaries.get(a)!;
const summaryB = summaries.get(b)!;
const cmp = compareAvailabilitySummaries(summaryA, summaryB) || summaryB.total - summaryA.total;
return descending ? -cmp : cmp;
});
return sorted;
@@ -46,3 +48,31 @@ export function sortPackageOrderByAvailability(
export function preservePackageOrderForDisplay(packages: PackageEntry[]): PackageEntry[] {
return packages;
}
export function createAvailabilitySortCycle() {
let phase: "off" | "online" | "offline" = "off";
let originalOrder: string[] = [];
return {
reset(): void {
phase = "off";
originalOrder = [];
},
next(currentOrder: string[]): { descending: boolean | null; order: string[] } {
if (phase === "off") {
originalOrder = [...currentOrder];
phase = "online";
return { descending: false, order: currentOrder };
}
if (phase === "online") {
phase = "offline";
return { descending: true, order: currentOrder };
}
phase = "off";
const available = new Set(currentOrder);
const order = originalOrder.filter((id) => available.delete(id));
order.push(...available);
originalOrder = [];
return { descending: null, order };
}
};
}
@@ -551,7 +551,7 @@ export interface DownloadsTableHeaderProps {
actions: DownloadsTableActions;
columnOrder: readonly string[];
gridTemplate: string;
sortColumn: DownloadSortColumn;
sortColumn?: DownloadSortColumn;
sortDirection: "asc" | "desc";
selectedCount: number;
visibleIds: string[];
@@ -260,7 +260,7 @@ export function DownloadsContent({ actions, model }: { actions: DownloadsViewAct
return (
<main className="downloads-content">
<div className="downloads-table" role="table" aria-label="Downloads">
<DownloadsTableHeader actions={actions} columnOrder={model.columnOrder} gridTemplate={model.gridTemplate} selectedCount={model.actionableSelectedIds.length} sortColumn={model.sortColumn ?? "name"} sortDirection={model.sortDirection ?? "asc"} visibleIds={model.visibleRowIds} />
<DownloadsTableHeader actions={actions} columnOrder={model.columnOrder} gridTemplate={model.gridTemplate} selectedCount={model.actionableSelectedIds.length} sortColumn={model.sortColumn} sortDirection={model.sortDirection ?? "asc"} visibleIds={model.visibleRowIds} />
<VirtualizedDownloadsBody actions={actions} model={model} state={state} />
</div>
</main>