* feat(web): collapse per-scan rows on the Libraries page Bulk single-file scans (e.g. autoscan picking up a season drop) rendered one full-width row per scan under the library, each with its own stop button that actually cancelled every scan for the library. With 70+ queued files the table became an unusable wall of paths. Each library's scans now collapse into one summary line (N running · N queued) with a single cancel control, compact rows for the first few scans (file basename, full path on hover, progress for running ones), and a Show all expander capped to a scrollable height. The Scan Queue popover gets the same running-first collapse per library group, shows basenames instead of full paths, and caps its staggered fade-in delay so deep rows don't appear seconds late. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): address scan-collapse review feedback - Keep the 'Entire library' target visible on pathless (full-library) scan rows in the inline active-work list. - Drop the redundant '+ N more' button in LibraryScanTasks; the header 'Show all N' toggle is the single expand control there. - Extract useCollapsedScans so the table rows and Scan Queue popover share one collapse implementation, and always derive the popover target from formatActiveScanTarget instead of a hardcoded fallback. - Update the active-scan progress test for the split mode/target markup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
97 lines
2.6 KiB
TypeScript
97 lines
2.6 KiB
TypeScript
import type { ScanRun } from "@/api/types";
|
|
|
|
export function compareActiveScans(left: ScanRun, right: ScanRun) {
|
|
const leftRank = left.status === "running" ? 0 : 1;
|
|
const rightRank = right.status === "running" ? 0 : 1;
|
|
if (leftRank !== rightRank) {
|
|
return leftRank - rightRank;
|
|
}
|
|
const modeCompare = formatActiveScanMode(left).localeCompare(formatActiveScanMode(right));
|
|
if (modeCompare !== 0) {
|
|
return modeCompare;
|
|
}
|
|
return (left.path ?? "").localeCompare(right.path ?? "");
|
|
}
|
|
|
|
export function formatActiveScanMode(scan: Pick<ScanRun, "mode">) {
|
|
switch (scan.mode) {
|
|
case "library":
|
|
return "Full library scan";
|
|
case "subtree":
|
|
return "Subtree scan";
|
|
case "file":
|
|
return "Single file scan";
|
|
default:
|
|
return scan.mode;
|
|
}
|
|
}
|
|
|
|
export function formatActiveScanTarget(scan: Pick<ScanRun, "path">) {
|
|
if (!scan.path) {
|
|
return "Entire library";
|
|
}
|
|
const segments = scan.path.replace(/\/+$/, "").split("/");
|
|
return segments[segments.length - 1] || scan.path;
|
|
}
|
|
|
|
export function formatActiveScanTrigger(trigger: string) {
|
|
switch (trigger) {
|
|
case "autoscan":
|
|
return "Autoscan";
|
|
case "library_created":
|
|
return "Created";
|
|
case "library_paths_changed":
|
|
case "library_updated":
|
|
return "Updated";
|
|
case "library_id":
|
|
case "manual":
|
|
return "Manual";
|
|
case "task:scan_libraries":
|
|
case "task_scan_libraries":
|
|
return "Scheduled";
|
|
default:
|
|
return trigger.replace(/_/g, " ");
|
|
}
|
|
}
|
|
|
|
export function formatActiveScanTime(iso: string | undefined, prefix: string) {
|
|
if (!iso) {
|
|
return prefix;
|
|
}
|
|
|
|
const date = new Date(iso);
|
|
if (Number.isNaN(date.getTime())) {
|
|
return prefix;
|
|
}
|
|
|
|
const seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));
|
|
if (seconds < 60) {
|
|
return `${prefix} just now`;
|
|
}
|
|
if (seconds < 3600) {
|
|
const minutes = Math.floor(seconds / 60);
|
|
return `${prefix} ${minutes}m ago`;
|
|
}
|
|
if (seconds < 86400) {
|
|
const hours = Math.floor(seconds / 3600);
|
|
return `${prefix} ${hours}h ago`;
|
|
}
|
|
const days = Math.floor(seconds / 86400);
|
|
return `${prefix} ${days}d ago`;
|
|
}
|
|
|
|
export function formatActiveScanProgress(scan: ScanRun) {
|
|
const result = scan.result;
|
|
if (!result) {
|
|
return "";
|
|
}
|
|
if (result.total_files && result.files_processed) {
|
|
const percent = Math.max(
|
|
0,
|
|
Math.min(100, Math.round((result.files_processed / result.total_files) * 100)),
|
|
);
|
|
return `${result.message ?? "Processing files"} · ${result.files_processed.toLocaleString()} / ${result.total_files.toLocaleString()} (${percent}%)`;
|
|
}
|
|
return result.message ?? "";
|
|
}
|