Files
siftlode/frontend/src/lib/downloadActions.ts
T
peter fb8fb980e5 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
2026-10-12 15:28:31 +02:00

49 lines
1.8 KiB
TypeScript

import type { DownloadJob } from "./api";
// Which row actions apply to a completed library / shared item — gated on the stored media_kind and
// job_kind. A generic uploaded file (media_kind='file') opens in the Files viewer instead of a player,
// and has no trim and no details edit. Re-download needs a re-fetchable source, which an edit clip and an upload
// both lack (the backend 409s both). A single source for the gate the Library + Shared rows render,
// so it's unit-testable instead of buried in JSX conditionals — and the two tabs can't drift apart.
const isMediaKind = (job: DownloadJob): boolean => job.asset?.media_kind !== "file";
const hasDuration = (job: DownloadJob): boolean => (job.asset?.duration_s ?? 0) > 0;
export interface LibraryRowActions {
play: boolean;
/** Opens in the Files viewer: a file that is not media (its preview, or the "no preview" card). */
view: boolean;
edit: boolean;
editDetails: boolean;
redownload: boolean;
}
export function libraryRowActions(job: DownloadJob): LibraryRowActions {
const media = isMediaKind(job);
return {
play: media,
view: !media && job.can_download,
edit: media && job.can_download && hasDuration(job),
editDetails: media,
redownload: !job.job_kind || job.job_kind === "download",
};
}
export interface SharedRowActions {
play: boolean;
view: boolean;
save: boolean;
edit: boolean;
}
export function sharedRowActions(job: DownloadJob): SharedRowActions {
const media = isMediaKind(job);
// The owner's `can_download` gates save/play/edit on a shared item (it reflects their link ACL).
return {
play: media && job.can_download,
view: !media && job.can_download,
save: job.can_download,
edit: media && job.can_download && hasDuration(job),
};
}