"use client"; // THE /channels ROW CONTROL for the channel priority model. // // It replaces the sync-inclusion toggle that used to sit in this cell. That // toggle flipped one boolean on one channel's config.json; this control writes // the corpus-wide priority document — one base tier per channel, plus optional // per-operation pins — through `saveChannelPriorityAction`, which is the ONE // writer of that block and recompiles the four lane trees in the same save. // // THE BASE TIER IS THE ROW; THE PINS ARE BEHIND A DISCLOSURE. A channel's tier // is the answer for every operation unless an operation is pinned, and pins are // the rare case (they exist because "stop syncing, keep everything else" — the // retired `excludeFromSync` — has to remain sayable). Putting five selects in // every row would bury the one number the page is for, so the row shows the // base tier and a marker counting the pins, and the disclosure is where they // are set. // // `focused` and `heldReason` are DISPLAY facts passed down from the page, not a // second read: focus is a corpus-wide selector resolved once on the server, and // a row cannot work out on its own whether it is being held. // // OPTIMISTIC, NOT STATEFUL. `useOptimistic` shows the operator's choice for the // length of the transition and then defers to the server value — so a bulk edit // or the page's auto-refresh can never leave this select disagreeing with // settings.json, which a `useState` seeded from props would. import { useOptimistic, useState, useTransition } from "react"; import { PRIORITY_OPERATIONS, STORED_CHANNEL_TIERS, type PriorityOperation, type StoredChannelTier, } from "yt-dlp-transcript-common/lib/channelPriority"; import { applyChannelPriorityPresetAction, setChannelOperationTierAction, setChannelTierAction, type ActionResult, } from "../actions"; import { RACK_LAYERS } from "./rackLayout"; export type ChannelTierSelectProps = { slug: string; // The channel's BASE tier (tierOf), not its effective tier for any one // operation — the overrides are shown beside it, not folded into it. tier: StoredChannelTier; // Per-operation pins, normalised (only operations that differ from `tier`). overrides?: Partial>; // This channel is in the active focus set. Focus is a corpus-wide selector, // never a stored tier, so it is passed in rather than read off `tier`. focused?: boolean; // "Held — focus: " for a non-focus row while a focus holds the lanes. heldReason?: string | null; // Why the machine paused this channel, or null. Distinct from `heldReason`, // which is the focus holding a channel that is otherwise running: this one // says the tier ITSELF was set by something other than the operator, and // will be set back. autoPausedReason?: string | null; // THE CHANNEL'S MEDIA HOLD — "held: its media is moving (…)" while a // relocation marker stands — or null (common/views/channelRow.ts' // `mediaHold`). Distinct from both chips above: the lanes skip this channel // whatever its tier and whatever the focus, until the move completes or its // marker is cleared (release 16 slice RM). mediaHold?: string | null; disabled?: boolean; }; const TIER_LABEL: Record = { normal: "Normal", low: "Low", paused: "Paused", }; const OPERATION_LABEL: Record = { sync: "Sync", transcription: "Transcription", download: "Download", digest: "Digest", backfill: "Backfill", }; type View = { tier: StoredChannelTier; overrides: Partial>; }; export default function ChannelTierSelect({ slug, tier, overrides = {}, focused = false, heldReason = null, autoPausedReason = null, mediaHold = null, disabled = false, }: ChannelTierSelectProps): React.ReactNode { const [pending, startTransition] = useTransition(); const [view, setView] = useOptimistic({ tier, overrides }); // A FAILED WRITE MUST SAY SO. The writer returns `{error}` before it // revalidates, so on a failure the optimistic value silently snaps back to // the server's — which reads as "the click did nothing" rather than as an // error. Same shape as SyncAllChannelsButton's: role="alert", a labelled // span, text-destructive. const [error, setError] = useState(null); const pins = PRIORITY_OPERATIONS.filter((op) => view.overrides[op]); const busy = disabled || pending; function run(next: View, action: () => Promise) { startTransition(async () => { setView(next); try { const result = await action(); setError(result?.error ?? null); } catch (e) { setError((e as Error).message); } }); } return ( // ONE LINE, not a four-deep stack. The tier cell used to be a select, a // chip row, an orange held sentence and a disclosure stacked vertically in // every one of 67 rows — which is what made the rows 90px tall and made the // most-repeated element on the page (the held note, identical on 61 rows) // the one carrying the least information.
{focused && ( Focused )} {pins.length > 0 && ( `${OPERATION_LABEL[op]}: ${TIER_LABEL[view.overrides[op]!]}`) .join(" · ")} > {pins.length} pinned )} {/* THE HELD CHIP. A corpus-wide focus holds every channel outside it, so the SENTENCE is the same on 61 rows and belongs above the table, where the focus line states it once. What is per-row is the FACT, and a chip says it in a tenth of the width. The full reason is still carried on the element — as screen-reader text and on `title` — so nothing is lost to the reader who needs it, and the element's text still contains the sentence the suite pins. */} {heldReason && ( held — {heldReason} )} {/* WHY IT IS PAUSED, when it was not the operator who paused it. Without this the rack shows a Paused channel and no way to tell a deliberate pause from a drive that fell off a USB cable — which is exactly the "automatic disable AND FLAG" the operator asked for. */} {autoPausedReason && ( storage — {autoPausedReason} )} {/* THE MEDIA HOLD. The chip says the fact in the rack's width; the sentence ("held: its media is moving …") is on `title` and in the screen-reader text, as for the two chips above. */} {mediaHold && ( media held — {mediaHold} )} {error && ( {error} )} {/* OPENING THIS MUST NOT MOVE THE RACK. In a 40px row an inline panel would push every row below it down by 150px, so the panel is absolute and overlays them instead. ...WHICH MEANS THE SCROLL REGION CLIPS IT. Opened on a row near the rack's bottom (or, below md, its right edge) the panel hung off the region with its selects out of reach, and nothing said it was there. Opening scrolls the region — and the document, below md — just far enough to show the whole panel; `nearest` leaves an already-visible one exactly where it is. Below md the selection deck pins to the bottom of the SCREEN, outside the (isolated) rack, and paints over it — so the panel keeps a 12rem scroll margin there, about the deck's height, and "nearest" stops it above the deck instead of under it. */}
{ const details = e.currentTarget; if (!details.open) return; requestAnimationFrame(() => details.lastElementChild?.scrollIntoView({ block: "nearest", inline: "nearest", }), ); }} > Advanced
{PRIORITY_OPERATIONS.map((op) => ( ))}
{/* The preset that retires `excludeFromSync`'s inverse: keep the playlist and metadata current, dispatch nothing. */}
); }