feat(files): the Files page — the library in one aligned list, its folders in a rail

Files is the one library: every done file, the ones shared with me, or one
folder's, picked in a side rail (sources, own folders, the storage meter) and
mirrored to ?folder=. One table with every kind of value in its own column;
Copy link, Share and Save in sight, every action on a right click: play,
cut, rename in the row, move to an own folder or back home, keep, details,
re-download, delete.

The Downloads page keeps only the queue until the tray replaces it. The old
/api/downloads library routes are gone: list shared, upload, the details
PATCH, delete, the file and the usage now live under /api/files, and a
listed file says which source it came from. The job's "expired" flag is gone
with the state it described.
This commit is contained in:
2026-10-11 05:34:25 +02:00
parent 1a1665b8ec
commit 85aa917b38
65 changed files with 2295 additions and 1044 deletions
+35
View File
@@ -0,0 +1,35 @@
import { Film, Folder, Globe, Server, Upload, type LucideIcon } from "lucide-react";
import { YouTubeIcon } from "../lib/brandIcons";
import type { FileFolder } from "../lib/api";
// A library folder's and a file's origin, named and drawn. The backend sends a source folder's KEY
// (`youtube`, `plex`, `upload`, `hub`, `site:<host>`, see backend downloads/folders.py) and leaves
// its name to the client, in the reader's language; an own folder carries the name its user gave it.
const SITE_PREFIX = "site:";
type T = (key: string) => string;
/** The name of a source: a site is its host, the rest are words of the UI. */
export function sourceLabel(key: string, t: T): string {
if (key.startsWith(SITE_PREFIX)) return key.slice(SITE_PREFIX.length);
if (key === "youtube" || key === "plex" || key === "upload" || key === "hub")
return t(`files.source.${key}`);
return key;
}
export function sourceIcon(key: string): LucideIcon | typeof YouTubeIcon {
if (key === "youtube") return YouTubeIcon;
if (key === "plex") return Film;
if (key === "upload") return Upload;
if (key === "hub") return Server;
return Globe;
}
export function folderLabel(folder: FileFolder, t: T): string {
return folder.kind === "own" ? (folder.name ?? "") : sourceLabel(folder.source_key ?? "", t);
}
export function folderIcon(folder: FileFolder): LucideIcon | typeof YouTubeIcon {
return folder.kind === "own" ? Folder : sourceIcon(folder.source_key ?? "");
}