Add progress sorting, extraction priority by packageOrder, auto-expand extracting packages
- Fortschritt column is now clickable/sortable (ascending/descending by package %) - Extraction queue respects packageOrder: top packages get extracted first - Packages currently extracting are auto-expanded so user can see progress - Increased Fortschritt column width for better spacing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d23740eac7
commit
be7a8fd103
@ -775,7 +775,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
private packagePostProcessActive = 0;
|
private packagePostProcessActive = 0;
|
||||||
|
|
||||||
private packagePostProcessWaiters: Array<() => void> = [];
|
private packagePostProcessWaiters: Array<{ packageId: string; resolve: () => void }> = [];
|
||||||
|
|
||||||
private packagePostProcessTasks = new Map<string, Promise<void>>();
|
private packagePostProcessTasks = new Map<string, Promise<void>>();
|
||||||
|
|
||||||
@ -1175,7 +1175,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.hybridExtractRequeue.clear();
|
this.hybridExtractRequeue.clear();
|
||||||
this.packagePostProcessQueue = Promise.resolve();
|
this.packagePostProcessQueue = Promise.resolve();
|
||||||
this.packagePostProcessActive = 0;
|
this.packagePostProcessActive = 0;
|
||||||
for (const waiter of this.packagePostProcessWaiters) { waiter(); }
|
for (const waiter of this.packagePostProcessWaiters) { waiter.resolve(); }
|
||||||
this.packagePostProcessWaiters = [];
|
this.packagePostProcessWaiters = [];
|
||||||
this.summary = null;
|
this.summary = null;
|
||||||
this.nonResumableActive = 0;
|
this.nonResumableActive = 0;
|
||||||
@ -2996,24 +2996,36 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async acquirePostProcessSlot(): Promise<void> {
|
private async acquirePostProcessSlot(packageId: string): Promise<void> {
|
||||||
const maxConcurrent = this.settings.maxParallelExtract || 2;
|
const maxConcurrent = this.settings.maxParallelExtract || 2;
|
||||||
if (this.packagePostProcessActive < maxConcurrent) {
|
if (this.packagePostProcessActive < maxConcurrent) {
|
||||||
this.packagePostProcessActive += 1;
|
this.packagePostProcessActive += 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
this.packagePostProcessWaiters.push(resolve);
|
this.packagePostProcessWaiters.push({ packageId, resolve });
|
||||||
});
|
});
|
||||||
this.packagePostProcessActive += 1;
|
this.packagePostProcessActive += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private releasePostProcessSlot(): void {
|
private releasePostProcessSlot(): void {
|
||||||
this.packagePostProcessActive -= 1;
|
this.packagePostProcessActive -= 1;
|
||||||
const next = this.packagePostProcessWaiters.shift();
|
if (this.packagePostProcessWaiters.length === 0) return;
|
||||||
if (next) {
|
// Pick the waiter whose package appears earliest in packageOrder
|
||||||
next();
|
const order = this.session.packageOrder;
|
||||||
|
let bestIdx = 0;
|
||||||
|
let bestOrder = order.indexOf(this.packagePostProcessWaiters[0].packageId);
|
||||||
|
if (bestOrder === -1) bestOrder = Infinity;
|
||||||
|
for (let i = 1; i < this.packagePostProcessWaiters.length; i++) {
|
||||||
|
let pos = order.indexOf(this.packagePostProcessWaiters[i].packageId);
|
||||||
|
if (pos === -1) pos = Infinity;
|
||||||
|
if (pos < bestOrder) {
|
||||||
|
bestOrder = pos;
|
||||||
|
bestIdx = i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
const [next] = this.packagePostProcessWaiters.splice(bestIdx, 1);
|
||||||
|
next.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
private runPackagePostProcessing(packageId: string): Promise<void> {
|
private runPackagePostProcessing(packageId: string): Promise<void> {
|
||||||
@ -3027,7 +3039,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.packagePostProcessAbortControllers.set(packageId, abortController);
|
this.packagePostProcessAbortControllers.set(packageId, abortController);
|
||||||
|
|
||||||
const task = (async () => {
|
const task = (async () => {
|
||||||
await this.acquirePostProcessSlot();
|
await this.acquirePostProcessSlot(packageId);
|
||||||
try {
|
try {
|
||||||
await this.handlePackagePostProcessing(packageId, abortController.signal);
|
await this.handlePackagePostProcessing(packageId, abortController.signal);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -374,7 +374,33 @@ function sortPackageOrderByHoster(order: string[], packages: Record<string, Pack
|
|||||||
return sorted;
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PkgSortColumn = "name" | "size" | "hoster";
|
function sortPackageOrderByProgress(order: string[], packages: Record<string, PackageEntry>, items: Record<string, DownloadItem>, descending: boolean): string[] {
|
||||||
|
const sorted = [...order];
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
const progressA = computePackageProgress(packages[a], items);
|
||||||
|
const progressB = computePackageProgress(packages[b], items);
|
||||||
|
const cmp = progressA - progressB;
|
||||||
|
return descending ? -cmp : cmp;
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computePackageProgress(pkg: PackageEntry | undefined, items: Record<string, DownloadItem>): number {
|
||||||
|
if (!pkg) return 0;
|
||||||
|
const ids = pkg.itemIds ?? [];
|
||||||
|
if (ids.length === 0) return 0;
|
||||||
|
let totalDown = 0;
|
||||||
|
let totalSize = 0;
|
||||||
|
for (const id of ids) {
|
||||||
|
const item = items[id];
|
||||||
|
if (!item) continue;
|
||||||
|
totalDown += item.downloadedBytes || 0;
|
||||||
|
totalSize += item.totalBytes || item.downloadedBytes || 0;
|
||||||
|
}
|
||||||
|
return totalSize > 0 ? totalDown / totalSize : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PkgSortColumn = "name" | "size" | "hoster" | "progress";
|
||||||
|
|
||||||
function sameStringArray(a: string[], b: string[]): boolean {
|
function sameStringArray(a: string[], b: string[]): boolean {
|
||||||
if (a.length !== b.length) {
|
if (a.length !== b.length) {
|
||||||
@ -808,6 +834,25 @@ export function App(): ReactElement {
|
|||||||
}
|
}
|
||||||
}, [snapshot.session.running]);
|
}, [snapshot.session.running]);
|
||||||
|
|
||||||
|
// Auto-expand packages that are currently extracting
|
||||||
|
useEffect(() => {
|
||||||
|
const extractingPkgIds: string[] = [];
|
||||||
|
for (const pkg of packages) {
|
||||||
|
if (collapsedPackages[pkg.id]) {
|
||||||
|
const items = (pkg.itemIds ?? []).map((id) => snapshot.session.items[id]).filter(Boolean);
|
||||||
|
const isExtracting = items.some((item) => item.fullStatus?.startsWith("Entpacken -") && !item.fullStatus?.includes("Done"));
|
||||||
|
if (isExtracting) extractingPkgIds.push(pkg.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (extractingPkgIds.length > 0) {
|
||||||
|
setCollapsedPackages((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
for (const id of extractingPkgIds) next[id] = false;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [packages, snapshot.session.items]);
|
||||||
|
|
||||||
const allPackagesCollapsed = useMemo(() => (
|
const allPackagesCollapsed = useMemo(() => (
|
||||||
packages.length > 0 && packages.every((pkg) => collapsedPackages[pkg.id])
|
packages.length > 0 && packages.every((pkg) => collapsedPackages[pkg.id])
|
||||||
), [packages, collapsedPackages]);
|
), [packages, collapsedPackages]);
|
||||||
@ -2105,13 +2150,10 @@ export function App(): ReactElement {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="pkg-column-header">
|
<div className="pkg-column-header">
|
||||||
{(["name", "size", "hoster"] as PkgSortColumn[]).flatMap((col) => {
|
{(["name", "progress", "size", "hoster"] as PkgSortColumn[]).flatMap((col) => {
|
||||||
const labels: Record<PkgSortColumn, string> = { name: "Name", size: "Geladen / Größe", hoster: "Hoster" };
|
const labels: Record<PkgSortColumn, string> = { name: "Name", progress: "Fortschritt", size: "Geladen / Größe", hoster: "Hoster" };
|
||||||
const isActive = downloadsSortColumn === col;
|
const isActive = downloadsSortColumn === col;
|
||||||
const before: React.ReactNode[] = [];
|
|
||||||
if (col === "size") before.push(<span key="progress" className="pkg-col pkg-col-progress">Fortschritt</span>);
|
|
||||||
return [
|
return [
|
||||||
...before,
|
|
||||||
<span
|
<span
|
||||||
key={col}
|
key={col}
|
||||||
className={`pkg-col pkg-col-${col} sortable${isActive ? " sort-active" : ""}`}
|
className={`pkg-col pkg-col-${col} sortable${isActive ? " sort-active" : ""}`}
|
||||||
@ -2121,7 +2163,9 @@ export function App(): ReactElement {
|
|||||||
setDownloadsSortDescending(nextDesc);
|
setDownloadsSortDescending(nextDesc);
|
||||||
const baseOrder = packageOrderRef.current.length > 0 ? packageOrderRef.current : snapshot.session.packageOrder;
|
const baseOrder = packageOrderRef.current.length > 0 ? packageOrderRef.current : snapshot.session.packageOrder;
|
||||||
let sorted: string[];
|
let sorted: string[];
|
||||||
if (col === "size") {
|
if (col === "progress") {
|
||||||
|
sorted = sortPackageOrderByProgress(baseOrder, snapshot.session.packages, snapshot.session.items, nextDesc);
|
||||||
|
} else if (col === "size") {
|
||||||
sorted = sortPackageOrderBySize(baseOrder, snapshot.session.packages, snapshot.session.items, nextDesc);
|
sorted = sortPackageOrderBySize(baseOrder, snapshot.session.packages, snapshot.session.items, nextDesc);
|
||||||
} else if (col === "hoster") {
|
} else if (col === "hoster") {
|
||||||
sorted = sortPackageOrderByHoster(baseOrder, snapshot.session.packages, snapshot.session.items, nextDesc);
|
sorted = sortPackageOrderByHoster(baseOrder, snapshot.session.packages, snapshot.session.items, nextDesc);
|
||||||
|
|||||||
@ -577,7 +577,7 @@ body,
|
|||||||
|
|
||||||
.pkg-column-header {
|
.pkg-column-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 70px 160px 220px 180px 100px;
|
grid-template-columns: 1fr 90px 160px 220px 180px 100px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 5px 12px;
|
padding: 5px 12px;
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
@ -603,7 +603,7 @@ body,
|
|||||||
|
|
||||||
.pkg-columns {
|
.pkg-columns {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 70px 160px 220px 180px 100px;
|
grid-template-columns: 1fr 90px 160px 220px 180px 100px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@ -1272,7 +1272,7 @@ td {
|
|||||||
|
|
||||||
.item-row {
|
.item-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 70px 160px 220px 180px 100px;
|
grid-template-columns: 1fr 90px 160px 220px 180px 100px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0 -12px;
|
margin: 0 -12px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user