import { useEffect, useMemo, useRef, useState } from 'react'; import type { LiveShowLayoutMode, LiveShowPhoto, LiveShowSettings } from '../services/liveShowApi'; const MIN_FIXED_SECONDS = 3; const MAX_FIXED_SECONDS = 20; function resolveApprovedAt(photo: LiveShowPhoto): number { if (!photo.approved_at) { return 0; } const parsed = Date.parse(photo.approved_at); return Number.isNaN(parsed) ? 0 : parsed; } function resolvePriority(photo: LiveShowPhoto): number { return Number.isFinite(photo.live_priority) ? photo.live_priority : 0; } export function resolveItemsPerFrame(layout: LiveShowLayoutMode): number { switch (layout) { case 'split': return 2; case 'grid_burst': return 4; case 'single': default: return 1; } } export function resolveIntervalMs(settings: LiveShowSettings, totalCount: number): number { if (settings.pace_mode === 'fixed') { const safeSeconds = Math.min(MAX_FIXED_SECONDS, Math.max(MIN_FIXED_SECONDS, settings.fixed_interval_seconds)); return safeSeconds * 1000; } if (totalCount >= 60) return 4500; if (totalCount >= 30) return 5500; if (totalCount >= 15) return 6500; if (totalCount >= 6) return 7500; return 9000; } export function resolvePlaybackQueue(photos: LiveShowPhoto[], settings: LiveShowSettings): LiveShowPhoto[] { if (photos.length === 0) { return []; } const newestFirst = [...photos].sort((a, b) => { const timeDiff = resolveApprovedAt(b) - resolveApprovedAt(a); if (timeDiff !== 0) return timeDiff; return b.id - a.id; }); if (settings.playback_mode === 'newest_first') { return newestFirst; } if (settings.playback_mode === 'curated') { const curated = photos.filter((photo) => photo.is_featured || resolvePriority(photo) > 0); const base = curated.length > 0 ? curated : photos; return [...base].sort((a, b) => { const priorityDiff = resolvePriority(b) - resolvePriority(a); if (priorityDiff !== 0) return priorityDiff; const timeDiff = resolveApprovedAt(b) - resolveApprovedAt(a); if (timeDiff !== 0) return timeDiff; return b.id - a.id; }); } const oldestFirst = [...photos].sort((a, b) => { const timeDiff = resolveApprovedAt(a) - resolveApprovedAt(b); if (timeDiff !== 0) return timeDiff; return a.id - b.id; }); const balanced: LiveShowPhoto[] = []; const seen = new Set(); let newestIndex = 0; let oldestIndex = 0; let newestStreak = 0; while (balanced.length < photos.length) { let added = false; if (newestIndex < newestFirst.length && newestStreak < 2) { const candidate = newestFirst[newestIndex++]; if (!seen.has(candidate.id)) { balanced.push(candidate); seen.add(candidate.id); newestStreak += 1; added = true; } } if (!added) { while (oldestIndex < oldestFirst.length && seen.has(oldestFirst[oldestIndex].id)) { oldestIndex += 1; } if (oldestIndex < oldestFirst.length) { const candidate = oldestFirst[oldestIndex++]; balanced.push(candidate); seen.add(candidate.id); newestStreak = 0; added = true; } } if (!added) { while (newestIndex < newestFirst.length && seen.has(newestFirst[newestIndex].id)) { newestIndex += 1; } if (newestIndex < newestFirst.length) { const candidate = newestFirst[newestIndex++]; balanced.push(candidate); seen.add(candidate.id); newestStreak += 1; added = true; } } if (!added) { break; } } return balanced; } export function buildFramePhotos( queue: LiveShowPhoto[], startIndex: number, itemsPerFrame: number ): LiveShowPhoto[] { if (queue.length === 0) { return []; } const safeCount = Math.min(itemsPerFrame, queue.length); const result: LiveShowPhoto[] = []; for (let offset = 0; offset < safeCount; offset += 1) { const idx = (startIndex + offset) % queue.length; result.push(queue[idx]); } return result; } export type LiveShowPlaybackState = { frame: LiveShowPhoto[]; layout: LiveShowLayoutMode; intervalMs: number; frameKey: string; nextFrame: LiveShowPhoto[]; }; export function useLiveShowPlayback( photos: LiveShowPhoto[], settings: LiveShowSettings, options: { paused?: boolean } = {} ): LiveShowPlaybackState { const queue = useMemo(() => resolvePlaybackQueue(photos, settings), [photos, settings]); const layout = settings.layout_mode; const itemsPerFrame = resolveItemsPerFrame(layout); const [index, setIndex] = useState(0); const currentIdRef = useRef(null); const paused = Boolean(options.paused); useEffect(() => { if (queue.length === 0) { setIndex(0); currentIdRef.current = null; return; } if (currentIdRef.current !== null) { const existingIndex = queue.findIndex((photo) => photo.id === currentIdRef.current); if (existingIndex >= 0) { setIndex(existingIndex); return; } } setIndex((prev) => prev % queue.length); }, [queue]); const frame = useMemo(() => { const framePhotos = buildFramePhotos(queue, index, itemsPerFrame); currentIdRef.current = framePhotos[0]?.id ?? null; return framePhotos; }, [queue, index, itemsPerFrame]); const frameKey = useMemo(() => { if (frame.length === 0) { return `empty-${layout}`; } return frame.map((photo) => photo.id).join('-'); }, [frame, layout]); const nextFrame = useMemo(() => { if (queue.length === 0) { return []; } return buildFramePhotos(queue, index + itemsPerFrame, itemsPerFrame); }, [index, itemsPerFrame, queue]); const intervalMs = resolveIntervalMs(settings, queue.length); useEffect(() => { if (queue.length === 0 || paused) { return undefined; } const timer = window.setInterval(() => { setIndex((prev) => (prev + itemsPerFrame) % queue.length); }, intervalMs); return () => window.clearInterval(timer); }, [intervalMs, itemsPerFrame, queue.length]); return { frame, layout, intervalMs, frameKey, nextFrame, }; }