refactor(frontend): extract logic to hooks, split inline components
Area 1 (tv/page.tsx 964→423 lines): - hooks: use-fullscreen, use-idle, use-volume, use-quality, use-subtitles, use-channel-input, use-channel-passwords, use-tv-keyboard - components: SubtitlePicker, VolumeControl, QualityPicker, TopControlBar, LogoWatermark, AutoplayPrompt, ChannelNumberOverlay, TvBaseLayer Area 2 (edit-channel-sheet.tsx 1244→678 lines): - hooks: use-channel-form (all form state + reset logic) - lib/schemas.ts: extracted Zod schemas + extractErrors - components: AlgorithmicFilterEditor, RecyclePolicyEditor, WebhookEditor, AccessSettingsEditor, LogoEditor Area 3 (dashboard/page.tsx 406→261 lines): - hooks: use-channel-order, use-import-channel, use-regenerate-all - lib/channel-export.ts: pure export utility - components: DashboardHeader
This commit is contained in:
158
k-tv-frontend/hooks/use-channel-form.ts
Normal file
158
k-tv-frontend/hooks/use-channel-form.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { minsToTime } from "@/app/(main)/dashboard/components/block-timeline";
|
||||
import type {
|
||||
AccessMode,
|
||||
ChannelResponse,
|
||||
LogoPosition,
|
||||
ProgrammingBlock,
|
||||
MediaFilter,
|
||||
RecyclePolicy,
|
||||
} from "@/lib/types";
|
||||
|
||||
export const WEBHOOK_PRESETS = {
|
||||
discord: `{
|
||||
"embeds": [{
|
||||
"title": "📺 {{event}}",
|
||||
"description": "{{#if data.item.title}}Now playing: **{{data.item.title}}**{{else}}No signal{{/if}}",
|
||||
"color": 3447003,
|
||||
"timestamp": "{{timestamp}}"
|
||||
}]
|
||||
}`,
|
||||
slack: `{
|
||||
"text": "📺 *{{event}}*{{#if data.item.title}} — {{data.item.title}}{{/if}}"
|
||||
}`,
|
||||
} as const;
|
||||
|
||||
export type WebhookFormat = "default" | "discord" | "slack" | "custom";
|
||||
|
||||
export function defaultFilter(): MediaFilter {
|
||||
return {
|
||||
content_type: null,
|
||||
genres: [],
|
||||
decade: null,
|
||||
tags: [],
|
||||
min_duration_secs: null,
|
||||
max_duration_secs: null,
|
||||
collections: [],
|
||||
series_names: [],
|
||||
search_term: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultBlock(startMins = 20 * 60, durationMins = 60): ProgrammingBlock {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
name: "",
|
||||
start_time: minsToTime(startMins),
|
||||
duration_mins: durationMins,
|
||||
content: { type: "algorithmic", filter: defaultFilter(), strategy: "random" },
|
||||
loop_on_finish: true,
|
||||
ignore_recycle_policy: false,
|
||||
access_mode: "public",
|
||||
};
|
||||
}
|
||||
|
||||
export function useChannelForm(channel: ChannelResponse | null) {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [timezone, setTimezone] = useState("UTC");
|
||||
const [blocks, setBlocks] = useState<ProgrammingBlock[]>([]);
|
||||
const [recyclePolicy, setRecyclePolicy] = useState<RecyclePolicy>({
|
||||
cooldown_days: null,
|
||||
cooldown_generations: null,
|
||||
min_available_ratio: 0.1,
|
||||
});
|
||||
const [autoSchedule, setAutoSchedule] = useState(false);
|
||||
const [accessMode, setAccessMode] = useState<AccessMode>("public");
|
||||
const [accessPassword, setAccessPassword] = useState("");
|
||||
const [logo, setLogo] = useState<string | null>(null);
|
||||
const [logoPosition, setLogoPosition] = useState<LogoPosition>("top_right");
|
||||
const [logoOpacity, setLogoOpacity] = useState(100);
|
||||
const [webhookUrl, setWebhookUrl] = useState("");
|
||||
const [webhookPollInterval, setWebhookPollInterval] = useState<number | "">(5);
|
||||
const [webhookFormat, setWebhookFormat] = useState<WebhookFormat>("default");
|
||||
const [webhookBodyTemplate, setWebhookBodyTemplate] = useState("");
|
||||
const [webhookHeaders, setWebhookHeaders] = useState("");
|
||||
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Reset form when channel changes
|
||||
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);
|
||||
}
|
||||
}, [channel]);
|
||||
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
// Basic info
|
||||
name, setName,
|
||||
description, setDescription,
|
||||
timezone, setTimezone,
|
||||
autoSchedule, setAutoSchedule,
|
||||
// Access
|
||||
accessMode, setAccessMode,
|
||||
accessPassword, setAccessPassword,
|
||||
// Logo
|
||||
logo, setLogo,
|
||||
logoPosition, setLogoPosition,
|
||||
logoOpacity, setLogoOpacity,
|
||||
fileInputRef,
|
||||
// Webhook
|
||||
webhookUrl, setWebhookUrl,
|
||||
webhookPollInterval, setWebhookPollInterval,
|
||||
webhookFormat, setWebhookFormat,
|
||||
webhookBodyTemplate, setWebhookBodyTemplate,
|
||||
webhookHeaders, setWebhookHeaders,
|
||||
// Blocks
|
||||
blocks, setBlocks,
|
||||
selectedBlockId, setSelectedBlockId,
|
||||
recyclePolicy, setRecyclePolicy,
|
||||
addBlock,
|
||||
updateBlock,
|
||||
removeBlock,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user