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
+55
View File
@@ -0,0 +1,55 @@
import { useCallback, useMemo, useState, type ReactNode } from "react";
import { LS, getAccountRaw, setAccountRaw } from "../lib/storage";
import { useUrlMirror } from "../lib/useUrlMirror";
import { useNavigation } from "./navigationContext";
import {
ALL_FILES,
FilesActionsContext,
FilesStateContext,
parseView,
viewParam,
type FilesActions,
type FilesState,
type FilesView,
} from "./filesContext";
// Owns the Files module's shared state — the header search term and the view the rail picked (every
// file, shared with me, or a folder) — so the header's search box, the FilesRail and the Files page
// read it via hooks. Shaped like PlaylistsProvider: split State + Actions contexts, composed in
// main.tsx around <App/>.
//
// The search is ephemeral (resets on reload, like the other list filters); the view is remembered per
// account and mirrored to `?folder=` on the Files page, so a reload or a bookmark opens the same folder.
// A `?folder=` in the address wins over the remembered one. A folder that no longer exists is the
// page's to fall back from (it knows the folder list).
function initialView(): FilesView {
return (
parseView(new URLSearchParams(window.location.search).get("folder")) ??
parseView(getAccountRaw(LS.filesView)) ??
ALL_FILES
);
}
export function FilesProvider({ children }: { children: ReactNode }) {
const [search, setSearchState] = useState("");
const [view, setViewState] = useState<FilesView>(initialView);
const setSearch = useCallback((q: string) => setSearchState(q), []);
const setView = useCallback((next: FilesView) => {
setViewState(next);
setAccountRaw(LS.filesView, viewParam(next));
}, []);
const { page } = useNavigation();
useUrlMirror({ folder: page === "files" ? viewParam(view) : null });
const state = useMemo<FilesState>(() => ({ search, view }), [search, view]);
const actions = useMemo<FilesActions>(() => ({ setSearch, setView }), [setSearch, setView]);
return (
<FilesActionsContext.Provider value={actions}>
<FilesStateContext.Provider value={state}>{children}</FilesStateContext.Provider>
</FilesActionsContext.Provider>
);
}