feat(files): a file that is not media opens in a viewer — an image, a PDF, text as text
- GET /api/files/{id}/preview: an allow-list on the stored extension (downloads/preview.py) picks
image / PDF / text; served inline under `CSP: sandbox; frame-ancestors 'self'`, the type from the
table; HTML and SVG arrive as plain text; a text is its first 1 MiB (X-Preview-Truncated), sent
as UTF-8 whatever it was written in (BOM, UTF-8, else cp1250); a NUL means binary: 415
- the list says which preview each file gets (asset.preview); FE/BE contract on kinds and header
- the viewer (shared dialog shell): ← / → through the list's such files in its order, Save, open in
a tab, the file's menu; one stage height for every kind; a no-preview card offers the file
- the name, the card's picture and the menu's Preview open it; an image up to 8 MB is its own
thumbnail
This commit is contained in:
@@ -44,6 +44,7 @@ import { useContextMenu } from "./ui/useContextMenu";
|
||||
const UploadDialog = lazy(() => import("./UploadDialog"));
|
||||
const ShareDialog = lazy(() => import("./ShareDialog"));
|
||||
const LibraryPlayer = lazy(() => import("./LibraryPlayer"));
|
||||
const FilePreview = lazy(() => import("./FilePreview"));
|
||||
const VideoEditor = lazy(() => import("./VideoEditor"));
|
||||
|
||||
// The Files library: every done file of mine, the ones shared with me, or one folder's (the rail
|
||||
@@ -110,12 +111,12 @@ function uploadFolder(view: FilesView, folders: readonly FileFolder[]): number |
|
||||
function NameCell({
|
||||
file,
|
||||
renaming,
|
||||
onPlay,
|
||||
onOpen,
|
||||
onRenamed,
|
||||
}: {
|
||||
file: LibraryFile;
|
||||
renaming: boolean;
|
||||
onPlay: (() => void) | null;
|
||||
onOpen: (() => void) | null;
|
||||
onRenamed: (name: string | null, byKey: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -128,11 +129,11 @@ function NameCell({
|
||||
const title = <span className="block truncate text-sm font-medium">{jobTitle(file)}</span>;
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col">
|
||||
{onPlay ? (
|
||||
{onOpen ? (
|
||||
<Pressable
|
||||
selectable
|
||||
data-nav-open=""
|
||||
onClick={onPlay}
|
||||
onClick={onOpen}
|
||||
title={jobTitle(file)}
|
||||
className="min-w-0 rounded-sm hover:text-accent"
|
||||
>
|
||||
@@ -194,10 +195,13 @@ export default function Files() {
|
||||
const [editFile, setEditFile] = useState<LibraryFile | null>(null);
|
||||
const [playing, setPlaying] = useState<{ file: LibraryFile; fromStart: boolean } | null>(null);
|
||||
const play = (file: LibraryFile, fromStart = false) => setPlaying({ file, fromStart });
|
||||
// The file open in the viewer (one that is not media), by id: the list under it keeps re-reading.
|
||||
const [viewing, setViewing] = useState<number | null>(null);
|
||||
|
||||
const actions = useFileActions({
|
||||
folders,
|
||||
onPlay: play,
|
||||
onView: (f) => setViewing(f.id),
|
||||
onEdit: setEditFile,
|
||||
onRename: (f) => setRenamingId(f.id),
|
||||
onEditDetails: setMetaFile,
|
||||
@@ -309,8 +313,11 @@ export default function Files() {
|
||||
}, []);
|
||||
|
||||
const iconSlot = <span className="size-9 shrink-0" aria-hidden />;
|
||||
const playOf = (f: LibraryFile) =>
|
||||
(f.shared ? sharedRowActions(f) : libraryRowActions(f)).play ? () => play(f) : null;
|
||||
// What a row's name or a card's picture does: plays it, or opens it in the viewer.
|
||||
const openOf = (f: LibraryFile) => {
|
||||
const a = f.shared ? sharedRowActions(f) : libraryRowActions(f);
|
||||
return a.play ? () => play(f) : a.view ? () => setViewing(f.id) : null;
|
||||
};
|
||||
const columns: Column<LibraryFile>[] = [
|
||||
{
|
||||
key: "thumb",
|
||||
@@ -338,7 +345,7 @@ export default function Files() {
|
||||
key={renamingId === f.id ? "renaming" : "shown"}
|
||||
file={f}
|
||||
renaming={renamingId === f.id}
|
||||
onPlay={playOf(f)}
|
||||
onOpen={openOf(f)}
|
||||
onRenamed={(name, byKey) => {
|
||||
if (byKey) refocusFile.current = f.id;
|
||||
setRenamingId(null);
|
||||
@@ -504,7 +511,7 @@ export default function Files() {
|
||||
key={f.id}
|
||||
file={f}
|
||||
index={i}
|
||||
onPlay={playOf(f)}
|
||||
onOpen={openOf(f)}
|
||||
menuProps={rowMenu.triggerProps(f)}
|
||||
moreProps={rowMenu.buttonProps(f)}
|
||||
menuOpen={rowMenu.open && rowMenu.target?.id === f.id}
|
||||
@@ -655,6 +662,18 @@ export default function Files() {
|
||||
/>
|
||||
)}
|
||||
{editFile && <VideoEditor job={editFile} onClose={() => setEditFile(null)} />}
|
||||
{viewing !== null && (
|
||||
<FilePreview
|
||||
// The viewer pages the list as it reads (its sort), through the files it can open.
|
||||
files={sortRows(rows, columns, sort, lang).filter(
|
||||
(f) => (f.shared ? sharedRowActions(f) : libraryRowActions(f)).view,
|
||||
)}
|
||||
fileId={viewing}
|
||||
onFileChange={setViewing}
|
||||
onClose={() => setViewing(null)}
|
||||
menuItems={(f) => actions.items(f, true)}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</div>
|
||||
{dragging && (
|
||||
|
||||
Reference in New Issue
Block a user