import React, { useState, useEffect, useCallback } from 'react'; import { Badge, Button, Divider, Flex, Group, Image, Modal, Stack, Text, Title, } from '@mantine/core'; import { Calendar, Video } from 'lucide-react'; import API from '../api'; import useVideoStore from '../store/useVideoStore'; import useSettingsStore from '../store/settings'; import { getShowVideoUrl } from '../utils/cards/RecordingCardUtils'; import { formatSeasonEpisode } from '../utils/guideUtils'; import { format, initializeTime, diff, useDateTimeFormat, } from '../utils/dateTimeUtils'; import { imdbUrl, tmdbUrl } from '../utils/externalUrls'; const overlayProps = { color: '#000', backgroundOpacity: 0.55, blur: 0 }; function formatDurationMinutes(startTime, endTime) { if (!startTime || !endTime) return null; const start = initializeTime(startTime); const end = initializeTime(endTime); const minutes = diff(end, start, 'minute'); if (minutes >= 60) { const hours = Math.floor(minutes / 60); const mins = minutes % 60; return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; } return `${minutes}m`; } function resolveApiUrl(url) { if (!url || url.startsWith('http')) return url; const apiHost = import.meta.env.DEV ? `http://${window.location.hostname}:5656` : ''; return `${apiHost}${url}`; } function resolveImageUrl(detail) { if (detail?.tmdb_poster_url) return detail.tmdb_poster_url; if (detail?.poster_url) return resolveApiUrl(detail.poster_url); if (detail?.images?.length > 0) return resolveApiUrl(detail.images[0].url); if (detail?.icon) return resolveApiUrl(detail.icon); return null; } function formatCredits(actors) { if (!actors?.length) return null; return actors .map((a) => (a.role ? `${a.name} (${a.role})` : a.name)) .join(', '); } export default function ProgramDetailModal({ program, channel, opened, onClose, onRecord, }) { const [detailData, setDetailData] = useState(null); const showVideo = useVideoStore((s) => s.showVideo); const env_mode = useSettingsStore((s) => s.environment.env_mode); const { timeFormat } = useDateTimeFormat(); useEffect(() => { if (!opened || !program) { setDetailData(null); return; } // Dummy programs may use UUID-style IDs that aren't real DB PKs const programId = program.id; if (!programId || typeof programId === 'string') { setDetailData(null); return; } let cancelled = false; API.getProgramDetail(programId) .then((data) => { if (!cancelled) setDetailData(data); }) .catch(() => { if (!cancelled) setDetailData(null); }); return () => { cancelled = true; }; }, [opened, program?.id]); const handleWatchLive = useCallback(() => { if (!channel) return; showVideo(getShowVideoUrl(channel, env_mode), 'live', { name: channel.name, }); onClose(); }, [channel, env_mode, showVideo, onClose]); const handleRecord = useCallback(() => { if (onRecord) onRecord(program); }, [onRecord, program]); if (!program) return null; // Merge detail data with grid data (detail enriches, grid is baseline) const d = detailData || {}; const seasonEpisodeLabel = formatSeasonEpisode( d.season ?? program.season, d.episode ?? program.episode ); const hasBadges = seasonEpisodeLabel || program.is_live || program.is_new || d.is_previously_shown || program.is_premiere || program.is_finale || d.video_quality || d.rating; const categories = d.categories || []; const credits = d.credits || {}; const hasCredits = credits.actors?.length > 0 || credits.directors?.length > 0 || credits.writers?.length > 0; const starRatings = d.star_ratings || []; const description = d.description || program.description; const subtitle = d.sub_title ?? program.sub_title; const posterUrl = resolveImageUrl(d) || program?.custom_properties?.icon || null; const duration = formatDurationMinutes(program.start_time, program.end_time); const programStart = initializeTime(program.start_time || program.startMs); const programEnd = initializeTime(program.end_time || program.endMs); return ( {channel.channel_number ? `${channel.channel_number} - ` : ''} {channel.name} ) : null } size="lg" centered overlayProps={overlayProps} zIndex={9999} > {posterUrl && ( { e.currentTarget.style.display = 'none'; }} /> )} {program.title} {subtitle && ( {subtitle} )} {hasBadges && ( {program.is_live && ( LIVE )} {program.is_new && ( NEW )} {d.is_previously_shown && ( RERUN )} {program.is_premiere && ( PREMIERE )} {program.is_finale && ( FINALE )} {seasonEpisodeLabel && ( {seasonEpisodeLabel} )} {d.rating && ( {d.rating} )} {d.video_quality && ( {d.video_quality} )} )} {format(programStart, timeFormat)} –{' '} {format(programEnd, timeFormat)} {duration && ( <> · {duration} )} {categories.length > 0 && ( <> · {categories.join(', ')} )} {program.isLive && channel && ( )} {!program.isPast && ( )} {description && ( <> {description} )} {d.content_advisory?.length > 0 && ( {d.content_advisory.join(', ')} )} {d.event_details && ( <> {d.event_details.venue100 && ( Venue: {d.event_details.venue100} )} {d.event_details.teams?.length > 0 && ( Teams: {d.event_details.teams.map((t, i) => ( {i > 0 ? ' vs ' : ''} {t.name}{t.isHome ? ' (Home)' : ''} ))} )} )} {hasCredits && ( <> {credits.actors?.length > 0 && ( Cast:{' '} {formatCredits(credits.actors)} )} {credits.directors?.length > 0 && ( Director:{' '} {credits.directors.join(', ')} )} {credits.writers?.length > 0 && ( Writer:{' '} {credits.writers.join(', ')} )} )} {(d.language || d.original_air_date || (d.production_date && d.is_previously_shown) || starRatings.length > 0) && ( <> {d.language && ( Language:{' '} {d.language} )} {d.production_date && d.is_previously_shown && ( First aired:{' '} {d.production_date} )} {d.original_air_date && ( Original Air:{' '} {d.original_air_date} )} {starRatings.map((sr, i) => ( ★ {sr.value} {sr.system ? ` (${sr.system})` : ''} ))} )} {(d.imdb_id || d.tmdb_id) && ( {d.imdb_id && ( IMDb ↗ )} {d.tmdb_id && ( TMDB ↗ )} )} ); }