diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/layout.tsx | 11 | ||||
| -rw-r--r-- | src/app/page.tsx | 925 | ||||
| -rw-r--r-- | src/app/registry.tsx | 26 |
3 files changed, 572 insertions, 390 deletions
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7dbde38..66b7930 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,7 @@ -export const metadata = { +import type { Metadata } from "next"; +import StyledComponentsRegistry from "./registry"; + +export const metadata: Metadata = { title: "LRC-Karaoke Player", description: "A karaoke oriented media player with support for lyrics, subtitles, and offset adjustments!", @@ -11,7 +14,9 @@ export default function RootLayout({ }) { return ( <html lang="en"> - <body>{children}</body> + <body> + <StyledComponentsRegistry>{children}</StyledComponentsRegistry> + </body> </html> ); -} +}
\ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 6dba085..5051541 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,102 +1,334 @@ "use client"; -import React, { useEffect, useRef, useState } from "react"; -import styled from "styled-components"; +import React, { + useCallback, + useEffect, + useRef, + useState, + Suspense, +} from "react"; +import styled, { css } from "styled-components"; import LRCPlayer from "./components/LRCPlayer"; import { toast, ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; -import { FaPlay, FaPause } from "react-icons/fa"; +import { + FaPlay, + FaPause, + FaFileAlt, + FaVideo, + FaClosedCaptioning, + FaHeadphones, + FaSyncAlt, +} from "react-icons/fa"; import { CaptionsRenderer } from "react-srv3"; import { useSearchParams } from "next/navigation"; -// Styled components +// ─── Layout ────────────────────────────────────────────────────────────────── + const Root = styled.div` position: absolute; + inset: 0; + display: flex; + flex-direction: column; + background-color: #f5f5f5; + overflow: hidden; +`; + +const PanesContainer = styled.div` + display: flex; + flex: 1; + height: 100vh; + overflow: hidden; + user-select: none; +`; + +// ─── Lyrics pane ───────────────────────────────────────────────────────────── + +const LyricsPane = styled.div<{ $width: number }>` + width: ${({ $width }) => $width}%; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #ffffff; +`; + +// ─── Resize handle ─────────────────────────────────────────────────────────── + +const ResizeHandle = styled.div` + width: 5px; + flex-shrink: 0; + background-color: #ddd; + cursor: col-resize; + transition: background-color 0.15s ease; + position: relative; + + &:hover, + &:active { + background-color: #aaa; + } + + &::after { + content: ""; + position: absolute; + inset: 0 -4px; + } +`; + +const VideoPane = styled.div<{ $dragOver: boolean }>` + flex: 1; + position: relative; + background-color: ${({ $dragOver }) => ($dragOver ? "#dbeeff" : "#ffffff")}; + transition: background-color 0.15s ease; + overflow: hidden; +`; + +const VideoElement = styled.video` + position: absolute; + inset: 0; width: 100%; height: 100%; - top: 0; +`; + +const CaptionsOverlay = styled.div` + position: absolute; + inset: 0; + width: 90%; + height: 90%; + margin: auto; + cursor: pointer; +`; + +const ControlBar = styled.div` + position: absolute; + bottom: 0; left: 0; + width: 100%; + height: 50px; display: flex; - flex-direction: column; align-items: center; - background-color: #f5f5f5; + background-color: rgba(0, 0, 0, 0.6); + z-index: 10; `; -const FileInputContainer = styled.div` - margin-bottom: 20px; +const PlayButton = styled.button` + flex-shrink: 0; + width: 50px; + height: 50px; + padding: 0; + border: none; + background-color: transparent; + color: #fff; + cursor: pointer; display: flex; + align-items: center; justify-content: center; - gap: 20px; - padding: 10px; - border-radius: 5px; - background-color: #ffffff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + font-size: 15px; + transition: background-color 0.15s ease; + + &:hover { + background-color: rgba(255, 255, 255, 0.12); + } `; -const FileInput = styled.input` - padding: 10px 15px; - border-radius: 5px; - border: 1px solid #ddd; - justify-content: center; +const ScrubBar = styled.input` + flex: 1; + height: 4px; + margin: 0 12px 0 4px; cursor: pointer; - display: none; - font-family: Arial; - &:hover, + accent-color: #fff; +`; + +const ControlPanel = styled.div<{ $visible: boolean }>` + position: absolute; + bottom: 50px; + left: 0; + right: 0; + background: rgba(14, 14, 14, 0.88); + backdrop-filter: blur(8px); + border-top: 1px solid rgba(255, 255, 255, 0.08); + padding: 7px 12px; + display: flex; + flex-direction: column; + gap: 5px; + z-index: 9; + transform: translateY(${({ $visible }) => ($visible ? "0" : "6px")}); + opacity: ${({ $visible }) => ($visible ? 1 : 0)}; + pointer-events: ${({ $visible }) => ($visible ? "auto" : "none")}; + transition: + transform 0.18s ease, + opacity 0.18s ease; +`; + +const PanelRow = styled.div` + display: flex; + align-items: center; + gap: 5px; + flex-wrap: wrap; +`; + +const PanelDivider = styled.div` + width: 1px; + height: 20px; + background-color: rgba(255, 255, 255, 0.15); + flex-shrink: 0; + margin: 0 2px; +`; + +const panelItemStyles = css` + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 10px; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.15); + background-color: rgba(255, 255, 255, 0.07); + color: rgba(255, 255, 255, 0.85); + font-size: 12px; + font-family: Arial, sans-serif; + cursor: pointer; + white-space: nowrap; + transition: background-color 0.15s ease; + line-height: 1.4; + + &:hover { + background-color: rgba(255, 255, 255, 0.16); + } + &:focus { - background-color: #eaeaea; outline: none; + border-color: rgba(255, 255, 255, 0.35); } `; -const FileInputLabel = styled.label` - padding: 10px 15px; - border-radius: 5px; - border: 1px solid #ddd; - cursor: pointer; - &:hover, +const PanelLabel = styled.label` + ${panelItemStyles} +`; + +const PanelButton = styled.button` + ${panelItemStyles} +`; + +const HiddenFileInput = styled.input` + display: none; +`; + +const PanelFieldLabel = styled.span` + color: rgba(255, 255, 255, 0.45); + font-size: 11px; + font-family: Arial, sans-serif; + white-space: nowrap; +`; + +const PanelNumberInput = styled.input` + width: 68px; + padding: 3px 6px; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.15); + background-color: rgba(255, 255, 255, 0.08); + color: #fff; + font-size: 12px; + font-family: Arial, sans-serif; + &:focus { - background-color: #eaeaea; outline: none; + border-color: rgba(255, 255, 255, 0.4); + } + + /* Remove number input arrows in Firefox */ + -moz-appearance: textfield; + + /* Remove number input arrows in Chrome/Safari */ + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + opacity: 0.4; } `; -const ControlBarButton = styled.button` - padding: 10px 15px; - border-radius: 5px; - border: 1px solid #ddd; +const PanelRangeInput = styled.input` + width: 90px; + accent-color: rgba(255, 255, 255, 0.8); + cursor: pointer; + vertical-align: middle; +`; + +const PanelCheckboxLabel = styled.label` + display: inline-flex; align-items: center; + gap: 5px; + color: rgba(255, 255, 255, 0.8); + font-size: 12px; + font-family: Arial, sans-serif; cursor: pointer; - &:hover, - &:focus { - background-color: #eaeaea; - outline: none; - } + user-select: none; + white-space: nowrap; `; -const StyledLink = styled.a` - font-size: 20px; - font-family: Arial; - text-decoration: none; - color: black; - &:hover { - text-decoration: underline; - } +const ColorSwatch = styled.input` + width: 24px; + height: 24px; + padding: 1px; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 3px; + cursor: pointer; + background: none; + vertical-align: middle; `; -const LRCPlayerWrapper = styled.div` - flex: 1; +// ─── Placeholder (no video loaded) ─────────────────────────────────────────── + +const PlaceholderWrapper = styled.div` + width: 100%; + height: 100%; display: flex; flex-direction: column; - height: 100vh; - overflow-y: auto; - scroll-behavior: smooth; - background-color: #ffffff; + align-items: center; + justify-content: center; + padding: 24px; + box-sizing: border-box; + text-align: center; + font-family: Arial, sans-serif; +`; + +const PlaceholderHeading = styled.h1` + font-size: 28px; + font-weight: bold; + margin: 0 0 12px; +`; + +const PlaceholderBody = styled.p` + font-size: 18px; + line-height: 1.6; + margin: 0 0 20px; `; -const StyledButton = styled.button` - padding: 10px 15px; +const CodeInputWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + max-width: 420px; + font-family: Arial, sans-serif; + font-size: 14px; +`; + +const CodeInput = styled.input` + width: 100%; + font-size: 15px; + padding: 6px 8px; + border: 1px solid #ddd; + border-radius: 5px; + box-sizing: border-box; +`; + +const LoadButton = styled.button` + padding: 7px 14px; border-radius: 5px; border: 1px solid #ddd; + background-color: #fff; + font-family: Arial, sans-serif; + font-size: 13px; cursor: pointer; + transition: background-color 0.15s ease; + &:hover, &:focus { background-color: #eaeaea; @@ -104,15 +336,28 @@ const StyledButton = styled.button` } `; +// ─── Misc ──────────────────────────────────────────────────────────────────── + +const StyledLink = styled.a` + font-family: Arial, sans-serif; + text-decoration: none; + color: #0066cc; + + &:hover { + text-decoration: underline; + } +`; + +// ─── Component ─────────────────────────────────────────────────────────────── + function KaraokePage() { const [currentMillisecond, setCurrentMillisecond] = useState(0); const [lrcContent, setLrcContent] = useState<string>(""); const [videoUrl, setVideoUrl] = useState<string>(""); const [supplementAudioUrl, setSupplementAudioUrl] = useState<string>(""); const [isPlaying, setIsPlaying] = useState<boolean>(false); - const [showVolume, setShowVolume] = useState<boolean>(false); const [scrubValue, setScrubValue] = useState<number>(0); - const [showFileInputs, setShowFileInputs] = useState<boolean>(true); + const [showPanel, setShowPanel] = useState<boolean>(true); const videoRef = useRef<HTMLVideoElement>(null); const supplementAudioRef = useRef<HTMLAudioElement>(null); const [captionsText, setCaptionsText] = useState<string>(""); @@ -126,8 +371,15 @@ function KaraokePage() { const [supplementAudioOffset, setSupplementAudioOffset] = useState<number>(0); const [base64Input, setBase64Input] = useState<string>(""); + // Resizable panes + const [leftWidth, setLeftWidth] = useState<number>(50); + const isResizing = useRef(false); + const containerRef = useRef<HTMLDivElement>(null); + const searchParams = useSearchParams(); + // ── Persist color preferences ───────────────────────────────────────────── + useEffect(() => { const savedLrcColor = localStorage.getItem("lrcColor"); const savedFontColor = localStorage.getItem("fontColor"); @@ -143,14 +395,46 @@ function KaraokePage() { localStorage.setItem("fontColor", fontColor); }, [fontColor]); - // Functions for handling file input changes + // ── Resize logic ────────────────────────────────────────────────────────── + + const handleResizeMouseDown = useCallback( + (e: React.MouseEvent<HTMLDivElement>) => { + e.preventDefault(); + isResizing.current = true; + document.body.style.cursor = "col-resize"; + }, + [], + ); + + useEffect(() => { + const onMouseMove = (e: MouseEvent) => { + if (!isResizing.current || !containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + const newWidth = ((e.clientX - rect.left) / rect.width) * 100; + setLeftWidth(Math.min(Math.max(newWidth, 15), 85)); + }; + + const onMouseUp = () => { + if (!isResizing.current) return; + isResizing.current = false; + document.body.style.cursor = ""; + }; + + window.addEventListener("mousemove", onMouseMove); + window.addEventListener("mouseup", onMouseUp); + return () => { + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("mouseup", onMouseUp); + }; + }, []); + const handleLrcFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { const file = event.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { setLrcContent(e.target?.result as string); - if (videoUrl) setShowFileInputs(false); + if (videoUrl) setShowPanel(false); }; reader.readAsText(file); toast.success("LRC file loaded successfully", { autoClose: 2000 }); @@ -167,9 +451,7 @@ function KaraokePage() { setCurrentMillisecond(0); setScrubValue(0); setIsPlaying(false); - toast.success("Video file loaded successfully", { - autoClose: 2000, - }); + toast.success("Video file loaded successfully", { autoClose: 2000 }); } }; @@ -177,9 +459,7 @@ function KaraokePage() { const file = event.target.files?.[0]; if (file) { const reader = new FileReader(); - reader.onload = (e) => { - setCaptionsText(e.target?.result as string); - }; + reader.onload = (e) => setCaptionsText(e.target?.result as string); reader.readAsText(file); toast.success("SRV file loaded successfully", { autoClose: 2000 }); } @@ -197,7 +477,7 @@ function KaraokePage() { setScrubValue(0); setIsPlaying(false); if (video) video.pause(); - toast.success("Supplemental Audio file loaded successfully", { + toast.success("Supplemental audio file loaded successfully", { autoClose: 2000, }); } @@ -210,10 +490,8 @@ function KaraokePage() { setOffset(-1550); fetch( "https://utfs.io/f/e2e18ea7-9841-437b-9ca3-5723355bd41a-rlck46.lrc", - ).then(function (response) { - response.text().then(function (responseString) { - setLrcContent(responseString); - }); + ).then((response) => { + response.text().then((text) => setLrcContent(text)); }); setVideoUrl( "https://utfs.io/f/84f5dfa6-821d-407f-a16d-a685b09c11d9-7xx2h4.webm", @@ -222,79 +500,67 @@ function KaraokePage() { toast.success("Applied offset of -1550ms"); }; - // Side effects for keyboard shortcuts + // ── Keyboard shortcuts ──────────────────────────────────────────────────── + useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.code === "Space") { - handlePlayPause(); - } + if (e.code === "Space") handlePlayPause(); if (e.code === "ArrowRight") { if (document.activeElement?.tagName === "INPUT") return; const video = videoRef.current; - if (!video) return; - video.currentTime += 5; + if (video) video.currentTime += 5; } if (e.code === "ArrowLeft") { if (document.activeElement?.tagName === "INPUT") return; const video = videoRef.current; - if (!video) return; - video.currentTime -= 5; + if (video) video.currentTime -= 5; } }; document.addEventListener("keydown", handleKeyDown); - return () => { - document.removeEventListener("keydown", handleKeyDown); - }; + return () => document.removeEventListener("keydown", handleKeyDown); }); - // Side effects for the video itself useEffect(() => { const video = videoRef.current; if (!video) return; - const syncLrcWithVideo = () => { - setCurrentMillisecond(video.currentTime * 1000 + offset); // updates lrc position - setScrubValue((video.currentTime / video.duration) * 100); // update playhead position - }; - video.addEventListener("timeupdate", syncLrcWithVideo); - - return () => { - video.removeEventListener("timeupdate", syncLrcWithVideo); + const sync = () => { + setCurrentMillisecond(video.currentTime * 1000 + offset); + setScrubValue((video.currentTime / video.duration) * 100); }; + video.addEventListener("timeupdate", sync); + return () => video.removeEventListener("timeupdate", sync); }); - // Side effect for volume controls useEffect(() => { const video = videoRef.current; const audio = supplementAudioRef.current; if (!video || !audio) return; - if (balance < 0) { video.volume = 1 + balance; + audio.volume = 1; } else { video.volume = 1; audio.volume = 1 - balance; } }, [balance]); - // Side effect for audio useEffect(() => { const video = videoRef.current; const audio = supplementAudioRef.current; if (!video || !audio) return; - if (supplementAudioOffset === null || supplementAudioOffset == null) return; audio.currentTime = video.currentTime + supplementAudioOffset / 1000; }, [supplementAudioOffset]); - // General video control functionality - - const handleVolumeToggle = () => { - setShowVolume(!showVolume); - }; + useEffect(() => { + if (videoUrl && lrcContent) setStatusText("Ready to play!"); + else if (videoUrl) setStatusText("No lyrics file selected"); + else if (lrcContent) setStatusText("No video file selected"); + else setStatusText("No video or lyrics file selected"); + }, [videoUrl, lrcContent]); const handlePlayPause = () => { const video = videoRef.current; if (!video) return; - if (video.paused) { video.play(); if (supplementAudioUrl) supplementAudioRef.current?.play(); @@ -306,28 +572,12 @@ function KaraokePage() { } }; - // Status text styling depending on whats loaded. Not all visible - useEffect(() => { - if (videoUrl && lrcContent) { - setStatusText("Ready to play!"); - } else if (videoUrl) { - setStatusText("No lyrics file selected"); - } else if (lrcContent) { - setStatusText("No video file selected"); - } else { - setStatusText("No video or lyrics file selected"); - } - }, [videoUrl, lrcContent]); - - // Video Control Bar functionality const handleScrub = (event: React.ChangeEvent<HTMLInputElement>) => { const time = (parseFloat(event.target.value) / 100) * videoRef.current!.duration; videoRef.current!.currentTime = time; - if (supplementAudioOffset === null || supplementAudioOffset == null) { - supplementAudioRef.current!.currentTime = time; - } else { - supplementAudioRef.current!.currentTime = + if (supplementAudioRef.current) { + supplementAudioRef.current.currentTime = time + supplementAudioOffset / 1000; } setScrubValue(parseFloat(event.target.value)); @@ -342,11 +592,9 @@ function KaraokePage() { const video = videoRef.current; const audio = supplementAudioRef.current; if (!video || !audio) return; - if (supplementAudioOffset === null || supplementAudioOffset == null) return; audio.currentTime = video.currentTime + supplementAudioOffset / 1000; }; - // Handling drag and drop files const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => { setDragOver(true); event.preventDefault(); @@ -366,19 +614,19 @@ function KaraokePage() { event.preventDefault(); setDragOver(false); const file = event.dataTransfer.files?.[0]; + if (!file) return; + if (file.name.endsWith(".lrc")) { const reader = new FileReader(); reader.onload = (e) => { setLrcContent(e.target?.result as string); - if (videoUrl) setShowFileInputs(false); + if (videoUrl) setShowPanel(false); }; reader.readAsText(file); toast.success("LRC file loaded successfully", { autoClose: 2000 }); } else if (file.name.endsWith(".srv3")) { const reader = new FileReader(); - reader.onload = (e) => { - setCaptionsText(e.target?.result as string); - }; + reader.onload = (e) => setCaptionsText(e.target?.result as string); reader.readAsText(file); toast.success("SRV file loaded successfully", { autoClose: 2000 }); } else if (file.type.startsWith("video") || file.type.startsWith("audio")) { @@ -398,98 +646,79 @@ function KaraokePage() { function processData(data: any) { if (data.lrc) { fetch(data.lrc) - .then((response) => response.text()) + .then((r) => r.text()) .then((text) => { setLrcContent(text); - if (videoUrl) setShowFileInputs(false); - toast.success("LRC file loaded successfully", { - autoClose: 2000, - }); + if (videoUrl) setShowPanel(false); + toast.success("LRC file loaded successfully", { autoClose: 2000 }); }) - .catch((error) => { - toast.error("Failed to load LRC file", { autoClose: 2000 }); - }); + .catch(() => + toast.error("Failed to load LRC file", { autoClose: 2000 }), + ); } if (data.srv3) { fetch(data.srv3) - .then((response) => response.text()) + .then((r) => r.text()) .then((text) => { setCaptionsText(text); - toast.success("SRV file loaded successfully", { - autoClose: 2000, - }); + toast.success("SRV file loaded successfully", { autoClose: 2000 }); }) - .catch((error) => { - toast.error("Failed to load SRV3 file", { - autoClose: 2000, - }); - }); + .catch(() => + toast.error("Failed to load SRV3 file", { autoClose: 2000 }), + ); } if (data.file1) { setVideoUrl(data.file1); setCurrentMillisecond(0); setScrubValue(0); setIsPlaying(false); - toast.success("Video file loaded successfully", { - autoClose: 2000, - }); + toast.success("Video file loaded successfully", { autoClose: 2000 }); } if (data.file2) { setSupplementAudioUrl(data.file2); setCurrentMillisecond(0); setScrubValue(0); setIsPlaying(false); - toast.success("Supplemental Audio file loaded successfully", { + toast.success("Supplemental audio file loaded successfully", { autoClose: 2000, }); } - if (data.offset1) { - setOffset(Number(data.offset)); - } - if (data.offset2) { - setOffset(Number(data.offset2)); - } + if (data.offset1) setOffset(Number(data.offset)); + if (data.offset2) setSupplementAudioOffset(Number(data.offset2)); } - // Handle base64 input from user const handleKaraokeb64Code = () => { try { - const decodedString = atob(base64Input); - console.log(decodedString); - const data = JSON.parse(decodedString); + const data = JSON.parse(atob(base64Input)); processData(data); toast.success("Data loaded successfully", { autoClose: 2000 }); - } catch (e) { + } catch { toast.error("Invalid base64 or JSON data", { autoClose: 2000 }); } }; - // Check for query parameter useEffect(() => { const dataParam = searchParams.get("code"); if (dataParam) { try { - const decodedString = atob(dataParam); - const data = JSON.parse(decodedString); + const data = JSON.parse(atob(dataParam)); processData(data); - toast.success("Data loaded from query parameter", { - autoClose: 2000, - }); - } catch (e) { - toast.error("Invalid data in query parameter", { - autoClose: 2000, - }); + toast.success("Data loaded from query parameter", { autoClose: 2000 }); + } catch { + toast.error("Invalid data in query parameter", { autoClose: 2000 }); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams]); + // ── Render ──────────────────────────────────────────────────────────────── + return ( <Root> <ToastContainer /> - {/*LRC viewer*/} - <div style={{ display: "flex", width: "100%", height: "100vh" }}> - <LRCPlayerWrapper> + + <PanesContainer ref={containerRef}> + <LyricsPane $width={leftWidth}> <LRCPlayer lrc={lrcContent} currentMillisecond={currentMillisecond} @@ -497,17 +726,14 @@ function KaraokePage() { lrcColor={lrcColor} fontColor={fontColor} /> - </LRCPlayerWrapper> + </LyricsPane> - {/* Ternary operation for if videoUrl has been set */} - <div - style={{ - flex: 1, - position: "relative", - backgroundColor: dragOver ? "lightblue" : "white", - }} - onMouseEnter={() => setShowFileInputs(true)} - onMouseLeave={() => setShowFileInputs(false)} + <ResizeHandle onMouseDown={handleResizeMouseDown} /> + + <VideoPane + $dragOver={dragOver} + onMouseEnter={() => setShowPanel(true)} + onMouseLeave={() => setShowPanel(false)} onDragOver={handleDragOver} onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} @@ -515,277 +741,202 @@ function KaraokePage() { > {videoUrl ? ( <> - <video + <VideoElement ref={videoRef} src={videoUrl} - style={{ - position: "absolute", - width: "100%", - height: "100%", - }} onEnded={handleVideoEnded} /> <audio ref={supplementAudioRef} - src={supplementAudioUrl} + src={supplementAudioUrl || undefined} style={{ display: "none" }} /> - <div - style={{ - width: "90%", - height: "90%", - margin: "auto", - }} - onClick={() => handlePlayPause()} - > + <CaptionsOverlay onClick={handlePlayPause}> <CaptionsRenderer srv3={captionsText} currentTime={currentMillisecond / 1000} /> - </div> - {/*Video control bar*/} - <div - style={{ - position: "absolute", - bottom: 0, - left: 0, - width: "100%", - display: "flex", - alignItems: "center", - backgroundColor: "rgba(0, 0, 0, 0.5)", - height: "50px", - }} - > - <button + </CaptionsOverlay> + + <ControlBar> + <PlayButton onClick={handlePlayPause} - style={{ - width: "50px", - height: "50px", - padding: "10px", - border: "none", - borderRadius: "5px", - backgroundColor: "white", - color: "black", - }} + aria-label={isPlaying ? "Pause" : "Play"} > {isPlaying ? <FaPause /> : <FaPlay />} - </button> - <input + </PlayButton> + <ScrubBar type="range" min="0" max="100" value={scrubValue} - style={{ - flex: 1, - height: "50px", - width: "100%", - }} - onInput={handleScrub} + onChange={handleScrub} /> - </div> + </ControlBar> </> ) : ( - <div - style={{ - width: "100%", - height: "100%", - display: "flex", - justifyContent: "center", - alignItems: "center", - flexDirection: "column", - }} - > - <h1 - style={{ - fontFamily: "Arial", - fontWeight: "bold", - fontSize: "32px", - }} - > - {statusText} - </h1> - {/* Show a placeholder while no video selected */} - <p - style={{ - fontSize: "30px", - textAlign: "center", - fontFamily: "Arial", - }} - > - Please select the video and lrc (lyrics) file <br /> - (Drag and Drop them here, or use the menus below!) <br /> + <PlaceholderWrapper> + <PlaceholderHeading>{statusText}</PlaceholderHeading> + <PlaceholderBody> + Please select the video and lrc (lyrics) file + <br /> + (Drag and Drop them here, or use the menus below!) + <br /> <br /> Chrome is recommended! <br /> - <StyledLink href="/about"> About </StyledLink> + <StyledLink href="/about">About</StyledLink> + {" · "} <StyledLink href="" onClick={handleOnClickDemoButton}> - {" "} - Demo{" "} + Demo </StyledLink> - </p> - <div> + </PlaceholderBody> + + <CodeInputWrapper> <label htmlFor="base64Input"> or enter a MoekyunKaraoke code: </label> - <input + <CodeInput id="base64Input" type="text" value={base64Input} onChange={(e) => setBase64Input(e.target.value)} - style={{ width: "100%", fontSize: "16px" }} /> - <StyledButton onClick={handleKaraokeb64Code}> + <LoadButton onClick={handleKaraokeb64Code}> Load Data - </StyledButton> - </div> - </div> + </LoadButton> + </CodeInputWrapper> + </PlaceholderWrapper> )} - {/* File inputs, shown on hover over video div region*/} - {showFileInputs && ( - <FileInputContainer - style={{ - position: "absolute", - bottom: "30px", - left: 0, - }} - > - <FileInputLabel htmlFor="lrcUpload" style={{ cursor: "pointer" }}> - LRC - </FileInputLabel> - <FileInput + <ControlPanel $visible={showPanel}> + <PanelRow> + <PanelLabel htmlFor="lrcUpload"> + <FaFileAlt /> LRC + </PanelLabel> + <HiddenFileInput id="lrcUpload" type="file" accept=".lrc" onChange={handleLrcFileChange} /> - <FileInputLabel - htmlFor="videoUpload" - style={{ cursor: "pointer" }} - > - Media - </FileInputLabel> - <FileInput + + <PanelLabel htmlFor="videoUpload"> + <FaVideo /> Media + </PanelLabel> + <HiddenFileInput id="videoUpload" type="file" accept="video/*,audio/*" onChange={handleVideoFileChange} /> - <FileInputLabel htmlFor="srvUpload" style={{ cursor: "pointer" }}> - SRV - </FileInputLabel> - <FileInput + + <PanelLabel htmlFor="srvUpload"> + <FaClosedCaptioning /> SRV + </PanelLabel> + <HiddenFileInput id="srvUpload" type="file" accept=".srv3" onChange={handleSrvFileChange} /> - <FileInputLabel - htmlFor="supplementAudioUpload" - style={{ cursor: "pointer" }} - > - Audio #2 - </FileInputLabel> - <FileInput + + <PanelLabel htmlFor="supplementAudioUpload"> + <FaHeadphones /> Audio #2 + </PanelLabel> + <HiddenFileInput id="supplementAudioUpload" type="file" accept="audio/*" onChange={handleSupplementAudioFileChange} /> - <ControlBarButton onClick={syncSupplementAudioWithVideo}> - Sync Audio - </ControlBarButton> - <div - style={{ - display: "flex", - flexDirection: "column", - fontFamily: "Arial", - }} - > - <label>Audio/Video Balance</label> - <input - type="range" - min="-1" - max="1" - step="0.01" - value={balance} - onChange={(e) => setBalance(Number(e.target.value))} - /> - <label>Offset (±ms) </label> + + <PanelDivider /> + + <PanelButton onClick={syncSupplementAudioWithVideo}> + <FaSyncAlt /> Sync Audio + </PanelButton> + </PanelRow> + + <PanelRow> + <PanelFieldLabel>Balance</PanelFieldLabel> + <PanelRangeInput + type="range" + min="-1" + max="1" + step="0.01" + value={balance} + onChange={(e) => setBalance(Number(e.target.value))} + /> + + <PanelDivider /> + + <PanelFieldLabel>Offset</PanelFieldLabel> + <PanelNumberInput + type="number" + value={offset} + onChange={(e) => setOffset(Number(e.target.value))} + step="25" + /> + + <PanelDivider /> + + <PanelFieldLabel>Audio 2 Offset</PanelFieldLabel> + <PanelNumberInput + type="number" + value={supplementAudioOffset} + onChange={(e) => + setSupplementAudioOffset(Number(e.target.value)) + } + step="25" + /> + + <PanelDivider /> + + <PanelCheckboxLabel> <input - type="number" - style={{ fontSize: "14px" }} - id="numberInput" - value={offset} - onChange={(e) => setOffset(Number(e.target.value))} - step="25" + type="checkbox" + checked={animate} + onChange={(e) => setAnimate(e.target.checked)} /> - </div> - <div - style={{ - display: "flex", - flexDirection: "column", - fontFamily: "Arial", + Animate + </PanelCheckboxLabel> + + <PanelDivider /> + + <PanelFieldLabel>Colors</PanelFieldLabel> + <ColorSwatch + type="color" + value={lrcColor} + onChange={(e) => setLrcColor(e.target.value)} + title="Highlight colour" + /> + <ColorSwatch + type="color" + value={fontColor} + onChange={(e) => setFontColor(e.target.value)} + title="Font colour" + /> + <PanelButton + onClick={() => { + setLrcColor("#C8BEBE"); + setFontColor("#000000"); }} > - <label>Audio 2 Offset (±ms) </label> - <input - type="number" - style={{ fontSize: "14px" }} - id="numberInput" - value={supplementAudioOffset} - onChange={(e) => - setSupplementAudioOffset(Number(e.target.value)) - } - step="25" - /> - <label - style={{ - fontSize: "14px", - fontFamily: "Arial", - userSelect: "none", - }} - > - <input - type="checkbox" - checked={animate} - onChange={(e) => setAnimate(e.target.checked)} - onSelect={(e) => e.preventDefault()} - style={{ marginRight: "8px" }} - /> - Line Animation - </label> - <div - style={{ - display: "flex", - }} - > - <input - type="color" - value={lrcColor} - onChange={(e) => setLrcColor(e.target.value)} - /> - <input - type="color" - value={fontColor} - onChange={(e) => setFontColor(e.target.value)} - /> - <button - onClick={() => { - setLrcColor("#C8BEBE"); - setFontColor("#000000"); - }} - > - Reset - </button> - </div> - </div> - </FileInputContainer> - )} - </div> - </div> + Reset + </PanelButton> + </PanelRow> + </ControlPanel> + </VideoPane> + </PanesContainer> </Root> ); } -export default KaraokePage; +export default function Page() { + return ( + <Suspense> + <KaraokePage /> + </Suspense> + ); +} diff --git a/src/app/registry.tsx b/src/app/registry.tsx new file mode 100644 index 0000000..4a5a4e1 --- /dev/null +++ b/src/app/registry.tsx @@ -0,0 +1,26 @@ +"use client"; +import React, { useState } from "react"; +import { useServerInsertedHTML } from "next/navigation"; +import { ServerStyleSheet, StyleSheetManager } from "styled-components"; + +export default function StyledComponentsRegistry({ + children, +}: { + children: React.ReactNode; +}) { + const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet()); + + useServerInsertedHTML(() => { + const styles = styledComponentsStyleSheet.getStyleElement(); + styledComponentsStyleSheet.instance.clearTag(); + return <>{styles}</>; + }); + + if (typeof window !== "undefined") return <>{children}</>; + + return ( + <StyleSheetManager sheet={styledComponentsStyleSheet.instance}> + {children as React.ReactElement} + </StyleSheetManager> + ); +} |
