aboutsummaryrefslogtreecommitdiffstats
path: root/src/app
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2026-04-12 22:14:05 -0700
committerPinapelz <yukais@pinapelz.com>2026-04-12 22:14:05 -0700
commit8cfa334717481bc2fcfa716b546304924157abff (patch)
treec49dc578124991920ebc072be86275b71dee9795 /src/app
parent851d48fbf341e0f9f5f7d734666822976ab06a0e (diff)
make windows re-sizeable general lint/formatting
Diffstat (limited to 'src/app')
-rw-r--r--src/app/layout.tsx11
-rw-r--r--src/app/page.tsx925
-rw-r--r--src/app/registry.tsx26
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>
+ );
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage