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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user