feat: enhance schedule slot handling with episode details and duration calculation

This commit is contained in:
2026-03-11 22:30:05 +01:00
parent 0f1b9c11fe
commit ee64fc0b8a
3 changed files with 101 additions and 24 deletions

View File

@@ -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"