feat: enhance schedule slot handling with episode details and duration calculation
This commit is contained in:
@@ -3,7 +3,12 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
export interface ScheduleSlot {
|
||||
id: string;
|
||||
/** Headline: series name for episodes, film title for everything else. */
|
||||
title: string;
|
||||
/** Secondary line: "S1 · E3 · Episode Title" for episodes, year for movies. */
|
||||
subtitle?: string | null;
|
||||
/** Rounded slot duration in minutes. */
|
||||
durationMins: number;
|
||||
startTime: string; // "HH:MM"
|
||||
endTime: string; // "HH:MM"
|
||||
isCurrent?: boolean;
|
||||
@@ -50,8 +55,17 @@ export function ScheduleOverlay({ channelName, slots }: ScheduleOverlayProps) {
|
||||
>
|
||||
{slot.title}
|
||||
</p>
|
||||
{slot.subtitle && (
|
||||
<p className={cn(
|
||||
"truncate text-xs leading-snug",
|
||||
slot.isCurrent ? "text-zinc-400" : "text-zinc-600"
|
||||
)}>
|
||||
{slot.subtitle}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-0.5 font-mono text-[10px] text-zinc-600">
|
||||
{slot.startTime} – {slot.endTime}
|
||||
{" · "}{slot.durationMins}m
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
NoSignal,
|
||||
} from "./components";
|
||||
import type { SubtitleTrack } from "./components/video-player";
|
||||
import { Maximize2, Minimize2, Volume2, VolumeX } from "lucide-react";
|
||||
import { Maximize2, Minimize2, Volume1, Volume2, VolumeX } from "lucide-react";
|
||||
import { useAuthContext } from "@/context/auth-context";
|
||||
import { useChannels, useCurrentBroadcast, useEpg } from "@/hooks/use-channels";
|
||||
import {
|
||||
@@ -76,12 +76,17 @@ export default function TvPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Volume / mute
|
||||
// Volume control
|
||||
const [volume, setVolume] = useState(1); // 0.0 – 1.0
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const [showVolumeSlider, setShowVolumeSlider] = useState(false);
|
||||
useEffect(() => {
|
||||
if (videoRef.current) videoRef.current.muted = isMuted;
|
||||
}, [isMuted]);
|
||||
if (!videoRef.current) return;
|
||||
videoRef.current.muted = isMuted;
|
||||
videoRef.current.volume = volume;
|
||||
}, [isMuted, volume]);
|
||||
const toggleMute = useCallback(() => setIsMuted((m) => !m), []);
|
||||
const VolumeIcon = isMuted || volume === 0 ? VolumeX : volume < 0.5 ? Volume1 : Volume2;
|
||||
|
||||
// Channel jump by number (e.g. press "1","4" → jump to ch 14 after 1.5 s)
|
||||
const [channelInput, setChannelInput] = useState("");
|
||||
@@ -137,10 +142,11 @@ export default function TvPage() {
|
||||
const resetIdle = useCallback(() => {
|
||||
setShowOverlays(true);
|
||||
if (idleTimer.current) clearTimeout(idleTimer.current);
|
||||
idleTimer.current = setTimeout(
|
||||
() => setShowOverlays(false),
|
||||
IDLE_TIMEOUT_MS,
|
||||
);
|
||||
idleTimer.current = setTimeout(() => {
|
||||
setShowOverlays(false);
|
||||
setShowVolumeSlider(false);
|
||||
setShowSubtitlePicker(false);
|
||||
}, IDLE_TIMEOUT_MS);
|
||||
// Resume playback if autoplay was blocked (e.g. on page refresh with no prior interaction)
|
||||
videoRef.current?.play().catch(() => {});
|
||||
}, []);
|
||||
@@ -395,15 +401,44 @@ export default function TvPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="pointer-events-auto rounded-md bg-black/50 p-1.5 text-zinc-400 backdrop-blur transition-colors hover:bg-black/70 hover:text-white"
|
||||
onClick={toggleMute}
|
||||
title={isMuted ? "Unmute [M]" : "Mute [M]"}
|
||||
>
|
||||
{isMuted
|
||||
? <VolumeX className="h-4 w-4" />
|
||||
: <Volume2 className="h-4 w-4" />}
|
||||
</button>
|
||||
{/* Volume control */}
|
||||
<div className="pointer-events-auto relative">
|
||||
<button
|
||||
className="rounded-md bg-black/50 p-1.5 text-zinc-400 backdrop-blur transition-colors hover:bg-black/70 hover:text-white"
|
||||
onClick={() => setShowVolumeSlider((s) => !s)}
|
||||
title="Volume"
|
||||
>
|
||||
<VolumeIcon className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{showVolumeSlider && (
|
||||
<div className="absolute right-0 top-9 z-30 w-36 rounded-lg border border-zinc-700 bg-zinc-900/95 p-3 shadow-xl backdrop-blur">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
value={isMuted ? 0 : Math.round(volume * 100)}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value) / 100;
|
||||
setVolume(v);
|
||||
setIsMuted(v === 0);
|
||||
}}
|
||||
className="w-full accent-white"
|
||||
/>
|
||||
<div className="mt-1.5 flex items-center justify-between">
|
||||
<button
|
||||
onClick={toggleMute}
|
||||
className="text-[10px] text-zinc-500 hover:text-zinc-300"
|
||||
>
|
||||
{isMuted ? "Unmute [M]" : "Mute [M]"}
|
||||
</button>
|
||||
<span className="font-mono text-[10px] text-zinc-500">
|
||||
{isMuted ? "0" : Math.round(volume * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="pointer-events-auto rounded-md bg-black/50 p-1.5 text-zinc-400 backdrop-blur transition-colors hover:bg-black/70 hover:text-white"
|
||||
|
||||
Reference in New Issue
Block a user