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
+13 -14
View File
@@ -34,7 +34,7 @@ export default function LibraryPlayer({
}) {
const { t } = useTranslation();
const qc = useQueryClient();
const fileUrl = useMemo(() => api.downloadFileUrl(job.id), [job.id]);
const fileUrl = useMemo(() => api.fileUrl(job.id), [job.id]);
// Video vs audio-only comes from the stored media_kind (the single source every sibling reads —
// WatchPage/downloadRow/DownloadCenter) — NOT a re-derived container list, which would misclassify
// an uploaded audio file whose container is outside a hardcoded set (uploads accept any container).
@@ -68,23 +68,22 @@ export default function LibraryPlayer({
artwork: poster ?? null,
});
useMediaKeys(media);
// The library row's bar follows the point at once — both listings this job may sit in — rather
// than after the next refetch. On the way out a listing poll may already be out, having read the
// server before the closing point lands: it is cancelled, or its answer replaced the row's point
// with the older one, Continue opened there, and its first checkpoint wrote it back over the right
// one. Once the point is on the server, the listings are read again.
const lists = [qk.downloads(), qk.downloadsShared()];
// The library row's bar follows the point at once — every library listing this file may sit in
// (each folder, search and the shared view is its own key under `files`) — rather than after the
// next refetch. On the way out a listing read may already be out, having read the server before
// the closing point lands: it is cancelled, or its answer replaced the row's point with the older
// one, Continue opened there, and its first checkpoint wrote it back over the right one. Once the
// point is on the server, the listings are read again.
const lists = { queryKey: qk.files() };
const keepPoint = (position: number, duration: number, leaving: boolean) => {
const at = keptPosition(position, duration);
for (const key of lists) {
if (leaving) void qc.cancelQueries({ queryKey: key, exact: true });
qc.setQueryData<DownloadJob[]>(key, (jobs) =>
jobs?.map((j) => (j.id === job.id ? { ...j, position_seconds: at } : j)),
);
}
if (leaving) void qc.cancelQueries(lists);
qc.setQueriesData<DownloadJob[]>(lists, (jobs) =>
jobs?.map((j) => (j.id === job.id ? { ...j, position_seconds: at } : j)),
);
if (leaving) {
api.saveDownloadProgressBeacon(job.id, position, duration, () => {
for (const key of lists) void qc.invalidateQueries({ queryKey: key, exact: true });
void qc.invalidateQueries(lists);
});
} else void api.saveDownloadProgress(job.id, position, duration).catch(() => {});
};