feat(downloads): gate library row actions by media_kind (E7 S5b)

Surface the S5a media_kind discriminator in the library UI. A generic
uploaded file (media_kind='file') is download+share only — no player and
no timeline to trim, so Play/Edit/Edit-details are media-only; it shows a
neutral file glyph instead of an (always-empty) thumbnail and no duration
chip (container/size still read). The "Shared with me" tab's file row is
gated the same way. Re-download is now hidden for edit clips and uploads
too (neither has a re-fetchable source — the backend already rejects both
with 409), removing a latent click-to-409 on edit rows.

FE-only: the serializer already emits media_kind (S5a). Adds media_kind to
the DownloadAsset type and "upload" to the job_kind union; DOM tests cover
the file-row presentation (glyph, no img, no duration).
This commit is contained in:
2026-08-29 18:38:21 +02:00
parent fe0b34cd00
commit b4ad59274b
5 changed files with 150 additions and 83 deletions
+10 -1
View File
@@ -1,6 +1,6 @@
import { useEffect, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { CalendarClock, Copy, Link2, Scissors } from "lucide-react";
import { CalendarClock, Copy, File, Link2, Scissors } from "lucide-react";
import clsx from "clsx";
import { qk } from "../lib/queryKeys";
import { useLiveQuery } from "../lib/useLiveQuery";
@@ -69,6 +69,15 @@ function Thumb({ job }: { job: DownloadJob }) {
// On load failure (a genuinely missing poster file) fall back to the remote source thumbnail.
const [src, setSrc] = useState(primary);
useEffect(() => setSrc(primary), [primary]);
// A generic uploaded file (media_kind='file') has no video frame to poster — it's a document/
// archive, not playable media. Show a neutral file glyph instead of an (always-empty) thumbnail.
if (job.asset?.media_kind === "file") {
return (
<div className="w-28 aspect-video shrink-0 rounded-lg overflow-hidden bg-surface border border-border flex items-center justify-center text-muted">
<File className="w-8 h-8" aria-hidden="true" />
</div>
);
}
return (
<div className="w-28 aspect-video shrink-0 rounded-lg overflow-hidden bg-surface border border-border">
{src ? (