From e41672da741b6ef9b37601bc84f97428d4b1f544 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Sun, 10 May 2026 22:03:45 -0700 Subject: type: add volume controls --- src/app/game/page.styles.ts | 35 +++++++++++++ src/app/game/page.tsx | 118 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 143 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/app/game/page.styles.ts b/src/app/game/page.styles.ts index a1c6cc8..d410339 100644 --- a/src/app/game/page.styles.ts +++ b/src/app/game/page.styles.ts @@ -427,6 +427,41 @@ export const OpacitySlider = styled.input` width: 100%; `; +export const PreviewWrap = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 8px; +`; + +export const PreviewBtn = styled.button` + width: 100%; + padding: 10px 16px; + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.08); + color: #ffffff; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; + + &:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.16); + } + + &:disabled { + opacity: 0.45; + cursor: not-allowed; + } +`; + +export const PreviewHint = styled.div` + font-size: 11px; + color: rgba(255, 255, 255, 0.45); + text-align: center; +`; + export const CountdownNumber = styled.div` font-size: 72px; font-weight: 900; diff --git a/src/app/game/page.tsx b/src/app/game/page.tsx index 5b5a782..7938d8f 100644 --- a/src/app/game/page.tsx +++ b/src/app/game/page.tsx @@ -45,6 +45,9 @@ import { OpacityLabel, OpacitySlider, OpacityValue, + PreviewWrap, + PreviewBtn, + PreviewHint, CompletedLineFade, GameFooter, ControlBtn, @@ -86,6 +89,7 @@ const isVideoUrl = (url: string) => { return !!ext && VIDEO_EXTENSIONS.has(ext); }; const BACKGROUND_OPACITY_KEY = "lrcType.backgroundOpacity"; +const AUDIO_VOLUME_KEY = "lrcType.audioVolume"; function GameInner() { const searchParams = useSearchParams(); @@ -130,6 +134,15 @@ function GameInner() { if (!Number.isFinite(parsed)) return 0; return Math.min(100, Math.max(0, parsed)); }); + const [audioVolume, setAudioVolume] = useState(() => { + if (typeof window === "undefined") return 100; + const stored = localStorage.getItem(AUDIO_VOLUME_KEY); + if (stored === null) return 100; + const parsed = Number(stored); + if (!Number.isFinite(parsed)) return 100; + return Math.min(100, Math.max(0, parsed)); + }); + const [isPreviewPlaying, setIsPreviewPlaying] = useState(false); const [skipBacking, setSkipBacking] = useState(false); const isVideo = useMemo(() => isVideoUrl(audioUrl), [audioUrl]); @@ -139,6 +152,39 @@ function GameInner() { localStorage.setItem(BACKGROUND_OPACITY_KEY, String(backgroundOpacity)); }, [backgroundOpacity]); + useEffect(() => { + localStorage.setItem(AUDIO_VOLUME_KEY, String(audioVolume)); + }, [audioVolume]); + + useEffect(() => { + const media = isVideo ? videoRef.current : audioRef.current; + if (!media) return; + media.volume = audioVolume / 100; + }, [audioVolume, isVideo, audioUrl]); + + useEffect(() => { + const media = isVideo ? videoRef.current : audioRef.current; + if (!media) { + setIsPreviewPlaying(false); + return; + } + const handlePlay = () => setIsPreviewPlaying(true); + const handlePause = () => setIsPreviewPlaying(false); + const handleEnded = () => setIsPreviewPlaying(false); + media.addEventListener("play", handlePlay); + media.addEventListener("pause", handlePause); + media.addEventListener("ended", handleEnded); + return () => { + media.removeEventListener("play", handlePlay); + media.removeEventListener("pause", handlePause); + media.removeEventListener("ended", handleEnded); + }; + }, [isVideo, audioUrl]); + + useEffect(() => { + setIsPreviewPlaying(false); + }, [audioUrl]); + const charRowRef = useRef(null); const charRefs = useRef<(HTMLSpanElement | null)[]>([]); const [wrapSpaceIndicators, setWrapSpaceIndicators] = useState([]); @@ -337,8 +383,12 @@ function GameInner() { } }; const onEnded = () => { - setPhase("finished"); - setGameDurationMs(Date.now() - gameStartTimeRef.current); + if (phaseRef.current === "playing") { + setPhase("finished"); + setGameDurationMs(Date.now() - gameStartTimeRef.current); + return; + } + setIsPreviewPlaying(false); }; media.addEventListener("timeupdate", onTimeUpdate); media.addEventListener("loadedmetadata", onLoadedMetadata); @@ -395,6 +445,23 @@ function GameInner() { } catch {} }, []); // eslint-disable-line react-hooks/exhaustive-deps + const handlePreviewToggle = useCallback(() => { + if (phase !== "idle") return; + const media = isVideo ? videoRef.current : audioRef.current; + if (!media || !audioUrl) return; + + if (media.paused) { + void media.play().catch(() => { + toast.error("Unable to start preview. Try interacting with the page again.", { + theme: "dark", + }); + }); + return; + } + + media.pause(); + }, [phase, isVideo, audioUrl]); + const handleStart = useCallback(() => { const media = isVideo ? videoRef.current : audioRef.current; if (!media || !lrcContent || !audioUrl) return; @@ -406,6 +473,7 @@ function GameInner() { lastHandledIdxRef.current = -1; media.pause(); media.currentTime = 0; + setIsPreviewPlaying(false); setPhase("countdown"); setCountdown(5); setGameDurationMs(0); @@ -440,6 +508,7 @@ function GameInner() { media.pause(); media.currentTime = 0; } + setIsPreviewPlaying(false); if (countdownIntervalRef.current !== null) { clearInterval(countdownIntervalRef.current); countdownIntervalRef.current = null; @@ -595,6 +664,43 @@ function GameInner() { )} + + {loadingLrc ? "Loading song..." : "▶ Start Game"} + + + + + Volume + {audioVolume}% + + setAudioVolume(Number(e.target.value))} + /> + + + + + {isPreviewPlaying ? "⏸ Pause Preview" : "▶ Preview Audio"} + + + {audioUrl + ? "Use preview to test your volume before starting." + : "Load a chart to enable audio preview."} + + + {isVideo && ( @@ -612,14 +718,6 @@ function GameInner() { /> )} - - - {loadingLrc ? "Loading song..." : "▶ Start Game"} -