aboutsummaryrefslogtreecommitdiffstats
path: root/src/app/page.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/page.tsx')
-rw-r--r--src/app/page.tsx1133
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>
- );
-}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage