From d2637f982fa00e034d3dbb7c5aa4d03118bf73c3 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Tue, 7 Jan 2025 01:10:06 -0800 Subject: add prisma ORM and schema for DB --- .gitignore | 1 + package.json | 2 + pnpm-lock.yaml | 74 +++++ prisma/schema.prisma | 20 ++ src/app/globals.css | 2 - src/app/page.tsx | 843 ++++-------------------------------------------- src/app/player/page.tsx | 828 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 992 insertions(+), 778 deletions(-) create mode 100644 prisma/schema.prisma delete mode 100644 src/app/globals.css create mode 100644 src/app/player/page.tsx 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(""); - const [videoUrl, setVideoUrl] = useState(""); - const [supplementAudioUrl, setSupplementAudioUrl] = useState(""); - const [isPlaying, setIsPlaying] = useState(false); - const [showVolume, setShowVolume] = useState(false); - const [scrubValue, setScrubValue] = useState(0); - const [showFileInputs, setShowFileInputs] = useState(true); - const videoRef = useRef(null); - const supplementAudioRef = useRef(null); - const [captionsText, setCaptionsText] = useState(""); - const [offset, setOffset] = useState(0); - const [dragOver, setDragOver] = useState(false); - const [statusText, setStatusText] = useState("No video selected"); - const [balance, setBalance] = useState(0); - const [animate, setAnimate] = useState(true); - const [lrcColor, setLrcColor] = useState("#C8BEBE"); - const [fontColor, setFontColor] = useState("#000000"); - const [supplementAudioOffset, setSupplementAudioOffset] = useState(0); - const [base64Input, setBase64Input] = useState(""); - - 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) => { - 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, - ) => { - 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) => { - 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, - ) => { - 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, - ) => { - 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 ( + + + + Sample Video 1 + + This is a description for sample video 1. + + + + Sample Video 2 + + This is a description for sample video 2. + + + + Sample Video 2 + + This is a description for sample video 2. + + + + Sample Video 2 + + This is a description for sample video 2. + + + + Sample Video 2 + + This is a description for sample video 2. + + + + Sample Video 2 + + This is a description for sample video 2. + + + + ); - 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) => { - 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) => { - setDragOver(true); - event.preventDefault(); - }; - - const handleDragEnter = (event: React.DragEvent) => { - setDragOver(true); - event.preventDefault(); - }; - - const handleDragLeave = (event: React.DragEvent) => { - setDragOver(false); - event.preventDefault(); - }; - - const handleDrop = (event: React.DragEvent) => { - 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 ( - - - {/*LRC viewer*/} -
- - - - - {/* Ternary operation for if videoUrl has been set */} -
setShowFileInputs(true)} - onMouseLeave={() => setShowFileInputs(false)} - onDragOver={handleDragOver} - onDragEnter={handleDragEnter} - onDragLeave={handleDragLeave} - onDrop={handleDrop} - > - {videoUrl ? ( - <> -
-
-
- ); -} +}; -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(""); + const [videoUrl, setVideoUrl] = useState(""); + const [supplementAudioUrl, setSupplementAudioUrl] = useState(""); + const [isPlaying, setIsPlaying] = useState(false); + const [showVolume, setShowVolume] = useState(false); + const [scrubValue, setScrubValue] = useState(0); + const [showFileInputs, setShowFileInputs] = useState(true); + const videoRef = useRef(null); + const supplementAudioRef = useRef(null); + const [captionsText, setCaptionsText] = useState(""); + const [offset, setOffset] = useState(0); + const [dragOver, setDragOver] = useState(false); + const [statusText, setStatusText] = useState("No video selected"); + const [balance, setBalance] = useState(0); + const [animate, setAnimate] = useState(true); + const [lrcColor, setLrcColor] = useState("#C8BEBE"); + const [fontColor, setFontColor] = useState("#000000"); + const [supplementAudioOffset, setSupplementAudioOffset] = + useState(0); + const [base64Input, setBase64Input] = useState(""); + + 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, + ) => { + 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, + ) => { + 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, + ) => { + 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, + ) => { + 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, + ) => { + 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) => { + 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) => { + setDragOver(true); + event.preventDefault(); + }; + + const handleDragEnter = (event: React.DragEvent) => { + setDragOver(true); + event.preventDefault(); + }; + + const handleDragLeave = (event: React.DragEvent) => { + setDragOver(false); + event.preventDefault(); + }; + + const handleDrop = (event: React.DragEvent) => { + 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 ( + + + {/*LRC viewer*/} +
+ + + + + {/* Ternary operation for if videoUrl has been set */} +
setShowFileInputs(true)} + onMouseLeave={() => setShowFileInputs(false)} + onDragOver={handleDragOver} + onDragEnter={handleDragEnter} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + > + {videoUrl ? ( + <> +
+
+
+ ); +} + +export default KaraokePage; -- cgit v1.2.3