"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { VideoPlayer, ChannelInfo, ChannelControls, ScheduleOverlay, UpNextBanner, NoSignal, } from "./components"; import { useAuthContext } from "@/context/auth-context"; import { useChannels, useCurrentBroadcast, useEpg } from "@/hooks/use-channels"; import { useStreamUrl, fmtTime, calcProgress, minutesUntil, toScheduleSlots, findNextSlot, } from "@/hooks/use-tv"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const IDLE_TIMEOUT_MS = 3500; const BANNER_THRESHOLD = 80; // show "up next" when progress ≥ this % // --------------------------------------------------------------------------- // Page // --------------------------------------------------------------------------- export default function TvPage() { const { token } = useAuthContext(); // Channel list const { data: channels, isLoading: isLoadingChannels } = useChannels(); // Channel navigation const [channelIdx, setChannelIdx] = useState(0); const channel = channels?.[channelIdx]; // Overlay / idle state const [showOverlays, setShowOverlays] = useState(true); const [showSchedule, setShowSchedule] = useState(false); const idleTimer = useRef | null>(null); // Tick for live progress calculation (every 30 s is fine for the progress bar) const [, setTick] = useState(0); useEffect(() => { const id = setInterval(() => setTick((n) => n + 1), 30_000); return () => clearInterval(id); }, []); // Per-channel data const { data: broadcast, isLoading: isLoadingBroadcast } = useCurrentBroadcast(channel?.id ?? ""); const { data: epgSlots } = useEpg(channel?.id ?? ""); const { data: streamUrl } = useStreamUrl(channel?.id, token); // ------------------------------------------------------------------ // Derived display values // ------------------------------------------------------------------ const hasBroadcast = !!broadcast; const progress = hasBroadcast ? calcProgress(broadcast.slot.start_at, broadcast.slot.item.duration_secs) : 0; const scheduleSlots = toScheduleSlots(epgSlots ?? [], broadcast?.slot.id); const nextSlot = findNextSlot(epgSlots ?? [], broadcast?.slot.id); const showBanner = hasBroadcast && progress >= BANNER_THRESHOLD && !!nextSlot; // ------------------------------------------------------------------ // Idle detection // ------------------------------------------------------------------ const resetIdle = useCallback(() => { setShowOverlays(true); if (idleTimer.current) clearTimeout(idleTimer.current); idleTimer.current = setTimeout( () => setShowOverlays(false), IDLE_TIMEOUT_MS, ); }, []); useEffect(() => { resetIdle(); return () => { if (idleTimer.current) clearTimeout(idleTimer.current); }; }, [resetIdle]); // ------------------------------------------------------------------ // Channel switching // ------------------------------------------------------------------ const channelCount = channels?.length ?? 0; const prevChannel = useCallback(() => { setChannelIdx((i) => (i - 1 + Math.max(channelCount, 1)) % Math.max(channelCount, 1)); resetIdle(); }, [channelCount, resetIdle]); const nextChannel = useCallback(() => { setChannelIdx((i) => (i + 1) % Math.max(channelCount, 1)); resetIdle(); }, [channelCount, resetIdle]); const toggleSchedule = useCallback(() => { setShowSchedule((s) => !s); resetIdle(); }, [resetIdle]); // ------------------------------------------------------------------ // Keyboard shortcuts // ------------------------------------------------------------------ useEffect(() => { const handleKey = (e: KeyboardEvent) => { if ( e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement ) return; switch (e.key) { case "ArrowUp": case "PageUp": e.preventDefault(); nextChannel(); break; case "ArrowDown": case "PageDown": e.preventDefault(); prevChannel(); break; case "g": case "G": toggleSchedule(); break; } }; window.addEventListener("keydown", handleKey); return () => window.removeEventListener("keydown", handleKey); }, [nextChannel, prevChannel, toggleSchedule]); // ------------------------------------------------------------------ // Render helpers // ------------------------------------------------------------------ const renderBase = () => { if (isLoadingChannels) { return ; } if (!channels || channels.length === 0) { return ( ); } if (isLoadingBroadcast) { return ; } if (!hasBroadcast) { return ( ); } if (streamUrl) { return ( ); } // Broadcast exists but stream URL resolving — show no-signal until ready return ; }; // ------------------------------------------------------------------ // Render // ------------------------------------------------------------------ return (
{/* ── Base layer ─────────────────────────────────────────────── */}
{renderBase()}
{/* ── Overlays — only when channels available ─────────────────── */} {channel && ( <>
{/* Top-right: guide toggle */}
{/* Bottom: banner + info row */}
{showBanner && nextSlot && ( )}
{hasBroadcast ? ( ) : ( /* Minimal channel badge when no broadcast */
{channelIdx + 1} {channel.name}
)}
{/* Schedule overlay — outside the fading div so it has its own visibility */} {showOverlays && showSchedule && (
)} )}
); }