diff --git a/web/sections/MediaBlockSummary.tsx b/web/sections/MediaBlockSummary.tsx new file mode 100644 index 0000000..ab6892b --- /dev/null +++ b/web/sections/MediaBlockSummary.tsx @@ -0,0 +1,111 @@ +import { useEffect, useState } from 'react'; +import { Loader2, RefreshCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { posterUrl, thumbUrl } from '../thumbnail'; +import { useMediaSync } from '../useMediaSync'; + +const IS_VIDEO = /\.(mp4|mov|webm|m4v)$/i; + +/** + * Which file a media block points at, and whether anything has been made from it yet. + * + * ── Why "not converted" has to be SAID ── + * + * Nothing on the site renders an ORIGINAL. Every picture and every video is served from the derived + * ladder, so a block pointing at a file the pipeline has not been through renders an empty box — on + * the page, in the preview, and here. + * + * That is a normal thing to hit rather than a mistake: drop a file in, choose it in the same minute, + * and you are ahead of a conversion that only starts twenty seconds after the folder goes quiet. An + * empty box is indistinguishable from a block saved wrong, so it gets looked for in the wrong place. + * + * ── Why it reads the sync rather than guessing ── + * + * "Not converted" and "converting right now" want different things from somebody: the second wants + * waiting and the first wants a button. `useMediaSync` already holds both — `current` is what the run + * has open this second, and the ledger's `missing`/`stale` are what a run would pick up — so this + * says which of the two it is instead of offering one answer for both. + * + * Reading is free here: the state is a `useGlobal` slot kept current by SyncButton in the action bar, + * which is mounted whenever this panel is. Nothing extra is polled by drawing this. + * + * ── The path is shown whole ── + * + * From `originals/`, and not truncated. Half these files are called the same thing — every collection + * has a hero, every folder a `_1` — so a bare filename identifies one only to somebody who already + * knows which it is, and `truncate` cuts the end, which is the wrong half of a path. + */ +export const MediaBlockSummary = ({ url, onChoose }: { url: string | null; onChoose: () => void }) => { + const [missing, setMissing] = useState(false); + const { running, state, ledger, busy, start } = useMediaSync(); + + // Cleared when the block is pointed somewhere else, or the old file's verdict sticks to the new one. + useEffect(() => setMissing(false), [url]); + + const reconciliation = ledger?.reconciliation ?? null; + // `current` is what the run has open this second — see SyncState. + const converting = Boolean(url) && (state?.current ?? []).includes(url!); + const queued = + Boolean(url) && + (reconciliation?.missing.includes(url!) === true || reconciliation?.stale.includes(url!) === true); + + return ( + <> +
+
+ Not converted yet. The site serves + converted files only, so this block renders nothing until the pipeline has been through + it.{' '} + {running + ? 'A run is going, but this file is not in it — it was picked up after the run started.' + : queued + ? 'It is outstanding and the next run will take it.' + : 'A run starts on its own about twenty seconds after a folder goes quiet. A file renamed or replaced while one was going needs another pass.'} +
+ + > + )} +