feat: implement configuration management and enhance user registration flow
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
NoSignal,
|
||||
} from "./components";
|
||||
import type { SubtitleTrack } from "./components/video-player";
|
||||
import { Maximize2, Minimize2, Volume2, VolumeX } from "lucide-react";
|
||||
import { useAuthContext } from "@/context/auth-context";
|
||||
import { useChannels, useCurrentBroadcast, useEpg } from "@/hooks/use-channels";
|
||||
import {
|
||||
@@ -59,6 +60,36 @@ export default function TvPage() {
|
||||
const [subtitleTracks, setSubtitleTracks] = useState<SubtitleTrack[]>([]);
|
||||
const [activeSubtitleTrack, setActiveSubtitleTrack] = useState(-1);
|
||||
const [showSubtitlePicker, setShowSubtitlePicker] = useState(false);
|
||||
|
||||
// Fullscreen
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
useEffect(() => {
|
||||
const handler = () => setIsFullscreen(!!document.fullscreenElement);
|
||||
document.addEventListener("fullscreenchange", handler);
|
||||
return () => document.removeEventListener("fullscreenchange", handler);
|
||||
}, []);
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch(() => {});
|
||||
} else {
|
||||
document.exitFullscreen().catch(() => {});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Volume / mute
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
useEffect(() => {
|
||||
if (videoRef.current) videoRef.current.muted = isMuted;
|
||||
}, [isMuted]);
|
||||
const toggleMute = useCallback(() => setIsMuted((m) => !m), []);
|
||||
|
||||
// Channel jump by number (e.g. press "1","4" → jump to ch 14 after 1.5 s)
|
||||
const [channelInput, setChannelInput] = useState("");
|
||||
const channelInputTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Touch-swipe state
|
||||
const touchStartY = useRef<number | null>(null);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Tick for live progress calculation (every 30 s is fine for the progress bar)
|
||||
@@ -169,12 +200,59 @@ export default function TvPage() {
|
||||
case "G":
|
||||
toggleSchedule();
|
||||
break;
|
||||
case "f":
|
||||
case "F":
|
||||
toggleFullscreen();
|
||||
break;
|
||||
case "m":
|
||||
case "M":
|
||||
toggleMute();
|
||||
break;
|
||||
default: {
|
||||
if (e.key >= "0" && e.key <= "9") {
|
||||
setChannelInput((prev) => {
|
||||
const next = prev + e.key;
|
||||
if (channelInputTimer.current) clearTimeout(channelInputTimer.current);
|
||||
channelInputTimer.current = setTimeout(() => {
|
||||
const num = parseInt(next, 10);
|
||||
if (num >= 1 && num <= Math.max(channelCount, 1)) {
|
||||
setChannelIdx(num - 1);
|
||||
resetIdle();
|
||||
}
|
||||
setChannelInput("");
|
||||
}, 1500);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKey);
|
||||
return () => window.removeEventListener("keydown", handleKey);
|
||||
}, [nextChannel, prevChannel, toggleSchedule]);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKey);
|
||||
if (channelInputTimer.current) clearTimeout(channelInputTimer.current);
|
||||
};
|
||||
}, [nextChannel, prevChannel, toggleSchedule, toggleFullscreen, toggleMute, channelCount, resetIdle]);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Touch swipe (swipe up = next channel, swipe down = prev channel)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
||||
touchStartY.current = e.touches[0].clientY;
|
||||
resetIdle();
|
||||
}, [resetIdle]);
|
||||
|
||||
const handleTouchEnd = useCallback((e: React.TouchEvent) => {
|
||||
if (touchStartY.current === null) return;
|
||||
const dy = touchStartY.current - e.changedTouches[0].clientY;
|
||||
touchStartY.current = null;
|
||||
if (Math.abs(dy) > 60) {
|
||||
if (dy > 0) nextChannel();
|
||||
else prevChannel();
|
||||
}
|
||||
}, [nextChannel, prevChannel]);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Stream error recovery
|
||||
@@ -258,6 +336,8 @@ export default function TvPage() {
|
||||
style={{ cursor: showOverlays ? "default" : "none" }}
|
||||
onMouseMove={resetIdle}
|
||||
onClick={resetIdle}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
{/* ── Base layer ─────────────────────────────────────────────── */}
|
||||
<div className="absolute inset-0">{renderBase()}</div>
|
||||
@@ -315,6 +395,26 @@ 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>
|
||||
|
||||
<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={toggleFullscreen}
|
||||
title={isFullscreen ? "Exit fullscreen [F]" : "Fullscreen [F]"}
|
||||
>
|
||||
{isFullscreen
|
||||
? <Minimize2 className="h-4 w-4" />
|
||||
: <Maximize2 className="h-4 w-4" />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="pointer-events-auto rounded-md bg-black/50 px-3 py-1.5 text-xs text-zinc-400 backdrop-blur transition-colors hover:bg-black/70 hover:text-white"
|
||||
onClick={toggleSchedule}
|
||||
@@ -369,6 +469,14 @@ export default function TvPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Channel number input overlay */}
|
||||
{channelInput && (
|
||||
<div className="pointer-events-none absolute left-1/2 top-1/2 z-30 -translate-x-1/2 -translate-y-1/2 rounded-xl bg-black/80 px-8 py-5 text-center backdrop-blur">
|
||||
<p className="mb-1 text-[10px] uppercase tracking-widest text-zinc-500">Channel</p>
|
||||
<p className="font-mono text-5xl font-bold text-white">{channelInput}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Schedule overlay — outside the fading div so it has its own visibility */}
|
||||
{showOverlays && showSchedule && (
|
||||
<div className="absolute bottom-4 right-4 top-14 z-20 w-80">
|
||||
|
||||
Reference in New Issue
Block a user