595 lines
19 KiB
JavaScript
595 lines
19 KiB
JavaScript
import React, { useState, useEffect, useRef } from "react";
|
|
import "./MusicPlayer.css";
|
|
import MusicComponent from "./MusicComponent";
|
|
import Switch from "react-switch";
|
|
|
|
export function MusicPlayer({ socket, shopId, user, shopOwnerId, isSpotifyNeedLogin, queue }) {
|
|
const [currentTime, setCurrentTime] = useState(0);
|
|
const [trackLength, setTrackLength] = useState(0);
|
|
const [viewing, setViewing] = useState(false); // State for expansion
|
|
const [expanded, setExpanded] = useState(false); // State for expansion
|
|
|
|
const [songName, setSongName] = useState("");
|
|
const [debouncedSongName, setDebouncedSongName] = useState(songName);
|
|
const [currentSong, setCurrentSong] = useState([]);
|
|
const [songs, setSongs] = useState([]);
|
|
const [paused, setPaused] = useState([]);
|
|
const [getRecommendedMusic, setGetRecommendedMusic] = useState(false);
|
|
|
|
const [lyrics, setLyrics] = useState([]);
|
|
const [currentLines, setCurrentLines] = useState({
|
|
past: [],
|
|
present: [],
|
|
future: [],
|
|
});
|
|
const [lyric_progress_ms, setLyricProgressMs] = useState(0);
|
|
|
|
const [subtitleColor, setSubtitleColor] = useState("black");
|
|
const [subtitleBG, setSubtitleBG] = useState("white");
|
|
const [backgroundImage, setBackgroundImage] = useState("");
|
|
|
|
const [clicked, setClicked] = useState(false);
|
|
|
|
|
|
const [canvaz, setCanvaz] = useState('');
|
|
const [videoSrc, setVideoSrc] = useState('');
|
|
const videoRef = useRef(null);
|
|
const inputRef = useRef(null); // Create a ref to the input field
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
// const getDominantColor = async (imageSrc) => {
|
|
// return new Promise((resolve, reject) => {
|
|
// const img = new Image();
|
|
// img.crossOrigin = "Anonymous";
|
|
// img.src = imageSrc;
|
|
|
|
// img.onload = () => {
|
|
// const canvas = document.createElement("canvas");
|
|
// const ctx = canvas.getContext("2d");
|
|
|
|
// canvas.width = img.width;
|
|
// canvas.height = img.height;
|
|
// ctx.drawImage(img, 0, 0);
|
|
|
|
// const imageData = ctx.getImageData(
|
|
// 0,
|
|
// 0,
|
|
// canvas.width,
|
|
// canvas.height,
|
|
// ).data;
|
|
// const length = imageData.length;
|
|
// let totalR = 0,
|
|
// totalG = 0,
|
|
// totalB = 0;
|
|
|
|
// for (let i = 0; i < length; i += 4) {
|
|
// totalR += imageData[i];
|
|
// totalG += imageData[i + 1];
|
|
// totalB += imageData[i + 2];
|
|
// }
|
|
|
|
// const averageR = Math.round(totalR / (length / 4));
|
|
// const averageG = Math.round(totalG / (length / 4));
|
|
// const averageB = Math.round(totalB / (length / 4));
|
|
|
|
// resolve({ r: averageR, g: averageG, b: averageB });
|
|
// };
|
|
|
|
// img.onerror = (error) => {
|
|
// reject(error);
|
|
// };
|
|
// });
|
|
// };
|
|
|
|
const fetchColor = async () => {
|
|
if (
|
|
currentSong &&
|
|
currentSong[0]?.image
|
|
) {
|
|
const imageUrl = currentSong[0]?.trackId != 'kCGs5_oCtBE' && currentSong[0]?.trackId != 'O8eYd7oAZtA' ? currentSong[0].image : '';
|
|
try {
|
|
// const dominantColor = await getDominantColor(imageUrl);
|
|
// // Calculate luminance (YIQ color space) to determine if subtitle should be black or white
|
|
// const luminance =
|
|
// (0.299 * dominantColor.r +
|
|
// 0.587 * dominantColor.g +
|
|
// 0.114 * dominantColor.b) /
|
|
// 255;
|
|
// if (luminance > 0.5) {
|
|
// setSubtitleColor("black");
|
|
// setSubtitleBG("white");
|
|
// } else {
|
|
// setSubtitleColor("white");
|
|
// setSubtitleBG("black");
|
|
// }
|
|
setBackgroundImage(imageUrl);
|
|
} catch (error) {
|
|
console.error("Error fetching or processing image:", error);
|
|
}
|
|
}
|
|
};
|
|
|
|
fetchColor();
|
|
}, [currentSong]);
|
|
|
|
const convertToMilliseconds = (timeStr) => {
|
|
const [minutes, seconds] = timeStr.split(':').map(Number);
|
|
return (minutes * 60 + seconds) * 1000;
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!socket) return;
|
|
|
|
socket.on("searchResponse", (response) => {
|
|
console.log(response);
|
|
setSongs(response);
|
|
});
|
|
|
|
socket.on("updateCurrentSong", (response) => {
|
|
console.log(response)
|
|
setCurrentSong(response);
|
|
// setCurrentTime(response.progress_ms / 1000); // Convert milliseconds to seconds
|
|
// setLyricProgressMs(response.progress_ms);
|
|
// setTrackLength(convertToMilliseconds(response.item.length));
|
|
});
|
|
|
|
|
|
socket.on("updatePlayer", (response) => {
|
|
setPaused(response.decision);
|
|
});
|
|
|
|
socket.on("updateLyrics", (response) => {
|
|
setLyrics(response);
|
|
console.log(response);
|
|
setCurrentLines({
|
|
past: [],
|
|
present: [],
|
|
future: [],
|
|
});
|
|
});
|
|
|
|
socket.on("updateQueue", ({ getRecommendedMusic }) => {
|
|
if (getRecommendedMusic == undefined) return;
|
|
setGetRecommendedMusic(getRecommendedMusic); // Only set the queue if it's a valid non-empty array
|
|
console.log("Updated config:", getRecommendedMusic); // Log the valid queue
|
|
});
|
|
|
|
return () => {
|
|
socket.off("searchResponse");
|
|
};
|
|
}, [socket]);
|
|
|
|
// useEffect for setting up the socket listener
|
|
useEffect(() => {
|
|
const handleUpdateCanvas = (response) => {
|
|
if (response && response !== canvaz) {
|
|
console.log(response);
|
|
console.log(canvaz);
|
|
setCanvaz(response);
|
|
fetch(response)
|
|
.then((response) => response.blob())
|
|
.then((blob) => {
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
setVideoSrc(blobUrl);
|
|
|
|
if (videoRef.current) {
|
|
videoRef.current.load(); // Reload the video element
|
|
}
|
|
})
|
|
.catch((error) => console.error('Error loading video:', error));
|
|
} else if (!response) {
|
|
// Clear the video source if response is empty
|
|
setVideoSrc('');
|
|
if (videoRef.current) {
|
|
videoRef.current.load(); // Reload the video element
|
|
}
|
|
}
|
|
};
|
|
|
|
// Listen for the "updateCanvas" event
|
|
socket.on("updateCanvas", handleUpdateCanvas);
|
|
|
|
socket.on("claimPlayerRes", (response) => {
|
|
if (response.error) {
|
|
console.log('Error:', response.error);
|
|
// Handle error
|
|
} else {
|
|
window.open(response.url);
|
|
}
|
|
});
|
|
|
|
socket.on("unClaimPlayerRes", (response) => {
|
|
if (response.error) {
|
|
console.log('Error:', response.error);
|
|
// Handle error
|
|
} else {
|
|
console.log('Player token:', response.token);
|
|
// Handle success and use the player token
|
|
}
|
|
});
|
|
|
|
// Clean up the socket listener when the component is unmounted
|
|
return () => {
|
|
socket.off("updateCanvas", handleUpdateCanvas);
|
|
};
|
|
}, [socket, canvaz]);
|
|
|
|
useEffect(() => {
|
|
// Simulate progress every 100ms
|
|
const interval = setInterval(() => {
|
|
setLyricProgressMs((prevProgress) => prevProgress + 100);
|
|
}, 100);
|
|
|
|
return () => clearInterval(interval); // Clean up interval on component unmount
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (lyrics == null) return;
|
|
const pastLines = lyrics.filter(
|
|
(line) => line.startTimeMs < lyric_progress_ms,
|
|
);
|
|
const presentLines = lyrics.filter(
|
|
(line) => line.startTimeMs > lyric_progress_ms,
|
|
);
|
|
const futureLines = lyrics.filter(
|
|
(line) => line.startTimeMs > lyric_progress_ms,
|
|
);
|
|
|
|
setCurrentLines({
|
|
past: pastLines.slice(-2, 1), // Get the last past line
|
|
present: pastLines.slice(-1),
|
|
future: futureLines.slice(0, 1), // Get the first future line
|
|
});
|
|
}, [lyrics, lyric_progress_ms]);
|
|
|
|
useEffect(() => {
|
|
const handler = setTimeout(() => {
|
|
setDebouncedSongName(songName);
|
|
}, 300);
|
|
|
|
// Cleanup function to clear the timeout if songName changes
|
|
return () => {
|
|
clearTimeout(handler);
|
|
};
|
|
}, [songName]);
|
|
|
|
const changeIsGetRecommendedMusic = () => {
|
|
const isGetRecommendedMusic = !getRecommendedMusic;
|
|
setGetRecommendedMusic(isGetRecommendedMusic)
|
|
const token = localStorage.getItem("auth");
|
|
if (socket != null && token) {
|
|
socket.emit("configPlayer", { token, shopId, getRecommendedMusic: isGetRecommendedMusic });
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (socket != null && debouncedSongName) {
|
|
socket.emit("searchRequest", { shopId, songName: debouncedSongName });
|
|
}
|
|
}, [debouncedSongName, shopId, socket]);
|
|
|
|
const handleInputChange = (event) => {
|
|
setSongName(event.target.value);
|
|
};
|
|
|
|
const onRequest = (track) => {
|
|
const token = localStorage.getItem("auth");
|
|
if (socket != null && token) {
|
|
socket.emit("songRequest", { token, shopId, track });
|
|
setSongName("");
|
|
}
|
|
};
|
|
|
|
const onDecision = (trackId, vote) => {
|
|
const token = localStorage.getItem("auth");
|
|
if (socket != null && token)
|
|
socket.emit("songVote", { token, shopId, trackId, vote });
|
|
};
|
|
|
|
const handlePauseOrResume = (trackId, vote) => {
|
|
const token = localStorage.getItem("auth");
|
|
if (socket != null && token) {
|
|
socket.emit("playOrPause", {
|
|
token,
|
|
shopId,
|
|
action: paused ? "pause" : "resume",
|
|
});
|
|
console.log(paused);
|
|
setPaused(!paused);
|
|
}
|
|
};
|
|
|
|
const handleSetPlayer = () => {
|
|
const token = localStorage.getItem("auth");
|
|
|
|
socket.emit("claimPlayer", {
|
|
token,
|
|
shopId,
|
|
});
|
|
if (isSpotifyNeedLogin) {
|
|
} else {
|
|
// socket.emit("unClaimPlayer", {
|
|
// token,
|
|
// shopId,
|
|
// });
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
setCurrentTime((prevTime) =>
|
|
prevTime < trackLength ? prevTime + 1 : prevTime,
|
|
);
|
|
}, 1000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [trackLength]);
|
|
|
|
const formatTime = (timeInSeconds) => {
|
|
const minutes = Math.floor(timeInSeconds / 60);
|
|
const seconds = Math.floor(timeInSeconds % 60);
|
|
|
|
// Ensure seconds and milliseconds are always displayed with two and three digits respectively
|
|
const formattedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
|
|
|
|
return `${minutes}:${formattedSeconds}`;
|
|
};
|
|
|
|
const toggleView = () => {
|
|
setViewing(!viewing);
|
|
};
|
|
const toggleExpand = () => {
|
|
setExpanded(!expanded);
|
|
};
|
|
|
|
const expandableContainerRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
if (expanded && expandableContainerRef.current) {
|
|
expandableContainerRef.current.scrollTo({ top: 0, behavior: "smooth" });
|
|
}
|
|
}, [expanded]);
|
|
|
|
|
|
const [text, setText] = useState("Menunggu musik favoritmu");
|
|
const textIndex = useRef(0);
|
|
const [messages, setMessages] = useState(["Menunggu musik favoritmu", "Klik untuk putar musik favoritmu"]);
|
|
|
|
|
|
useEffect(() => {
|
|
// Update the messages based on currentSong
|
|
const newMessages = [
|
|
currentSong != null && currentSong[0]?.trackId != 'kCGs5_oCtBE' && currentSong[0]?.trackId != 'O8eYd7oAZtA' && currentSong[0]?.name != undefined
|
|
? `${currentSong[0]?.name} - ${currentSong[0]?.artist}`
|
|
: "Menunggu musik favoritmu",
|
|
"Klik untuk putar musik favoritmu"
|
|
];
|
|
|
|
setMessages(newMessages);
|
|
setText(newMessages[0]); // Update the text state to the first message
|
|
|
|
const element = document.querySelector('.animated-text');
|
|
|
|
// Check if the element exists before adding the event listener
|
|
if (element) {
|
|
const handleAnimationIteration = () => {
|
|
// Toggle between the two text values based on the current index
|
|
textIndex.current = (textIndex.current + 1) % messages.length;
|
|
setText(messages[textIndex.current]);
|
|
};
|
|
|
|
element.addEventListener('animationiteration', handleAnimationIteration);
|
|
|
|
return () => {
|
|
element.removeEventListener('animationiteration', handleAnimationIteration);
|
|
};
|
|
}
|
|
}, [currentSong]); // Run effect when currentSong changes
|
|
|
|
const handleButtonClick = () => {
|
|
setClicked(true);
|
|
|
|
if (inputRef.current) {
|
|
inputRef.current.focus(); // Focus the input when the button is clicked
|
|
}
|
|
|
|
// After 1 second, remove the "clicked" class to let the color gradually return to the original
|
|
setTimeout(() => {
|
|
setClicked(false);
|
|
}, 1000); // 1 second timeout (same as the CSS transition duration)
|
|
};
|
|
|
|
function modifyUrl(url) {
|
|
// Use a regular expression to find the part of the URL that starts with '='
|
|
return url.replace(/=(w\d+)-(h\d+)-/, '=w255-h255-');
|
|
}
|
|
|
|
return (
|
|
<div className={`music-player`} style={{ marginBottom: `${viewing ? '-10px' : ''}` }}>
|
|
<div
|
|
onClick={toggleView}
|
|
className="current-bgr"
|
|
style={{ backgroundImage: `url(${modifyUrl(backgroundImage)})` }}
|
|
// style={{ backgroundImage: `url(${videoSrc != "" ? '' : backgroundImage})` }}
|
|
>
|
|
{/* <video
|
|
ref={videoRef}
|
|
autoPlay
|
|
loop
|
|
playsInline
|
|
muted
|
|
style={{ height: '100%', width: '100%', objectFit: 'cover', position: 'absolute', top: 0, right: 0, zIndex: -1 }}
|
|
>
|
|
{videoSrc && <source src={videoSrc} type="video/mp4" />}
|
|
</video> */}
|
|
{currentLines.present.map((line, index) => (
|
|
<div className="present" style={{
|
|
color: subtitleColor,
|
|
textShadow: `2px 2px 2px ${subtitleBG}`,
|
|
}} key={index}>
|
|
<p>{line.words}</p>
|
|
</div>
|
|
))}
|
|
{currentLines.future.map((line, index) => (
|
|
<div className="future" style={{
|
|
color: subtitleColor,
|
|
textShadow: `2px 2px 2px ${subtitleBG}`,
|
|
}} key={index}>
|
|
<p>{line.words}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="current-info" >
|
|
<div
|
|
className={`current-name ${viewing ? '' : 'animated-text'}`} style={{ margin: `${viewing ? '35px 30px' : '13px 30px'}` }}>
|
|
{currentSong && currentSong[0]?.name
|
|
? (viewing && currentSong[0]?.trackId != 'kCGs5_oCtBE' && currentSong[0]?.trackId != 'O8eYd7oAZtA' ? currentSong[0]?.name : text)
|
|
:
|
|
viewing ? messages[0] : text}
|
|
</div>
|
|
{viewing && <>
|
|
<div className="current-artist">
|
|
{
|
|
currentSong &&
|
|
currentSong[0]?.artist && currentSong[0]?.trackId != 'kCGs5_oCtBE' && currentSong[0]?.trackId != 'O8eYd7oAZtA'
|
|
? currentSong[0]?.artist
|
|
: "Pilih hits terbaikmu dibawah!"}
|
|
</div>
|
|
</>
|
|
}
|
|
</div>
|
|
{viewing &&
|
|
<>
|
|
<div
|
|
className={`expandable-container ${expanded ? "expanded" : ""}`}
|
|
ref={expandableContainerRef}
|
|
>
|
|
{user.cafeId == shopId || user.userId == shopOwnerId && (
|
|
<>
|
|
<div className="auth-box">
|
|
<div
|
|
onClick={handleSetPlayer}
|
|
>{isSpotifyNeedLogin ? "Jadikan perangkat sebagai pemutar musik" : "Unset as music player"}</div>
|
|
</div>
|
|
<div className="config-box">
|
|
<div
|
|
>
|
|
Dapatkan rekomendasi musik
|
|
<div><Switch
|
|
onChange={() => changeIsGetRecommendedMusic()}
|
|
checked={getRecommendedMusic}
|
|
/></div>
|
|
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
<div className="search-box">
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
placeholder="Cari musik..."
|
|
value={songName}
|
|
onChange={handleInputChange}
|
|
className={clicked ? 'clicked' : ''}
|
|
/>
|
|
</div>
|
|
|
|
|
|
<div
|
|
className="rectangle"
|
|
style={{
|
|
justifyContent: songName === "" && queue && queue.length > 0 && queue.length < 3 || songName != '' ? 'flex-start' : 'center'
|
|
}}
|
|
>
|
|
{(songName == "" && queue && queue.length < 2 || (queue[0] != undefined && queue.length < 1 && queue[0][5]) == true) && (
|
|
<div className="middle-text">
|
|
<span className="bold-text">Antrian kosong</span><br />
|
|
<span className="normal-text">Pilih musikmu</span>
|
|
<button
|
|
className={`search-button ${clicked ? 'clicked' : ''}`}
|
|
onClick={handleButtonClick}
|
|
>
|
|
Cari musik
|
|
</button>
|
|
</div>
|
|
)}
|
|
{songName != "" &&
|
|
songs.map((song, index) => (
|
|
<MusicComponent
|
|
key={index}
|
|
song={song}
|
|
min={0}
|
|
max={100}
|
|
onDecision={(e) => onRequest(song)}
|
|
/>
|
|
))}
|
|
{
|
|
songName === "" &&
|
|
queue &&
|
|
Array.isArray(queue) &&
|
|
queue.length > 0 && (
|
|
queue
|
|
.filter(song => song[5] !== true) // Filter out songs where song[5] is true or undefined
|
|
.map((song, index) => (
|
|
<MusicComponent
|
|
key={index}
|
|
song={song}
|
|
min={song[3] ? 0 : -100}
|
|
max={song[3] ? 0 : 100}
|
|
onDecision={(vote) => onDecision(song[0].trackId, vote)}
|
|
/>
|
|
))
|
|
)
|
|
}
|
|
|
|
{
|
|
songName === "" &&
|
|
queue &&
|
|
Array.isArray(queue) &&
|
|
queue.length > 0 && (
|
|
queue
|
|
.filter(song => song[5] === true) // Filter out songs where song[5] is true or undefined
|
|
.map((song, index) => (
|
|
<MusicComponent
|
|
key={index}
|
|
song={song}
|
|
min={song[3] ? 0 : -100}
|
|
max={song[3] ? 0 : 100}
|
|
onDecision={(vote) => onDecision(song[0].trackId, vote)}
|
|
/>
|
|
))
|
|
)
|
|
}
|
|
{/* {songName == "" && queue && queue.length > 0 && queue.length < 3 && (
|
|
|
|
<div className="middle-text">
|
|
<span className="normal-text">Tambahkan musikmu</span>
|
|
<button
|
|
className={`search-button ${clicked ? 'clicked' : ''}`}
|
|
onClick={handleButtonClick}
|
|
>
|
|
Cari musik
|
|
</button>
|
|
</div>
|
|
)} */}
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
<div className={`expand-button ${expanded ? "expanded" : ""}`} onClick={toggleExpand}>
|
|
|
|
<h5>
|
|
{expanded ? '⋀' : 'Lihat antrian musik'}
|
|
</h5>
|
|
</div></>
|
|
}
|
|
</div >
|
|
);
|
|
}
|