import type { ModuleUiHostContext } from "@ekklesia/module-ui-sdk"; import { ActionBar, Badge, Button, EmptyState, Field, Panel, Select, } from "@ekklesia/ui"; import { type ReactElement, useState } from "react"; import type { SignageApi } from "../api.js"; import { useAction, useResource, useTick } from "../hooks.js"; import type { ChannelGroupView, PlaybackDetailView, SettingsResponse, TimelineSummary, } from "../types.js"; import { PreviewPanel } from "./PreviewPanel.js"; import { FailureNote, formatDuration, formatInstant, renderLoadable, } from "./shared.js"; /** * Live transport for one channel group. * * Every control here is the same operation the `signage.playback` capability exposes, over the same * serialized service — a volunteer with a phone and an operator at this screen are pressing the same * button, and neither can produce a state the other did not ask for. */ export function PlaybackPanel({ api, context, settings, canControl, groupId, onGroupChange, }: { readonly api: SignageApi; readonly context: ModuleUiHostContext; readonly settings: SettingsResponse; readonly canControl: boolean; readonly groupId: string | undefined; readonly onGroupChange: (groupId: string) => void; }): ReactElement { const groups = useResource( () => api.get<{ groups: readonly ChannelGroupView[] }>("channel-groups"), [], ); const timelines = useResource( () => api.get<{ timelines: readonly TimelineSummary[] }>("timelines"), [], ); const detail = useResource( () => groupId === undefined ? Promise.resolve(undefined) : api.get(`playback/${groupId}`), [groupId], ); const command = useAction(() => { detail.reload(); groups.reload(); }); const [activateId, setActivateId] = useState(""); const previewChannelId = groups.state.status === "ready" ? (groups.state.value.groups .find((group) => group.id === groupId) ?.channels.find((channel) => channel.isDefault)?.id ?? undefined) : undefined; // A one-second tick so the remaining time counts down rather than sitting still between refreshes. useTick(1000); function run(path: string, body?: unknown): void { if (groupId === undefined) { return; } command.run(() => api.send("POST", `playback/${groupId}/${path}`, body)); } function timelineLabel(timelineId: string): string { if (timelines.state.status !== "ready") { return timelineId; } return ( timelines.state.value.timelines.find( (timeline) => timeline.id === timelineId, )?.name ?? timelineId ); } return (
( {(control) => ( )} ), { label: "Loading groups…" }, )} description="One clock for every channel in the group." title="Playback" > {groupId === undefined ? ( ) : ( renderLoadable( detail.state, (value) => value === undefined ? ( ) : (
{value.snapshot.activeTimelineId === null ? undefined : ( {`Active: ${timelineLabel(value.snapshot.activeTimelineId)}`} )} {value.snapshot.transport} {value.snapshot.blackout ? ( Blackout ) : undefined} {value.current?.stale === true ? ( Stale ) : undefined} {value.current?.withdrawn === true ? ( Withdrawn ) : undefined} {value.snapshot.pendingTimelineId === null ? undefined : ( Activation queued )}
{value.current === null ? ( ) : (

Now showing

Provider
{value.current.providerId}
Item
{value.current.itemId}
Renderer
{value.current.rendererContributionId ?? "—"}
Duration
{formatDuration(value.current.durationMs)}
Remaining
{value.snapshot.remainingMs === null ? "—" : formatDuration(value.snapshot.remainingMs)}
Started
{formatInstant( value.current.startedAtMs, settings.timezone, )}
)} {canControl ? ( {value.snapshot.transport === "paused" ? ( ) : ( )} {value.snapshot.staticAssetId === null ? undefined : ( )} ) : undefined} {canControl ? (
{renderLoadable( timelines.state, (loaded) => ( <> {(control) => ( )} ), { label: "Loading timelines…" }, )}
) : undefined} {value.history.length === 0 ? undefined : (

Recently played

    {value.history.slice(0, 8).map((entry) => (
  • {entry.itemId} {`${entry.providerId} · ${formatInstant(entry.startedAtMs, settings.timezone)}`}
  • ))}
)}
), { label: "Loading playback…", onRetry: detail.reload }, ) )}
); }