diff --git a/frontend/src/components/DownloadCenter.tsx b/frontend/src/components/DownloadCenter.tsx index 0f2cb7b5..819890dd 100644 --- a/frontend/src/components/DownloadCenter.tsx +++ b/frontend/src/components/DownloadCenter.tsx @@ -35,6 +35,7 @@ import { useLiveQuery } from "../lib/useLiveQuery"; import { useMe } from "../lib/useMe"; import { api, type DownloadJob } from "../lib/api"; import { invalidateDownloads, quickShareLink } from "../lib/downloads"; +import { libraryRowActions, sharedRowActions } from "../lib/downloadActions"; import { inputCls } from "./ui/form"; import { Button } from "./ui/Button"; import { DownloadRow, IconBtn, UsageBar } from "./downloadRow"; @@ -280,28 +281,22 @@ export default function DownloadCenter() {
{library.map((job) => { - // E7·S5b — gate row actions on the stored media_kind. A generic uploaded file - // (media_kind='file') is download+share only: no player and no timeline to trim - // (Play/Edit are media-only), and no metadata/channel edit. Re-download needs a - // re-fetchable source, which an edit clip and an upload both lack (backend rejects - // both), so it's a true-download-only action. - const isFile = job.asset?.media_kind === "file"; - const isMedia = !isFile; - const canRedownload = job.job_kind !== "edit" && job.job_kind !== "upload"; + // Row actions gated on the stored media_kind + job_kind (see downloadActions). + const a = libraryRowActions(job); return ( - {isMedia && ( + {a.play && ( setPlayJob(job)} title={t("downloads.actions.play")}> )} {saveBtn(job)} - {isMedia && job.can_download && (job.asset?.duration_s ?? 0) > 0 && ( + {a.edit && ( setEditJob(job)} title={t("downloads.actions.edit")}> )} - {isMedia && ( + {a.editDetails && ( setMetaJob(job)} title={t("downloads.actions.editDetails")} @@ -318,7 +313,7 @@ export default function DownloadCenter() { > - {canRedownload && ( + {a.redownload && ( confirmThen( @@ -367,18 +362,17 @@ export default function DownloadCenter() { {tab === "shared" && (
{(sharedQ.data ?? []).map((job) => { - // Same media_kind gate as the library (S5b): a shared generic file is download-only — - // no player, no trim. Its owner set can_download, which still gates save/edit here. - const isMedia = job.asset?.media_kind !== "file"; + // Same media_kind + can_download gate as the library, via the shared helper (S5b). + const a = sharedRowActions(job); return ( - {isMedia && job.can_download && ( + {a.play && ( setPlayJob(job)} title={t("downloads.actions.play")}> )} - {job.can_download && saveBtn(job)} - {isMedia && job.can_download && (job.asset?.duration_s ?? 0) > 0 && ( + {a.save && saveBtn(job)} + {a.edit && ( setEditJob(job)} title={t("downloads.actions.edit")}> @@ -399,11 +393,18 @@ export default function DownloadCenter() { ); })} - {sharedQ.data && sharedQ.data.length === 0 && ( -
- {t("downloads.empty.shared")} -
- )} + {(sharedQ.data ?? []).length === 0 && + (sharedQ.isError && !sharedQ.data ? ( + sharedQ.refetch()}> + {t("common.loadError")} + + ) : sharedQ.isLoading ? ( + + ) : ( +
+ {t("downloads.empty.shared")} +
+ ))}
)} diff --git a/frontend/src/components/LibraryPlayer.dom.test.tsx b/frontend/src/components/LibraryPlayer.dom.test.tsx index 0ca2fc96..aaa05ede 100644 --- a/frontend/src/components/LibraryPlayer.dom.test.tsx +++ b/frontend/src/components/LibraryPlayer.dom.test.tsx @@ -1,9 +1,9 @@ // @vitest-environment jsdom // // E7·S4b — the in-app library player streams the completed item's LOCAL file into a native media -// element (no transcode; S4a guarantees a browser-native container). A video asset (has pixel dims) -// renders