aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-01-07 01:10:06 -0800
committerPinapelz <yukais@pinapelz.com>2025-01-07 01:10:06 -0800
commitd2637f982fa00e034d3dbb7c5aa4d03118bf73c3 (patch)
treeb9f054e48ec8e66c23e7dbedaebb7da7aa69d721
parent0c4520544bb5ecc63e833f5e344cf8cd071854cb (diff)
add prisma ORM and schema for DBbrowser
-rw-r--r--.gitignore1
-rw-r--r--package.json2
-rw-r--r--pnpm-lock.yaml74
-rw-r--r--prisma/schema.prisma20
-rw-r--r--src/app/globals.css2
-rw-r--r--src/app/page.tsx843
-rw-r--r--src/app/player/page.tsx828
7 files changed, 992 insertions, 778 deletions
diff --git a/.gitignore b/.gitignore
index fd3dbb5..8585166 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+.env
diff --git a/package.json b/package.json
index d0724b0..9f68a76 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"lint": "next lint"
},
"dependencies": {
+ "@prisma/client": "^6.1.0",
"font-awesome": "^4.7.0",
"next": "14.0.2",
"react": "^18.3.1",
@@ -28,6 +29,7 @@
"eslint": "^9.0.0",
"eslint-config-next": "14.0.2",
"postcss": "^8.4.49",
+ "prisma": "^6.1.0",
"typescript": "^5.7.2"
},
"browser": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 29dd924..8c96f02 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ '@prisma/client':
+ specifier: ^6.1.0
+ version: 6.1.0(prisma@6.1.0)
font-awesome:
specifier: ^4.7.0
version: 4.7.0
@@ -60,6 +63,9 @@ importers:
postcss:
specifier: ^8.4.49
version: 8.4.49
+ prisma:
+ specifier: ^6.1.0
+ version: 6.1.0
typescript:
specifier: ^5.7.2
version: 5.7.2
@@ -212,6 +218,30 @@ packages:
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
+ '@prisma/client@6.1.0':
+ resolution: {integrity: sha512-AbQYc5+EJKm1Ydfq3KxwcGiy7wIbm4/QbjCKWWoNROtvy7d6a3gmAGkKjK0iUCzh+rHV8xDhD5Cge8ke/kiy5Q==}
+ engines: {node: '>=18.18'}
+ peerDependencies:
+ prisma: '*'
+ peerDependenciesMeta:
+ prisma:
+ optional: true
+
+ '@prisma/debug@6.1.0':
+ resolution: {integrity: sha512-0himsvcM4DGBTtvXkd2Tggv6sl2JyUYLzEGXXleFY+7Kp6rZeSS3hiTW9mwtUlXrwYbJP6pwlVNB7jYElrjWUg==}
+
+ '@prisma/engines-version@6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959':
+ resolution: {integrity: sha512-PdJqmYM2Fd8K0weOOtQThWylwjsDlTig+8Pcg47/jszMuLL9iLIaygC3cjWJLda69siRW4STlCTMSgOjZzvKPQ==}
+
+ '@prisma/engines@6.1.0':
+ resolution: {integrity: sha512-GnYJbCiep3Vyr1P/415ReYrgJUjP79fBNc1wCo7NP6Eia0CzL2Ot9vK7Infczv3oK7JLrCcawOSAxFxNFsAERQ==}
+
+ '@prisma/fetch-engine@6.1.0':
+ resolution: {integrity: sha512-asdFi7TvPlEZ8CzSZ/+Du5wZ27q6OJbRSXh+S8ISZguu+S9KtS/gP7NeXceZyb1Jv1SM1S5YfiCv+STDsG6rrg==}
+
+ '@prisma/get-platform@6.1.0':
+ resolution: {integrity: sha512-ia8bNjboBoHkmKGGaWtqtlgQOhCi7+f85aOkPJKgNwWvYrT6l78KgojLekE8zMhVk0R9lWcifV0Pf8l3/15V0Q==}
+
'@react-aria/ssr@3.9.7':
resolution: {integrity: sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==}
engines: {node: '>= 12'}
@@ -757,6 +787,11 @@ packages:
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@@ -1186,6 +1221,11 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
+ prisma@6.1.0:
+ resolution: {integrity: sha512-aFI3Yi+ApUxkwCJJwyQSwpyzUX7YX3ihzuHNHOyv4GJg3X5tQsmRaJEnZ+ZyfHpMtnyahhmXVfbTZ+lS8ZtfKw==}
+ engines: {node: '>=18.18'}
+ hasBin: true
+
prop-types-extra@1.1.1:
resolution: {integrity: sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==}
peerDependencies:
@@ -1657,6 +1697,31 @@ snapshots:
'@popperjs/core@2.11.8': {}
+ '@prisma/client@6.1.0(prisma@6.1.0)':
+ optionalDependencies:
+ prisma: 6.1.0
+
+ '@prisma/debug@6.1.0': {}
+
+ '@prisma/engines-version@6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959': {}
+
+ '@prisma/engines@6.1.0':
+ dependencies:
+ '@prisma/debug': 6.1.0
+ '@prisma/engines-version': 6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959
+ '@prisma/fetch-engine': 6.1.0
+ '@prisma/get-platform': 6.1.0
+
+ '@prisma/fetch-engine@6.1.0':
+ dependencies:
+ '@prisma/debug': 6.1.0
+ '@prisma/engines-version': 6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959
+ '@prisma/get-platform': 6.1.0
+
+ '@prisma/get-platform@6.1.0':
+ dependencies:
+ '@prisma/debug': 6.1.0
+
'@react-aria/ssr@3.9.7(react@18.3.1)':
dependencies:
'@swc/helpers': 0.5.15
@@ -2377,6 +2442,9 @@ snapshots:
fs.realpath@1.0.0: {}
+ fsevents@2.3.3:
+ optional: true
+
function-bind@1.1.2: {}
function.prototype.name@1.1.8:
@@ -2826,6 +2894,12 @@ snapshots:
prelude-ls@1.2.1: {}
+ prisma@6.1.0:
+ dependencies:
+ '@prisma/engines': 6.1.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
prop-types-extra@1.1.1(react@18.3.1):
dependencies:
react: 18.3.1
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 0000000..dc9edf7
--- /dev/null
+++ b/prisma/schema.prisma
@@ -0,0 +1,20 @@
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+model KaraokeSong {
+ id Int @id @default(autoincrement())
+ title String @map("title")
+ artist String @map("artist")
+ linkToMediaFile1 String @map("link_to_media_file_1")
+ linkToMediaFile2 String? @map("link_to_media_file_2")
+ linkToLrc String @map("link_to_lrc")
+ linkToSrv3 String @map("link_to_srv3")
+ thumbnail String? @map("thumbnail")
+ @@map("karaoke_songs")
+}
diff --git a/src/app/globals.css b/src/app/globals.css
deleted file mode 100644
index 139597f..0000000
--- a/src/app/globals.css
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 6dba085..bd3546e 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,791 +1,82 @@
"use client";
-import React, { useEffect, useRef, useState } from "react";
+import React from "react";
import styled 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 { CaptionsRenderer } from "react-srv3";
-import { useSearchParams } from "next/navigation";
-// Styled components
-const Root = styled.div`
- position: absolute;
- width: 100%;
- height: 100%;
- top: 0;
- left: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- background-color: #f5f5f5;
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 20px;
`;
-const FileInputContainer = styled.div`
- margin-bottom: 20px;
- display: flex;
- justify-content: center;
- gap: 20px;
- padding: 10px;
- border-radius: 5px;
- background-color: #ffffff;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+const KaraokeList = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 20px;
+ width: 100%;
+ padding: 20px;
`;
-const FileInput = styled.input`
- padding: 10px 15px;
- border-radius: 5px;
- border: 1px solid #ddd;
- justify-content: center;
- cursor: pointer;
- display: none;
- font-family: Arial;
- &:hover,
- &:focus {
- background-color: #eaeaea;
- outline: none;
- }
+const KaraokeItem = styled.div`
+ padding: 10px;
+ border: 1px solid #ccc;
+ display: flex;
+ flex-direction: column;
`;
-const FileInputLabel = styled.label`
- padding: 10px 15px;
- border-radius: 5px;
- border: 1px solid #ddd;
- cursor: pointer;
- &:hover,
- &:focus {
- background-color: #eaeaea;
- outline: none;
- }
+const KaraokeTitle = styled.h2`
+ font-size: 1.5em;
+ color: #333;
`;
-const ControlBarButton = styled.button`
- padding: 10px 15px;
- border-radius: 5px;
- border: 1px solid #ddd;
- align-items: center;
- cursor: pointer;
- &:hover,
- &:focus {
- background-color: #eaeaea;
- outline: none;
- }
+const KaraokeDescription = styled.p`
+ font-size: 1em;
+ color: #666;
`;
-const StyledLink = styled.a`
- font-size: 20px;
- font-family: Arial;
- text-decoration: none;
- color: black;
- &:hover {
- text-decoration: underline;
- }
-`;
-
-const LRCPlayerWrapper = styled.div`
- flex: 1;
- display: flex;
- flex-direction: column;
- height: 100vh;
- overflow-y: auto;
- scroll-behavior: smooth;
- background-color: #ffffff;
-`;
-
-const StyledButton = styled.button`
- padding: 10px 15px;
- border-radius: 5px;
- border: 1px solid #ddd;
- cursor: pointer;
- &:hover,
- &:focus {
- background-color: #eaeaea;
- outline: none;
- }
-`;
-
-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 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>("");
-
- const searchParams = useSearchParams();
-
- 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]);
-
- // Functions for handling file input changes
- 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);
- };
- 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(function (response) {
- response.text().then(function (responseString) {
- setLrcContent(responseString);
- });
- });
- setVideoUrl(
- "https://utfs.io/f/84f5dfa6-821d-407f-a16d-a685b09c11d9-7xx2h4.webm",
+const Homepage: React.FC = () => {
+ return (
+ <Container>
+ <KaraokeList>
+ <KaraokeItem>
+ <KaraokeTitle>Sample Video 1</KaraokeTitle>
+ <KaraokeDescription>
+ This is a description for sample video 1.
+ </KaraokeDescription>
+ </KaraokeItem>
+ <KaraokeItem>
+ <KaraokeTitle>Sample Video 2</KaraokeTitle>
+ <KaraokeDescription>
+ This is a description for sample video 2.
+ </KaraokeDescription>
+ </KaraokeItem>
+ <KaraokeItem>
+ <KaraokeTitle>Sample Video 2</KaraokeTitle>
+ <KaraokeDescription>
+ This is a description for sample video 2.
+ </KaraokeDescription>
+ </KaraokeItem>
+ <KaraokeItem>
+ <KaraokeTitle>Sample Video 2</KaraokeTitle>
+ <KaraokeDescription>
+ This is a description for sample video 2.
+ </KaraokeDescription>
+ </KaraokeItem>
+ <KaraokeItem>
+ <KaraokeTitle>Sample Video 2</KaraokeTitle>
+ <KaraokeDescription>
+ This is a description for sample video 2.
+ </KaraokeDescription>
+ </KaraokeItem>
+ <KaraokeItem>
+ <KaraokeTitle>Sample Video 2</KaraokeTitle>
+ <KaraokeDescription>
+ This is a description for sample video 2.
+ </KaraokeDescription>
+ </KaraokeItem>
+ </KaraokeList>
+ </Container>
);
- toast.success("Loading Demo: Mr.Raindrop - Amplified");
- toast.success("Applied offset of -1550ms");
- };
-
- // Side effects for 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) return;
- video.currentTime += 5;
- }
- if (e.code === "ArrowLeft") {
- if (document.activeElement?.tagName === "INPUT") return;
- const video = videoRef.current;
- if (!video) return;
- video.currentTime -= 5;
- }
- };
- document.addEventListener("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);
- };
- });
-
- // 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;
- } 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);
- };
-
- 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);
- }
- };
-
- // 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 =
- 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;
- 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();
- };
-
- 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.name.endsWith(".lrc")) {
- const reader = new FileReader();
- reader.onload = (e) => {
- setLrcContent(e.target?.result as string);
- if (videoUrl) setShowFileInputs(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 });
- }
- };
-
- function processData(data: any) {
- if (data.lrc) {
- fetch(data.lrc)
- .then((response) => response.text())
- .then((text) => {
- setLrcContent(text);
- if (videoUrl) setShowFileInputs(false);
- toast.success("LRC file loaded successfully", {
- autoClose: 2000,
- });
- })
- .catch((error) => {
- toast.error("Failed to load LRC file", { autoClose: 2000 });
- });
- }
- if (data.srv3) {
- fetch(data.srv3)
- .then((response) => response.text())
- .then((text) => {
- setCaptionsText(text);
- toast.success("SRV file loaded successfully", {
- autoClose: 2000,
- });
- })
- .catch((error) => {
- 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.offset1) {
- setOffset(Number(data.offset));
- }
- if (data.offset2) {
- setOffset(Number(data.offset2));
- }
- }
-
- // Handle base64 input from user
- const handleKaraokeb64Code = () => {
- try {
- const decodedString = atob(base64Input);
- console.log(decodedString);
- const data = JSON.parse(decodedString);
- processData(data);
- toast.success("Data loaded successfully", { autoClose: 2000 });
- } catch (e) {
- 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);
- processData(data);
- toast.success("Data loaded from query parameter", {
- autoClose: 2000,
- });
- } catch (e) {
- toast.error("Invalid data in query parameter", {
- autoClose: 2000,
- });
- }
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [searchParams]);
-
- return (
- <Root>
- <ToastContainer />
- {/*LRC viewer*/}
- <div style={{ display: "flex", width: "100%", height: "100vh" }}>
- <LRCPlayerWrapper>
- <LRCPlayer
- lrc={lrcContent}
- currentMillisecond={currentMillisecond}
- animate={animate}
- lrcColor={lrcColor}
- fontColor={fontColor}
- />
- </LRCPlayerWrapper>
-
- {/* Ternary operation for if videoUrl has been set */}
- <div
- style={{
- flex: 1,
- position: "relative",
- backgroundColor: dragOver ? "lightblue" : "white",
- }}
- onMouseEnter={() => setShowFileInputs(true)}
- onMouseLeave={() => setShowFileInputs(false)}
- onDragOver={handleDragOver}
- onDragEnter={handleDragEnter}
- onDragLeave={handleDragLeave}
- onDrop={handleDrop}
- >
- {videoUrl ? (
- <>
- <video
- ref={videoRef}
- src={videoUrl}
- style={{
- position: "absolute",
- width: "100%",
- height: "100%",
- }}
- onEnded={handleVideoEnded}
- />
- <audio
- ref={supplementAudioRef}
- src={supplementAudioUrl}
- style={{ display: "none" }}
- />
- <div
- style={{
- width: "90%",
- height: "90%",
- margin: "auto",
- }}
- 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
- onClick={handlePlayPause}
- style={{
- width: "50px",
- height: "50px",
- padding: "10px",
- border: "none",
- borderRadius: "5px",
- backgroundColor: "white",
- color: "black",
- }}
- >
- {isPlaying ? <FaPause /> : <FaPlay />}
- </button>
- <input
- type="range"
- min="0"
- max="100"
- value={scrubValue}
- style={{
- flex: 1,
- height: "50px",
- width: "100%",
- }}
- onInput={handleScrub}
- />
- </div>
- </>
- ) : (
- <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 />
- <br />
- Chrome is recommended!
- <br />
- <StyledLink href="/about"> About </StyledLink>
- <StyledLink href="" onClick={handleOnClickDemoButton}>
- {" "}
- Demo{" "}
- </StyledLink>
- </p>
- <div>
- <label htmlFor="base64Input">
- or enter a MoekyunKaraoke code:
- </label>
- <input
- id="base64Input"
- type="text"
- value={base64Input}
- onChange={(e) => setBase64Input(e.target.value)}
- style={{ width: "100%", fontSize: "16px" }}
- />
- <StyledButton onClick={handleKaraokeb64Code}>
- Load Data
- </StyledButton>
- </div>
- </div>
- )}
-
- {/* 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
- id="lrcUpload"
- type="file"
- accept=".lrc"
- onChange={handleLrcFileChange}
- />
- <FileInputLabel
- htmlFor="videoUpload"
- style={{ cursor: "pointer" }}
- >
- Media
- </FileInputLabel>
- <FileInput
- id="videoUpload"
- type="file"
- accept="video/*,audio/*"
- onChange={handleVideoFileChange}
- />
- <FileInputLabel htmlFor="srvUpload" style={{ cursor: "pointer" }}>
- SRV
- </FileInputLabel>
- <FileInput
- id="srvUpload"
- type="file"
- accept=".srv3"
- onChange={handleSrvFileChange}
- />
- <FileInputLabel
- htmlFor="supplementAudioUpload"
- style={{ cursor: "pointer" }}
- >
- Audio #2
- </FileInputLabel>
- <FileInput
- 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>
- <input
- type="number"
- style={{ fontSize: "14px" }}
- id="numberInput"
- value={offset}
- onChange={(e) => setOffset(Number(e.target.value))}
- step="25"
- />
- </div>
- <div
- style={{
- display: "flex",
- flexDirection: "column",
- fontFamily: "Arial",
- }}
- >
- <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>
- </Root>
- );
-}
+};
-export default KaraokePage;
+export default Homepage;
diff --git a/src/app/player/page.tsx b/src/app/player/page.tsx
new file mode 100644
index 0000000..e3fb2aa
--- /dev/null
+++ b/src/app/player/page.tsx
@@ -0,0 +1,828 @@
+"use client";
+import React, { useEffect, useRef, useState } from "react";
+import styled 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 { CaptionsRenderer } from "react-srv3";
+import { useSearchParams } from "next/navigation";
+
+// Styled components
+const Root = styled.div`
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background-color: #f5f5f5;
+`;
+
+const FileInputContainer = styled.div`
+ margin-bottom: 20px;
+ display: flex;
+ justify-content: center;
+ gap: 20px;
+ padding: 10px;
+ border-radius: 5px;
+ background-color: #ffffff;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+`;
+
+const FileInput = styled.input`
+ padding: 10px 15px;
+ border-radius: 5px;
+ border: 1px solid #ddd;
+ justify-content: center;
+ cursor: pointer;
+ display: none;
+ font-family: Arial;
+ &:hover,
+ &:focus {
+ background-color: #eaeaea;
+ outline: none;
+ }
+`;
+
+const FileInputLabel = styled.label`
+ padding: 10px 15px;
+ border-radius: 5px;
+ border: 1px solid #ddd;
+ cursor: pointer;
+ &:hover,
+ &:focus {
+ background-color: #eaeaea;
+ outline: none;
+ }
+`;
+
+const ControlBarButton = styled.button`
+ padding: 10px 15px;
+ border-radius: 5px;
+ border: 1px solid #ddd;
+ align-items: center;
+ cursor: pointer;
+ &:hover,
+ &:focus {
+ background-color: #eaeaea;
+ outline: none;
+ }
+`;
+
+const StyledLink = styled.a`
+ font-size: 20px;
+ font-family: Arial;
+ text-decoration: none;
+ color: black;
+ &:hover {
+ text-decoration: underline;
+ }
+`;
+
+const LRCPlayerWrapper = styled.div`
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow-y: auto;
+ scroll-behavior: smooth;
+ background-color: #ffffff;
+`;
+
+const StyledButton = styled.button`
+ padding: 10px 15px;
+ border-radius: 5px;
+ border: 1px solid #ddd;
+ cursor: pointer;
+ &:hover,
+ &:focus {
+ background-color: #eaeaea;
+ outline: none;
+ }
+`;
+
+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 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>("");
+
+ const searchParams = useSearchParams();
+
+ 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]);
+
+ // Functions for handling file input changes
+ 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);
+ };
+ 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(function (response) {
+ response.text().then(function (responseString) {
+ setLrcContent(responseString);
+ });
+ });
+ 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");
+ };
+
+ // Side effects for 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) return;
+ video.currentTime += 5;
+ }
+ if (e.code === "ArrowLeft") {
+ if (document.activeElement?.tagName === "INPUT") return;
+ const video = videoRef.current;
+ if (!video) return;
+ video.currentTime -= 5;
+ }
+ };
+ document.addEventListener("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);
+ };
+ });
+
+ // 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;
+ } 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);
+ };
+
+ 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);
+ }
+ };
+
+ // 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 =
+ 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;
+ 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();
+ };
+
+ 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.name.endsWith(".lrc")) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setLrcContent(e.target?.result as string);
+ if (videoUrl) setShowFileInputs(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 });
+ }
+ };
+
+ function processData(data: any) {
+ if (data.lrc) {
+ fetch(data.lrc)
+ .then((response) => response.text())
+ .then((text) => {
+ setLrcContent(text);
+ if (videoUrl) setShowFileInputs(false);
+ toast.success("LRC file loaded successfully", {
+ autoClose: 2000,
+ });
+ })
+ .catch((error) => {
+ toast.error("Failed to load LRC file", { autoClose: 2000 });
+ });
+ }
+ if (data.srv3) {
+ fetch(data.srv3)
+ .then((response) => response.text())
+ .then((text) => {
+ setCaptionsText(text);
+ toast.success("SRV file loaded successfully", {
+ autoClose: 2000,
+ });
+ })
+ .catch((error) => {
+ 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.offset1) {
+ setOffset(Number(data.offset));
+ }
+ if (data.offset2) {
+ setOffset(Number(data.offset2));
+ }
+ }
+
+ // Handle base64 input from user
+ const handleKaraokeb64Code = () => {
+ try {
+ const decodedString = atob(base64Input);
+ console.log(decodedString);
+ const data = JSON.parse(decodedString);
+ processData(data);
+ toast.success("Data loaded successfully", { autoClose: 2000 });
+ } catch (e) {
+ 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);
+ processData(data);
+ toast.success("Data loaded from query parameter", {
+ autoClose: 2000,
+ });
+ } catch (e) {
+ toast.error("Invalid data in query parameter", {
+ autoClose: 2000,
+ });
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [searchParams]);
+
+ return (
+ <Root>
+ <ToastContainer />
+ {/*LRC viewer*/}
+ <div style={{ display: "flex", width: "100%", height: "100vh" }}>
+ <LRCPlayerWrapper>
+ <LRCPlayer
+ lrc={lrcContent}
+ currentMillisecond={currentMillisecond}
+ animate={animate}
+ lrcColor={lrcColor}
+ fontColor={fontColor}
+ />
+ </LRCPlayerWrapper>
+
+ {/* Ternary operation for if videoUrl has been set */}
+ <div
+ style={{
+ flex: 1,
+ position: "relative",
+ backgroundColor: dragOver ? "lightblue" : "white",
+ }}
+ onMouseEnter={() => setShowFileInputs(true)}
+ onMouseLeave={() => setShowFileInputs(false)}
+ onDragOver={handleDragOver}
+ onDragEnter={handleDragEnter}
+ onDragLeave={handleDragLeave}
+ onDrop={handleDrop}
+ >
+ {videoUrl ? (
+ <>
+ <video
+ ref={videoRef}
+ src={videoUrl}
+ style={{
+ position: "absolute",
+ width: "100%",
+ height: "100%",
+ }}
+ onEnded={handleVideoEnded}
+ />
+ <audio
+ ref={supplementAudioRef}
+ src={supplementAudioUrl}
+ style={{ display: "none" }}
+ />
+ <div
+ style={{
+ width: "90%",
+ height: "90%",
+ margin: "auto",
+ }}
+ 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
+ onClick={handlePlayPause}
+ style={{
+ width: "50px",
+ height: "50px",
+ padding: "10px",
+ border: "none",
+ borderRadius: "5px",
+ backgroundColor: "white",
+ color: "black",
+ }}
+ >
+ {isPlaying ? <FaPause /> : <FaPlay />}
+ </button>
+ <input
+ type="range"
+ min="0"
+ max="100"
+ value={scrubValue}
+ style={{
+ flex: 1,
+ height: "50px",
+ width: "100%",
+ }}
+ onInput={handleScrub}
+ />
+ </div>
+ </>
+ ) : (
+ <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 />
+ <br />
+ Chrome is recommended!
+ <br />
+ <StyledLink href="/about"> About </StyledLink>
+ <StyledLink
+ href=""
+ onClick={handleOnClickDemoButton}
+ >
+ {" "}
+ Demo{" "}
+ </StyledLink>
+ </p>
+ <div>
+ <label htmlFor="base64Input">
+ or enter a MoekyunKaraoke code:
+ </label>
+ <input
+ id="base64Input"
+ type="text"
+ value={base64Input}
+ onChange={(e) =>
+ setBase64Input(e.target.value)
+ }
+ style={{ width: "100%", fontSize: "16px" }}
+ />
+ <StyledButton onClick={handleKaraokeb64Code}>
+ Load Data
+ </StyledButton>
+ </div>
+ </div>
+ )}
+
+ {/* 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
+ id="lrcUpload"
+ type="file"
+ accept=".lrc"
+ onChange={handleLrcFileChange}
+ />
+ <FileInputLabel
+ htmlFor="videoUpload"
+ style={{ cursor: "pointer" }}
+ >
+ Media
+ </FileInputLabel>
+ <FileInput
+ id="videoUpload"
+ type="file"
+ accept="video/*,audio/*"
+ onChange={handleVideoFileChange}
+ />
+ <FileInputLabel
+ htmlFor="srvUpload"
+ style={{ cursor: "pointer" }}
+ >
+ SRV
+ </FileInputLabel>
+ <FileInput
+ id="srvUpload"
+ type="file"
+ accept=".srv3"
+ onChange={handleSrvFileChange}
+ />
+ <FileInputLabel
+ htmlFor="supplementAudioUpload"
+ style={{ cursor: "pointer" }}
+ >
+ Audio #2
+ </FileInputLabel>
+ <FileInput
+ 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>
+ <input
+ type="number"
+ style={{ fontSize: "14px" }}
+ id="numberInput"
+ value={offset}
+ onChange={(e) =>
+ setOffset(Number(e.target.value))
+ }
+ step="25"
+ />
+ </div>
+ <div
+ style={{
+ display: "flex",
+ flexDirection: "column",
+ fontFamily: "Arial",
+ }}
+ >
+ <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>
+ </Root>
+ );
+}
+
+export default KaraokePage;
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage