"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;