-
Filter
-
- {providers.length > 1 && (
-
- setProviderId(v)}>
- {providers.map((p) => (
-
- ))}
-
-
- )}
-
-
-
-
- setFilter({
- content_type: v === "" ? null : (v as ContentType),
- // clear series names if switching away from episode
- series_names: v !== "episode" ? [] : content.filter.series_names,
- })
- }
- >
-
-
-
-
-
-
-
- setStrategy(v as FillStrategy)}
- >
-
-
-
-
-
-
-
- {/* Series β only meaningful for episodes when provider supports it */}
- {isEpisode && capabilities?.series !== false && (
-
- setFilter({ series_names: v })}
- series={series ?? []}
- isLoading={loadingSeries}
- />
-
- )}
-
- {/* Library/Directory β real collection names when the provider supports it */}
-
- {collections && collections.length > 0 ? (
- setFilter({ collections: v ? [v] : [] })}
- >
-
- {collections.map((c) => (
-
- ))}
-
- ) : (
- setFilter({ collections: v })}
- placeholder="Library IDβ¦"
- />
- )}
-
-
- {/* Genres β only shown when provider supports it */}
- {capabilities?.genres !== false && (
-
- setFilter({ genres: v })}
- placeholder="Comedy, Animationβ¦"
- />
- {genreOptions && genreOptions.length > 0 && (
-
-
- {showGenres && (
-
- {genreOptions
- .filter((g) => !content.filter.genres.includes(g))
- .map((g) => (
-
- ))}
-
- )}
-
- )}
-
- )}
-
-
- setFilter({ tags: v })}
- placeholder="classic, familyβ¦"
- />
-
-
-
-
- setFilter({ decade: v === "" ? null : (v as number) })}
- placeholder="1990"
- error={!!errors[`${pfx}.content.filter.decade`]}
- />
-
-
-
- setFilter({ min_duration_secs: v === "" ? null : (v as number) * 60 })
- }
- placeholder="30"
- error={!!errors[`${pfx}.content.filter.min_duration_secs`]}
- />
-
-
-
- setFilter({ max_duration_secs: v === "" ? null : (v as number) * 60 })
- }
- placeholder="120"
- error={!!errors[`${pfx}.content.filter.max_duration_secs`]}
- />
-
-
-
- {/* Preview β snapshot of current filter+strategy, only fetches on explicit click */}
-
-
- );
-}
-
-// ---------------------------------------------------------------------------
-// BlockEditor (detail form for a single block)
+// BlockEditor β inline because it's only used here and depends on local types
// ---------------------------------------------------------------------------
interface BlockEditorProps {
block: ProgrammingBlock;
index: number;
errors: FieldErrors;
- onChange: (block: ProgrammingBlock) => void;
providers: ProviderInfo[];
+ onChange: (block: ProgrammingBlock) => void;
}
-function BlockEditor({ block, index, errors, onChange, providers }: BlockEditorProps) {
+function BlockEditor({ block, index, errors, providers, onChange }: BlockEditorProps) {
const setField = (key: K, value: ProgrammingBlock[K]) =>
onChange({ ...block, [key]: value });
@@ -515,9 +180,8 @@ function BlockEditor({ block, index, errors, onChange, providers }: BlockEditorP
onChange({ ...block, content: { ...content, strategy } });
};
- const setProviderId = (id: string) => {
+ const setProviderId = (id: string) =>
onChange({ ...block, content: { ...content, provider_id: id } });
- };
return (
@@ -566,10 +230,10 @@ function BlockEditor({ block, index, errors, onChange, providers }: BlockEditorP
content={content}
pfx={pfx}
errors={errors}
+ providers={providers}
setFilter={setFilter}
setStrategy={setStrategy}
setProviderId={setProviderId}
- providers={providers}
/>
{content.strategy === "sequential" && (
@@ -577,11 +241,13 @@ function BlockEditor({ block, index, errors, onChange, providers }: BlockEditorP
Sequential options
-
- );
-}
-
-// ---------------------------------------------------------------------------
-// Recycle policy editor
-// ---------------------------------------------------------------------------
-
-function RecyclePolicyEditor({
- policy,
- errors,
- onChange,
-}: {
- policy: RecyclePolicy;
- errors: FieldErrors;
- onChange: (policy: RecyclePolicy) => void;
-}) {
- return (
-
-
-
- onChange({ ...policy, cooldown_days: v === "" ? null : (v as number) })}
- min={0}
- placeholder="7"
- />
-
-
- onChange({ ...policy, cooldown_generations: v === "" ? null : (v as number) })}
- min={0}
- placeholder="3"
- />
-
-
-
- onChange({ ...policy, min_available_ratio: v === "" ? 0.1 : (v as number) })}
- min={0}
- max={1}
- step={0.01}
- placeholder="0.1"
- error={!!errors["recycle_policy.min_available_ratio"]}
+
+ Block access
+
+ onChange({ ...block, access_mode: m })}
+ onAccessPasswordChange={(pw) => onChange({ ...block, access_password: pw })}
+ label="Access mode"
+ passwordLabel="Block password"
+ passwordHint="Leave blank to keep existing"
/>
-
+
);
}
// ---------------------------------------------------------------------------
-// Main sheet
+// EditChannelSheet
// ---------------------------------------------------------------------------
interface EditChannelSheetProps {
@@ -748,69 +363,22 @@ export function EditChannelSheet({
error,
providers = [],
}: EditChannelSheetProps) {
- const [name, setName] = useState("");
- const [description, setDescription] = useState("");
- const [timezone, setTimezone] = useState("UTC");
- const [blocks, setBlocks] = useState([]);
- const [recyclePolicy, setRecyclePolicy] = useState({
- cooldown_days: null,
- cooldown_generations: null,
- min_available_ratio: 0.1,
- });
- const [autoSchedule, setAutoSchedule] = useState(false);
- const [accessMode, setAccessMode] = useState("public");
- const [accessPassword, setAccessPassword] = useState("");
- const [logo, setLogo] = useState(null);
- const [logoPosition, setLogoPosition] = useState("top_right");
- const [logoOpacity, setLogoOpacity] = useState(100);
- const [webhookUrl, setWebhookUrl] = useState("");
- const [webhookPollInterval, setWebhookPollInterval] = useState(5);
- const [webhookFormat, setWebhookFormat] = useState("default");
- const [webhookBodyTemplate, setWebhookBodyTemplate] = useState("");
- const [webhookHeaders, setWebhookHeaders] = useState("");
- const [selectedBlockId, setSelectedBlockId] = useState(null);
+ const form = useChannelForm(channel);
const [fieldErrors, setFieldErrors] = useState({});
- const fileInputRef = useRef(null);
-
- useEffect(() => {
- if (channel) {
- setName(channel.name);
- setDescription(channel.description ?? "");
- setTimezone(channel.timezone);
- setBlocks(channel.schedule_config.blocks);
- setRecyclePolicy(channel.recycle_policy);
- setAutoSchedule(channel.auto_schedule);
- setAccessMode(channel.access_mode ?? "public");
- setAccessPassword("");
- setLogo(channel.logo ?? null);
- setLogoPosition(channel.logo_position ?? "top_right");
- setLogoOpacity(Math.round((channel.logo_opacity ?? 1) * 100));
- setWebhookUrl(channel.webhook_url ?? "");
- setWebhookPollInterval(channel.webhook_poll_interval_secs ?? 5);
- const tmpl = channel.webhook_body_template ?? "";
- setWebhookBodyTemplate(tmpl);
- setWebhookHeaders(channel.webhook_headers ?? "");
- if (!tmpl) {
- setWebhookFormat("default");
- } else if (tmpl === WEBHOOK_PRESETS.discord) {
- setWebhookFormat("discord");
- } else if (tmpl === WEBHOOK_PRESETS.slack) {
- setWebhookFormat("slack");
- } else {
- setWebhookFormat("custom");
- }
- setSelectedBlockId(null);
- setFieldErrors({});
- }
- }, [channel]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!channel) return;
const result = channelFormSchema.safeParse({
- name, description, timezone, blocks, recycle_policy: recyclePolicy,
- auto_schedule: autoSchedule, access_mode: accessMode, access_password: accessPassword,
+ name: form.name,
+ description: form.description,
+ timezone: form.timezone,
+ blocks: form.blocks,
+ recycle_policy: form.recyclePolicy,
+ auto_schedule: form.autoSchedule,
+ access_mode: form.accessMode,
+ access_password: form.accessPassword,
});
if (!result.success) {
@@ -820,40 +388,26 @@ export function EditChannelSheet({
setFieldErrors({});
onSubmit(channel.id, {
- name,
- description,
- timezone,
- schedule_config: { blocks },
- recycle_policy: recyclePolicy,
- auto_schedule: autoSchedule,
- access_mode: accessMode !== "public" ? accessMode : "public",
- access_password: accessPassword || "",
- logo: logo,
- logo_position: logoPosition,
- logo_opacity: logoOpacity / 100,
- webhook_url: webhookUrl || null,
- ...(webhookUrl
- ? { webhook_poll_interval_secs: webhookPollInterval === "" ? 5 : webhookPollInterval }
+ name: form.name,
+ description: form.description,
+ timezone: form.timezone,
+ schedule_config: { blocks: form.blocks },
+ recycle_policy: form.recyclePolicy,
+ auto_schedule: form.autoSchedule,
+ access_mode: form.accessMode !== "public" ? form.accessMode : "public",
+ access_password: form.accessPassword || "",
+ logo: form.logo,
+ logo_position: form.logoPosition,
+ logo_opacity: form.logoOpacity / 100,
+ webhook_url: form.webhookUrl || null,
+ ...(form.webhookUrl
+ ? {
+ webhook_poll_interval_secs:
+ form.webhookPollInterval === "" ? 5 : form.webhookPollInterval,
+ }
: {}),
- webhook_body_template: webhookBodyTemplate || null,
- webhook_headers: webhookHeaders || null,
- });
- };
-
- const addBlock = (startMins = 20 * 60, durationMins = 60) => {
- const block = defaultBlock(startMins, durationMins);
- setBlocks((prev) => [...prev, block]);
- setSelectedBlockId(block.id);
- };
-
- const updateBlock = (idx: number, block: ProgrammingBlock) =>
- setBlocks((prev) => prev.map((b, i) => (i === idx ? block : b)));
-
- const removeBlock = (idx: number) => {
- setBlocks((prev) => {
- const next = prev.filter((_, i) => i !== idx);
- if (selectedBlockId === prev[idx].id) setSelectedBlockId(null);
- return next;
+ webhook_body_template: form.webhookBodyTemplate || null,
+ webhook_headers: form.webhookHeaders || null,
});
};
@@ -869,27 +423,32 @@ export function EditChannelSheet({