diff options
Diffstat (limited to 'src/app/page.tsx')
| -rw-r--r-- | src/app/page.tsx | 1133 |
1 files changed, 288 insertions, 845 deletions
diff --git a/src/app/page.tsx b/src/app/page.tsx index 7b03272..5db9c41 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,942 +1,385 @@ "use client"; -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, - FaFileAlt, - FaVideo, - FaClosedCaptioning, - FaHeadphones, - FaSyncAlt, -} from "react-icons/fa"; -import { CaptionsRenderer } from "react-srv3"; -import { useSearchParams } from "next/navigation"; +import React, { useState } from "react"; +import styled from "styled-components"; +import Link from "next/link"; +import { FaPlay, FaMusic, FaBars, FaSearch, FaUserCircle } from "react-icons/fa"; +import { MdLibraryMusic } from "react-icons/md"; -// ─── Layout ────────────────────────────────────────────────────────────────── const Root = styled.div` - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - background-color: #f5f5f5; - overflow: hidden; + min-height: 100vh; + background-color: #f9f9f9; + color: #1a1a1a; + font-family: "Roboto", "Segoe UI", Arial, sans-serif; `; -const PanesContainer = styled.div` +const Navbar = styled.nav` + position: sticky; + top: 0; + z-index: 100; display: flex; - flex: 1; - height: 100vh; - overflow: hidden; - user-select: none; + align-items: center; + justify-content: space-between; + height: 56px; + padding: 0 20px; + background-color: #ffffffee; + backdrop-filter: blur(12px); + border-bottom: 1px solid #e5e5e5; `; -// ─── Lyrics pane ───────────────────────────────────────────────────────────── - -const LyricsPane = styled.div<{ $width: number }>` - width: ${({ $width }) => $width}%; +const NavLeft = styled.div` display: flex; - flex-direction: column; - overflow: hidden; - background-color: #ffffff; + align-items: center; + gap: 14px; `; -// ─── 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; +const Logo = styled(Link)` + font-size: 17px; + font-weight: 800; + letter-spacing: 0.3px; + color: #1a1a1a; + text-decoration: none; + display: flex; + align-items: center; + gap: 7px; + user-select: none; +`; - &:hover, - &:active { - background-color: #aaa; - } +const LogoIcon = styled.span` + display: inline-flex; + align-items: center; + justify-content: center; + background-color: #1a1a1a; + color: #fff; + border-radius: 6px; + width: 30px; + height: 22px; + font-size: 10px; +`; - &::after { - content: ""; - position: absolute; - inset: 0 -4px; - } +const NavCenter = styled.div` + display: flex; + align-items: center; + flex: 0 1 560px; `; -const VideoPane = styled.div<{ $dragOver: boolean }>` +const SearchBox = styled.div` + display: flex; + align-items: center; flex: 1; - position: relative; - background-color: ${({ $dragOver }) => ($dragOver ? "#dbeeff" : "#ffffff")}; - transition: background-color 0.15s ease; + height: 38px; + border: 1px solid #d4d4d4; + border-radius: 10px; overflow: hidden; + background-color: #f0f0f0; + transition: border-color 0.2s; + &:focus-within { + border-color: #1a1a1a; + } `; -const VideoElement = styled.video` - position: absolute; - inset: 0; - width: 100%; +const SearchInput = styled.input` + flex: 1; height: 100%; + padding: 0 14px; + background: transparent; + border: none; + outline: none; + color: #1a1a1a; + font-size: 14px; + &::placeholder { + color: #909090; + } `; -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; - align-items: center; - background-color: rgba(0, 0, 0, 0.6); - z-index: 10; -`; - -const PlayButton = styled.button` - flex-shrink: 0; - width: 50px; - height: 50px; - padding: 0; +const SearchButton = styled.button` + width: 52px; + height: 100%; + background-color: #e8e8e8; border: none; - background-color: transparent; - color: #fff; + border-left: 1px solid #d4d4d4; + color: #606060; + font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; - font-size: 15px; - transition: background-color 0.15s ease; - &:hover { - background-color: rgba(255, 255, 255, 0.12); + background-color: #d4d4d4; + color: #1a1a1a; } `; -const ScrubBar = styled.input` - flex: 1; - height: 4px; - margin: 0 12px 0 4px; - cursor: pointer; - 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` +const NavRight = styled.div` display: flex; align-items: center; - gap: 5px; - flex-wrap: wrap; + gap: 6px; `; -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; +const Avatar = styled.div` + font-size: 28px; + color: #909090; + display: 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; - + padding: 4px; + border-radius: 50%; &:hover { - background-color: rgba(255, 255, 255, 0.16); - } - - &:focus { - outline: none; - border-color: rgba(255, 255, 255, 0.35); + color: #606060; } `; -const PanelLabel = styled.label` - ${panelItemStyles} -`; - -const PanelButton = styled.button` - ${panelItemStyles} -`; - -const HiddenFileInput = styled.input` - display: none; +const ChipsBar = styled.div` + display: flex; + align-items: center; + gap: 10px; + padding: 14px 24px; + overflow-x: auto; + background-color: #f9f9f9; + &::-webkit-scrollbar { + display: none; + } `; -const PanelFieldLabel = styled.span` - color: rgba(255, 255, 255, 0.45); - font-size: 11px; - font-family: Arial, sans-serif; +const Chip = styled.button<{ $active?: boolean }>` 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 { - outline: none; - border-color: rgba(255, 255, 255, 0.4); + padding: 7px 16px; + border-radius: 10px; + border: 1px solid ${(p) => (p.$active ? "transparent" : "#d4d4d4")}; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; + background-color: ${(p) => (p.$active ? "#1a1a1a" : "transparent")}; + color: ${(p) => (p.$active ? "#fff" : "#606060")}; + &:hover { + background-color: ${(p) => (p.$active ? "#333" : "#f0f0f0")}; + color: ${(p) => (p.$active ? "#fff" : "#1a1a1a")}; } +`; - /* Remove number input arrows in Firefox */ - -moz-appearance: textfield; +const GridContainer = styled.div` + padding: 8px 24px 24px; +`; - /* Remove number input arrows in Chrome/Safari */ - &::-webkit-inner-spin-button, - &::-webkit-outer-spin-button { - opacity: 0.4; - } +const CardGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; `; -const PanelRangeInput = styled.input` - width: 90px; - accent-color: rgba(255, 255, 255, 0.8); +const Card = styled.div` cursor: pointer; - vertical-align: middle; + border-radius: 14px; + transition: transform 0.15s, box-shadow 0.15s; + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); + } `; -const PanelCheckboxLabel = styled.label` - display: inline-flex; +const ThumbnailWrapper = styled.div` + width: 100%; + aspect-ratio: 16 / 9; + background-color: #e4e4e4; + border-radius: 12px; + display: flex; align-items: center; - gap: 5px; - color: rgba(255, 255, 255, 0.8); - font-size: 12px; - font-family: Arial, sans-serif; - cursor: pointer; - user-select: none; - white-space: nowrap; + justify-content: center; + color: #c0c0c0; + font-size: 36px; + overflow: hidden; + position: relative; `; -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 PlayOverlay = styled.div` + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0); + border-radius: 12px; + transition: background 0.2s; + ${Card}:hover & { + background: rgba(0, 0, 0, 0.25); + } `; -// ─── Placeholder (no video loaded) ─────────────────────────────────────────── - -const PlaceholderWrapper = styled.div` - width: 100%; - height: 100%; +const PlayCircle = styled.div` + width: 48px; + height: 48px; + border-radius: 50%; + background: rgba(0, 0, 0, 0.7); + color: #fff; display: flex; - flex-direction: column; align-items: center; justify-content: center; - padding: 24px; - box-sizing: border-box; - text-align: center; - font-family: Arial, sans-serif; + font-size: 16px; + opacity: 0; + transform: scale(0.8); + transition: opacity 0.2s, transform 0.2s; + ${Card}:hover & { + opacity: 1; + transform: scale(1); + } `; -const PlaceholderHeading = styled.h1` - font-size: 28px; - font-weight: bold; - margin: 0 0 12px; +const CardMeta = styled.div` + display: flex; + gap: 12px; + margin-top: 12px; + padding: 12px 4px; `; -const PlaceholderBody = styled.p` - font-size: 18px; - line-height: 1.6; - margin: 0 0 20px; -`; -const CodeInputWrapper = styled.div` +const CardInfo = styled.div` display: flex; + padding: 4px; flex-direction: column; - gap: 8px; - width: 100%; - max-width: 420px; - font-family: Arial, sans-serif; + gap: 3px; + min-width: 0; +`; + +const CardTitle = styled.span` font-size: 14px; + font-weight: 600; + color: #1a1a1a; + line-height: 1.35; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; `; -const CodeInput = styled.input` - width: 100%; - font-size: 15px; - padding: 6px 8px; - border: 1px solid #ddd; - border-radius: 5px; - box-sizing: border-box; +const CardSub = styled.span` + font-size: 12px; + color: #909090; + line-height: 1.3; `; -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; +/* ── CTA Section ── */ - &:hover, - &:focus { - background-color: #eaeaea; - outline: none; - } +const CtaSection = styled.div` + padding: 32px 24px; + border-top: 1px solid #e5e5e5; + margin-top: 8px; `; -// ─── Misc ──────────────────────────────────────────────────────────────────── +const SectionHeading = styled.h2` + font-size: 17px; + font-weight: 700; + color: #1a1a1a; + margin: 0 0 14px; +`; -const StyledLink = styled.a` - font-family: Arial, sans-serif; +const OpenPlayerLink = styled(Link)` + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 22px; + border-radius: 10px; + background-color: #1a1a1a; + color: #fff; + font-size: 14px; + font-weight: 600; text-decoration: none; - color: #0066cc; - + transition: background-color 0.15s; &:hover { - text-decoration: underline; + background-color: #333; } `; -// ─── 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 [scrubValue, setScrubValue] = useState<number>(0); - const [showPanel, setShowPanel] = useState<boolean>(true); - const videoRef = useRef<HTMLVideoElement>(null); - const supplementAudioRef = useRef<HTMLAudioElement>(null); - const [captionsText, setCaptionsText] = useState<string>(""); - const [offset, setOffset] = useState<number>(0); - const [dragOver, setDragOver] = useState<boolean>(false); - const [statusText, setStatusText] = useState<string>("No video selected"); - const [balance, setBalance] = useState<number>(0); - const [animate, setAnimate] = useState<boolean>(true); - const [lrcColor, setLrcColor] = useState<string>("#C8BEBE"); - const [fontColor, setFontColor] = useState<string>("#000000"); - 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"); - if (savedLrcColor) setLrcColor(savedLrcColor); - if (savedFontColor) setFontColor(savedFontColor); - }, []); - - useEffect(() => { - localStorage.setItem("lrcColor", lrcColor); - }, [lrcColor]); - - useEffect(() => { - localStorage.setItem("fontColor", fontColor); - }, [fontColor]); - - // ── 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) setShowPanel(false); - }; - reader.readAsText(file); - toast.success("LRC file loaded successfully", { autoClose: 2000 }); - } - }; - - const handleVideoFileChange = ( - event: React.ChangeEvent<HTMLInputElement>, - ) => { - const file = event.target.files?.[0]; - if (file) { - const url = URL.createObjectURL(file); - setVideoUrl(url); - setCurrentMillisecond(0); - setScrubValue(0); - setIsPlaying(false); - toast.success("Video file loaded successfully", { autoClose: 2000 }); - } - }; - - const handleSrvFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { - const file = event.target.files?.[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (e) => setCaptionsText(e.target?.result as string); - reader.readAsText(file); - toast.success("SRV file loaded successfully", { autoClose: 2000 }); - } - }; - - const handleSupplementAudioFileChange = ( - event: React.ChangeEvent<HTMLInputElement>, - ) => { - const file = event.target.files?.[0]; - const video = videoRef.current; - if (file) { - const url = URL.createObjectURL(file); - setSupplementAudioUrl(url); - setCurrentMillisecond(0); - setScrubValue(0); - setIsPlaying(false); - if (video) video.pause(); - toast.success("Supplemental audio file loaded successfully", { - autoClose: 2000, - }); - } - }; - - const handleOnClickDemoButton = ( - event: React.MouseEvent<HTMLAnchorElement, MouseEvent>, - ) => { - event.preventDefault(); - setOffset(-1550); - fetch( - "https://utfs.io/f/e2e18ea7-9841-437b-9ca3-5723355bd41a-rlck46.lrc", - ).then((response) => { - response.text().then((text) => setLrcContent(text)); - }); - setVideoUrl( - "https://utfs.io/f/84f5dfa6-821d-407f-a16d-a685b09c11d9-7xx2h4.webm", - ); - toast.success("Loading Demo: Mr.Raindrop - Amplified"); - toast.success("Applied offset of -1550ms"); - }; - - // ── Keyboard shortcuts ──────────────────────────────────────────────────── - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.code === "Space") handlePlayPause(); - if (e.code === "ArrowRight") { - if (document.activeElement?.tagName === "INPUT") return; - const video = videoRef.current; - if (video) video.currentTime += 5; - } - if (e.code === "ArrowLeft") { - if (document.activeElement?.tagName === "INPUT") return; - const video = videoRef.current; - if (video) video.currentTime -= 5; - } - }; - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }); - - useEffect(() => { - const video = videoRef.current; - if (!video) return; - const sync = () => { - setCurrentMillisecond(video.currentTime * 1000 + offset); - setScrubValue((video.currentTime / video.duration) * 100); - }; - video.addEventListener("timeupdate", sync); - return () => video.removeEventListener("timeupdate", sync); - }); - - 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]); - - useEffect(() => { - const video = videoRef.current; - const audio = supplementAudioRef.current; - if (!video || !audio) return; - audio.currentTime = video.currentTime + supplementAudioOffset / 1000; - }, [supplementAudioOffset]); - - 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(); - setIsPlaying(true); - } else { - video.pause(); - if (supplementAudioUrl) supplementAudioRef.current?.pause(); - setIsPlaying(false); - } - }; - - const handleScrub = (event: React.ChangeEvent<HTMLInputElement>) => { - const time = - (parseFloat(event.target.value) / 100) * videoRef.current!.duration; - videoRef.current!.currentTime = time; - if (supplementAudioRef.current) { - supplementAudioRef.current.currentTime = - time + supplementAudioOffset / 1000; - } - setScrubValue(parseFloat(event.target.value)); - }; - - const handleVideoEnded = () => { - setIsPlaying(false); - supplementAudioRef.current?.pause(); - }; - - const syncSupplementAudioWithVideo = () => { - const video = videoRef.current; - const audio = supplementAudioRef.current; - if (!video || !audio) return; - audio.currentTime = video.currentTime + supplementAudioOffset / 1000; - }; - - const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => { - setDragOver(true); - event.preventDefault(); - }; - - const handleDragEnter = (event: React.DragEvent<HTMLDivElement>) => { - setDragOver(true); - event.preventDefault(); - }; - - const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => { - setDragOver(false); - event.preventDefault(); - }; - - const handleDrop = (event: React.DragEvent<HTMLDivElement>) => { - 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) 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.readAsText(file); - toast.success("SRV file loaded successfully", { autoClose: 2000 }); - } else if (file.type.startsWith("video") || file.type.startsWith("audio")) { - const url = URL.createObjectURL(file); - setVideoUrl(url); - setCurrentMillisecond(0); - setScrubValue(0); - setIsPlaying(false); - toast.success("Video/Audio file loaded successfully", { - autoClose: 2000, - }); - } else { - toast.error("Unsupported file type", { autoClose: 2000 }); - } - }; +const PlayerDescription = styled.p` + font-size: 13px; + color: #909090; + margin: 14px 0 0; + line-height: 1.6; + max-width: 480px; +`; - function processData(data: any) { - if (data.lrc) { - fetch(data.lrc) - .then((r) => r.text()) - .then((text) => { - setLrcContent(text); - if (videoUrl) setShowPanel(false); - toast.success("LRC file loaded successfully", { autoClose: 2000 }); - }) - .catch(() => - toast.error("Failed to load LRC file", { autoClose: 2000 }), - ); - } - if (data.srv3) { - fetch(data.srv3) - .then((r) => r.text()) - .then((text) => { - setCaptionsText(text); - toast.success("SRV file loaded successfully", { 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 }); - } - if (data.file2) { - setSupplementAudioUrl(data.file2); - setCurrentMillisecond(0); - setScrubValue(0); - setIsPlaying(false); - toast.success("Supplemental audio file loaded successfully", { - autoClose: 2000, - }); - } - if (data.offset) setOffset(Number(data.offset)); - if (data.offset2) setSupplementAudioOffset(Number(data.offset2)); - } - const handleKaraokeb64Code = () => { - try { - const data = JSON.parse(atob(base64Input)); - processData(data); - toast.success("Data loaded successfully", { autoClose: 2000 }); - } catch { - toast.error("Invalid base64 or JSON data", { autoClose: 2000 }); - } - }; +const CHIPS = ["All", "Music", "Karaoke", "Live", "J-Pop", "Anime", "Vocaloid", "Recently added"]; - useEffect(() => { - const dataParam = searchParams.get("code"); - if (dataParam) { - try { - const data = JSON.parse(atob(dataParam)); - processData(data); - 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]); +const STUB_ITEMS = [ + { title: "Mr.Raindrop - Amplified (Full Karaoke)", artist: "Amplified", uploaded: "2 days ago" }, +]; - // ── Render ──────────────────────────────────────────────────────────────── +export default function HomePage() { + const [activeChip, setActiveChip] = useState("All"); return ( <Root> - <ToastContainer /> - - <PanesContainer ref={containerRef}> - <LyricsPane $width={leftWidth}> - <LRCPlayer - lrc={lrcContent} - currentMillisecond={currentMillisecond} - animate={animate} - lrcColor={lrcColor} - fontColor={fontColor} - /> - </LyricsPane> - - <ResizeHandle onMouseDown={handleResizeMouseDown} /> - - <VideoPane - $dragOver={dragOver} - onMouseEnter={() => setShowPanel(true)} - onMouseLeave={() => setShowPanel(false)} - onDragOver={handleDragOver} - onDragEnter={handleDragEnter} - onDragLeave={handleDragLeave} - onDrop={handleDrop} - > - {videoUrl ? ( - <> - <VideoElement - ref={videoRef} - src={videoUrl} - onEnded={handleVideoEnded} - /> - <audio - ref={supplementAudioRef} - src={supplementAudioUrl || undefined} - style={{ display: "none" }} - /> - <CaptionsOverlay onClick={handlePlayPause}> - <CaptionsRenderer - srv3={captionsText} - currentTime={currentMillisecond / 1000} - /> - </CaptionsOverlay> - - <ControlBar> - <PlayButton - onClick={handlePlayPause} - aria-label={isPlaying ? "Pause" : "Play"} - > - {isPlaying ? <FaPause /> : <FaPlay />} - </PlayButton> - <ScrubBar - type="range" - min="0" - max="100" - value={scrubValue} - onChange={handleScrub} - /> - </ControlBar> - </> - ) : ( - <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="" onClick={handleOnClickDemoButton}> - Demo - </StyledLink> - </PlaceholderBody> - - <CodeInputWrapper> - <label htmlFor="base64Input"> - or enter a MoekyunKaraoke code: - </label> - <CodeInput - id="base64Input" - type="text" - value={base64Input} - onChange={(e) => setBase64Input(e.target.value)} - /> - <LoadButton onClick={handleKaraokeb64Code}> - Load Data - </LoadButton> - </CodeInputWrapper> - </PlaceholderWrapper> - )} - - <ControlPanel $visible={showPanel}> - <PanelRow> - <PanelLabel htmlFor="lrcUpload"> - <FaFileAlt /> LRC - </PanelLabel> - <HiddenFileInput - id="lrcUpload" - type="file" - accept=".lrc" - onChange={handleLrcFileChange} - /> - - <PanelLabel htmlFor="videoUpload"> - <FaVideo /> Media - </PanelLabel> - <HiddenFileInput - id="videoUpload" - type="file" - accept="video/*,audio/*" - onChange={handleVideoFileChange} - /> + <Navbar> + <NavLeft> + <Logo href="/"> + <LogoIcon> + <MdLibraryMusic /> + </LogoIcon> + LRC-Karaoke-Player + </Logo> + </NavLeft> - <PanelLabel htmlFor="srvUpload"> - <FaClosedCaptioning /> SRV - </PanelLabel> - <HiddenFileInput - id="srvUpload" - type="file" - accept=".srv3" - onChange={handleSrvFileChange} - /> + <NavCenter> + <SearchBox> + <SearchInput placeholder="Search songs..." /> + <SearchButton aria-label="Search"> + <FaSearch /> + </SearchButton> + </SearchBox> + </NavCenter> - <PanelLabel htmlFor="supplementAudioUpload"> - <FaHeadphones /> Audio #2 - </PanelLabel> - <HiddenFileInput - id="supplementAudioUpload" - type="file" - accept="audio/*" - onChange={handleSupplementAudioFileChange} - /> + <NavRight> + <Avatar> + <FaUserCircle /> + </Avatar> + </NavRight> + </Navbar> - <PanelDivider /> + <ChipsBar> + {CHIPS.map((chip) => ( + <Chip + key={chip} + $active={chip === activeChip} + onClick={() => setActiveChip(chip)} + > + {chip} + </Chip> + ))} + </ChipsBar> - <PanelButton onClick={syncSupplementAudioWithVideo}> - <FaSyncAlt /> Sync Audio - </PanelButton> - </PanelRow> + <GridContainer> + <CardGrid> + {STUB_ITEMS.map((item) => ( + <Card key={item.title}> + <ThumbnailWrapper> + <FaMusic /> + <PlayOverlay> + <PlayCircle> + <FaPlay /> + </PlayCircle> + </PlayOverlay> + </ThumbnailWrapper> + <CardMeta> + <CardInfo> + <CardTitle>{item.title}</CardTitle> + <CardSub>{item.artist}</CardSub> + </CardInfo> + </CardMeta> + </Card> + ))} + </CardGrid> + </GridContainer> - <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="checkbox" - checked={animate} - onChange={(e) => setAnimate(e.target.checked)} - /> - 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"); - }} - > - Reset - </PanelButton> - </PanelRow> - </ControlPanel> - </VideoPane> - </PanesContainer> + <CtaSection> + <SectionHeading>Custom Player</SectionHeading> + <OpenPlayerLink href="/player"> + <FaPlay /> Open Player + </OpenPlayerLink> + <PlayerDescription> + Load your own video, audio, LRC lyrics + </PlayerDescription> + </CtaSection> </Root> ); } - -export default function Page() { - return ( - <Suspense> - <KaraokePage /> - </Suspense> - ); -} |
