Spaces:
Sleeping
Sleeping
Commit
·
ba6c831
1
Parent(s):
89a38e9
major update: add playlist
Browse files- frontend/src/app/globals.css +11 -1
- frontend/src/app/layout.js +5 -4
- frontend/src/app/playlists/page.js +9 -0
- frontend/src/components/Card.css +19 -6
- frontend/src/components/Card.js +16 -4
- frontend/src/components/Header.js +3 -0
- frontend/src/components/MusicPlayer.css +5 -1
- frontend/src/components/MusicPlayer.js +134 -41
- frontend/src/components/Playlist.css +126 -0
- frontend/src/components/Playlist.js +93 -0
- frontend/src/components/SavedPlaylists.css +65 -0
- frontend/src/components/SavedPlaylists.js +65 -0
- frontend/src/context/MusicPlayerContext.js +84 -4
frontend/src/app/globals.css
CHANGED
|
@@ -13,6 +13,9 @@
|
|
| 13 |
--foreground-2: #0b5da9;
|
| 14 |
--foreground-3: #25253c;
|
| 15 |
--foreground-4: #878787;
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
html,
|
|
@@ -40,7 +43,6 @@ footer {
|
|
| 40 |
margin-right: 10px;
|
| 41 |
flex-grow: 1;
|
| 42 |
overflow-y: auto;
|
| 43 |
-
|
| 44 |
/* Styled scrollbar */
|
| 45 |
scrollbar-width: thin; /* Firefox */
|
| 46 |
scrollbar-color: #888 #222;
|
|
@@ -64,3 +66,11 @@ footer {
|
|
| 64 |
.app-container::-webkit-scrollbar-thumb:hover {
|
| 65 |
background-color: #555; /* Thumb color on hover */
|
| 66 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
--foreground-2: #0b5da9;
|
| 14 |
--foreground-3: #25253c;
|
| 15 |
--foreground-4: #878787;
|
| 16 |
+
--foreground-5: #403f4f;
|
| 17 |
+
--highlight-color: #00aaff53;
|
| 18 |
+
--text-highlight: rgb(31, 242, 179);
|
| 19 |
}
|
| 20 |
|
| 21 |
html,
|
|
|
|
| 43 |
margin-right: 10px;
|
| 44 |
flex-grow: 1;
|
| 45 |
overflow-y: auto;
|
|
|
|
| 46 |
/* Styled scrollbar */
|
| 47 |
scrollbar-width: thin; /* Firefox */
|
| 48 |
scrollbar-color: #888 #222;
|
|
|
|
| 66 |
.app-container::-webkit-scrollbar-thumb:hover {
|
| 67 |
background-color: #555; /* Thumb color on hover */
|
| 68 |
}
|
| 69 |
+
|
| 70 |
+
@media screen and (orientation: portrait) {
|
| 71 |
+
footer{
|
| 72 |
+
display: flex;
|
| 73 |
+
flex-direction: column;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
}
|
frontend/src/app/layout.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
-
// RootLayout.js
|
| 2 |
"use client";
|
| 3 |
|
| 4 |
import localFont from "next/font/local";
|
| 5 |
import "./globals.css";
|
| 6 |
import MusicPlayer from "@/components/MusicPlayer";
|
| 7 |
import { MusicPlayerProvider } from "@/context/MusicPlayerContext";
|
| 8 |
-
import { AppProgressBar as ProgressBar } from
|
| 9 |
import Header from "@/components/Header";
|
|
|
|
| 10 |
|
| 11 |
const geistSans = localFont({
|
| 12 |
src: "./fonts/GeistVF.woff",
|
|
@@ -32,9 +32,10 @@ export default function RootLayout({ children }) {
|
|
| 32 |
options={{ showSpinner: false }}
|
| 33 |
shallowRouting
|
| 34 |
/>
|
| 35 |
-
<Header/>
|
| 36 |
-
<div className="app-container">{children}</div>
|
| 37 |
<footer className="bottom-0 flex items-center w-full">
|
|
|
|
| 38 |
<MusicPlayer />
|
| 39 |
</footer>
|
| 40 |
</body>
|
|
|
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
import localFont from "next/font/local";
|
| 4 |
import "./globals.css";
|
| 5 |
import MusicPlayer from "@/components/MusicPlayer";
|
| 6 |
import { MusicPlayerProvider } from "@/context/MusicPlayerContext";
|
| 7 |
+
import { AppProgressBar as ProgressBar } from "next-nprogress-bar";
|
| 8 |
import Header from "@/components/Header";
|
| 9 |
+
import Playlist from "@/components/Playlist";
|
| 10 |
|
| 11 |
const geistSans = localFont({
|
| 12 |
src: "./fonts/GeistVF.woff",
|
|
|
|
| 32 |
options={{ showSpinner: false }}
|
| 33 |
shallowRouting
|
| 34 |
/>
|
| 35 |
+
<Header />
|
| 36 |
+
<div className="app-container">{children} </div>
|
| 37 |
<footer className="bottom-0 flex items-center w-full">
|
| 38 |
+
<Playlist />
|
| 39 |
<MusicPlayer />
|
| 40 |
</footer>
|
| 41 |
</body>
|
frontend/src/app/playlists/page.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import SavedPlaylists from "@/components/SavedPlaylists";
|
| 2 |
+
|
| 3 |
+
export default function PlaylistsPage() {
|
| 4 |
+
return (
|
| 5 |
+
<div className="playlists-page font-[family-name:var(--font-geist-sans)]">
|
| 6 |
+
<SavedPlaylists/>
|
| 7 |
+
</div>
|
| 8 |
+
);
|
| 9 |
+
}
|
frontend/src/components/Card.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
.music-card {
|
| 2 |
width: 250px;
|
| 3 |
-
height:
|
| 4 |
background-image: linear-gradient(var(--background-4), var(--foreground-3));
|
| 5 |
border-top-left-radius: 20px;
|
| 6 |
border-bottom-left-radius: 20px;
|
|
@@ -8,9 +8,10 @@
|
|
| 8 |
border-bottom-right-radius: 20px;
|
| 9 |
display: flex;
|
| 10 |
align-items: center;
|
| 11 |
-
|
|
|
|
| 12 |
overflow: hidden;
|
| 13 |
-
transition: scale .3s ease;
|
| 14 |
}
|
| 15 |
|
| 16 |
.card-title {
|
|
@@ -22,6 +23,7 @@
|
|
| 22 |
white-space: normal;
|
| 23 |
font-size: 0.8rem;
|
| 24 |
cursor: pointer;
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
.card-icon {
|
|
@@ -29,11 +31,22 @@
|
|
| 29 |
overflow: visible;
|
| 30 |
}
|
| 31 |
|
| 32 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
scale: 1.03;
|
| 34 |
}
|
| 35 |
|
| 36 |
-
.music-card.
|
| 37 |
background-image: linear-gradient(var(--background-2), var(--foreground-2));
|
| 38 |
border: 2px solid var(--foreground-secondary);
|
| 39 |
border-right: none;
|
|
@@ -43,7 +56,7 @@
|
|
| 43 |
scale: 1.03;
|
| 44 |
}
|
| 45 |
|
| 46 |
-
.music-card.now-playing .card-icon{
|
| 47 |
animation: rotateInfinite 1.5s linear infinite;
|
| 48 |
}
|
| 49 |
|
|
|
|
| 1 |
.music-card {
|
| 2 |
width: 250px;
|
| 3 |
+
height: 50px; /* Increased height to accommodate the button */
|
| 4 |
background-image: linear-gradient(var(--background-4), var(--foreground-3));
|
| 5 |
border-top-left-radius: 20px;
|
| 6 |
border-bottom-left-radius: 20px;
|
|
|
|
| 8 |
border-bottom-right-radius: 20px;
|
| 9 |
display: flex;
|
| 10 |
align-items: center;
|
| 11 |
+
justify-content: space-between; /* Space between elements */
|
| 12 |
+
padding: 0 5px; /* Added padding for better spacing */
|
| 13 |
overflow: hidden;
|
| 14 |
+
transition: scale 0.3s ease;
|
| 15 |
}
|
| 16 |
|
| 17 |
.card-title {
|
|
|
|
| 23 |
white-space: normal;
|
| 24 |
font-size: 0.8rem;
|
| 25 |
cursor: pointer;
|
| 26 |
+
flex-grow: 1; /* Allow the title to take available space */
|
| 27 |
}
|
| 28 |
|
| 29 |
.card-icon {
|
|
|
|
| 31 |
overflow: visible;
|
| 32 |
}
|
| 33 |
|
| 34 |
+
.add-to-playlist {
|
| 35 |
+
background: none; /* Remove default button background */
|
| 36 |
+
border: none; /* Remove default button border */
|
| 37 |
+
cursor: pointer; /* Change cursor to pointer */
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.add-icon {
|
| 41 |
+
font-size: 20px; /* Adjust the icon size as needed */
|
| 42 |
+
color: var(--foreground-secondary); /* Use a theme color */
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.music-card:hover {
|
| 46 |
scale: 1.03;
|
| 47 |
}
|
| 48 |
|
| 49 |
+
.music-card.now-playing {
|
| 50 |
background-image: linear-gradient(var(--background-2), var(--foreground-2));
|
| 51 |
border: 2px solid var(--foreground-secondary);
|
| 52 |
border-right: none;
|
|
|
|
| 56 |
scale: 1.03;
|
| 57 |
}
|
| 58 |
|
| 59 |
+
.music-card.now-playing .card-icon {
|
| 60 |
animation: rotateInfinite 1.5s linear infinite;
|
| 61 |
}
|
| 62 |
|
frontend/src/components/Card.js
CHANGED
|
@@ -1,24 +1,36 @@
|
|
| 1 |
"use client";
|
| 2 |
import "./Card.css";
|
| 3 |
-
import { FaCompactDisc } from "react-icons/fa";
|
|
|
|
| 4 |
import { useMusicPlayer } from "@/context/MusicPlayerContext";
|
| 5 |
import { formatTitle } from "@/lib/utils";
|
| 6 |
|
| 7 |
const Card = ({ src }) => {
|
| 8 |
-
const { initializePlayer, nowPlaying } = useMusicPlayer();
|
| 9 |
const title = formatTitle(src);
|
| 10 |
|
| 11 |
-
const
|
| 12 |
initializePlayer(src, title);
|
| 13 |
};
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
return (
|
| 16 |
-
|
|
|
|
| 17 |
<div className={`music-card ${nowPlaying === title ? "now-playing" : ""}`}>
|
| 18 |
<FaCompactDisc className="card-icon" />
|
| 19 |
<label className="card-title">{title}</label>
|
| 20 |
</div>
|
| 21 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
);
|
| 23 |
};
|
| 24 |
|
|
|
|
| 1 |
"use client";
|
| 2 |
import "./Card.css";
|
| 3 |
+
import { FaCompactDisc } from "react-icons/fa"; // Importing the add icon
|
| 4 |
+
import { TbPlaylistAdd } from "react-icons/tb";
|
| 5 |
import { useMusicPlayer } from "@/context/MusicPlayerContext";
|
| 6 |
import { formatTitle } from "@/lib/utils";
|
| 7 |
|
| 8 |
const Card = ({ src }) => {
|
| 9 |
+
const { initializePlayer, nowPlaying, addToPlaylist } = useMusicPlayer();
|
| 10 |
const title = formatTitle(src);
|
| 11 |
|
| 12 |
+
const handlePlayButtonClick = () => {
|
| 13 |
initializePlayer(src, title);
|
| 14 |
};
|
| 15 |
|
| 16 |
+
const handleAddToPlaylistClick = (e) => {
|
| 17 |
+
e.stopPropagation(); // Prevent the button click from triggering the card play
|
| 18 |
+
addToPlaylist({ source: src, title });
|
| 19 |
+
console.log(`${title} added to playlist`)
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
return (
|
| 23 |
+
<>
|
| 24 |
+
<button onClick={handlePlayButtonClick}>
|
| 25 |
<div className={`music-card ${nowPlaying === title ? "now-playing" : ""}`}>
|
| 26 |
<FaCompactDisc className="card-icon" />
|
| 27 |
<label className="card-title">{title}</label>
|
| 28 |
</div>
|
| 29 |
</button>
|
| 30 |
+
<button className="add-to-playlist" onClick={handleAddToPlaylistClick}>
|
| 31 |
+
<TbPlaylistAdd className="add-icon" />
|
| 32 |
+
</button>
|
| 33 |
+
</>
|
| 34 |
);
|
| 35 |
};
|
| 36 |
|
frontend/src/components/Header.js
CHANGED
|
@@ -14,6 +14,9 @@ const Header = () => {
|
|
| 14 |
<Link href="/" className="nav-link">
|
| 15 |
Home
|
| 16 |
</Link>
|
|
|
|
|
|
|
|
|
|
| 17 |
</nav>
|
| 18 |
</div>
|
| 19 |
</header>
|
|
|
|
| 14 |
<Link href="/" className="nav-link">
|
| 15 |
Home
|
| 16 |
</Link>
|
| 17 |
+
<Link href="/playlists" className="nav-link">
|
| 18 |
+
Playlists
|
| 19 |
+
</Link>
|
| 20 |
</nav>
|
| 21 |
</div>
|
| 22 |
</header>
|
frontend/src/components/MusicPlayer.css
CHANGED
|
@@ -257,7 +257,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|
| 257 |
/******** Firefox styles ********/
|
| 258 |
/* slider track */
|
| 259 |
input[type="range"]::-moz-range-track {
|
| 260 |
-
background-color:
|
| 261 |
border-radius: 0.5rem;
|
| 262 |
height: 0.5rem;
|
| 263 |
}
|
|
@@ -278,3 +278,7 @@ input[type="range"]::-moz-range-thumb {
|
|
| 278 |
font-size: 1.3em;
|
| 279 |
line-height: 1.2; /* Adjust line height for spacing */
|
| 280 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
/******** Firefox styles ********/
|
| 258 |
/* slider track */
|
| 259 |
input[type="range"]::-moz-range-track {
|
| 260 |
+
background-color: var(--foreground-5);
|
| 261 |
border-radius: 0.5rem;
|
| 262 |
height: 0.5rem;
|
| 263 |
}
|
|
|
|
| 278 |
font-size: 1.3em;
|
| 279 |
line-height: 1.2; /* Adjust line height for spacing */
|
| 280 |
}
|
| 281 |
+
|
| 282 |
+
button:disabled{
|
| 283 |
+
color: var(--foreground-5);
|
| 284 |
+
}
|
frontend/src/components/MusicPlayer.js
CHANGED
|
@@ -29,10 +29,14 @@ export default function MusicPlayer() {
|
|
| 29 |
title,
|
| 30 |
setNowPlaying,
|
| 31 |
nowPlaying,
|
| 32 |
-
didDestroy,
|
| 33 |
setDidDestroy,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
} = useMusicPlayer();
|
| 35 |
-
|
| 36 |
const [currentSrc, setCurrentSrc] = useState(src);
|
| 37 |
const [currentTime, setCurrentTime] = useState(0);
|
| 38 |
const [duration, setDuration] = useState(0);
|
|
@@ -46,7 +50,8 @@ export default function MusicPlayer() {
|
|
| 46 |
const [isFullscreen, setIsFullscreen] = useState(false);
|
| 47 |
const overlayTimeout = useRef(null);
|
| 48 |
const seekTime = 5;
|
| 49 |
-
|
|
|
|
| 50 |
// Event Handlers
|
| 51 |
const handleTimeUpdate = (videoElement) => {
|
| 52 |
if (videoElement) {
|
|
@@ -54,7 +59,7 @@ export default function MusicPlayer() {
|
|
| 54 |
setProgress((videoElement.currentTime / videoElement.duration) * 100);
|
| 55 |
}
|
| 56 |
};
|
| 57 |
-
|
| 58 |
const handleLoadedMetadata = (videoElement) => {
|
| 59 |
if (videoElement) {
|
| 60 |
setDuration(videoElement.duration);
|
|
@@ -63,63 +68,132 @@ export default function MusicPlayer() {
|
|
| 63 |
videoElement.play();
|
| 64 |
}
|
| 65 |
};
|
| 66 |
-
|
| 67 |
const handleProgress = (videoElement) => {
|
| 68 |
if (videoElement && videoElement.buffered.length > 0) {
|
| 69 |
-
const bufferEnd = videoElement.buffered.end(
|
|
|
|
|
|
|
| 70 |
const bufferValue = (bufferEnd / videoElement.duration) * 100;
|
| 71 |
setBufferProgress(bufferValue);
|
| 72 |
}
|
| 73 |
};
|
| 74 |
-
|
| 75 |
const handlePlay = () => {
|
| 76 |
setIsPlaying(true);
|
| 77 |
setNowPlaying(title);
|
| 78 |
};
|
| 79 |
-
|
| 80 |
const handlePause = () => {
|
| 81 |
setIsPlaying(false);
|
| 82 |
setNowPlaying("");
|
| 83 |
};
|
| 84 |
-
|
| 85 |
const attachEventListeners = (videoElement) => {
|
| 86 |
if (videoElement) {
|
| 87 |
-
videoElement.addEventListener("timeupdate", () =>
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
videoElement.addEventListener("play", handlePlay);
|
| 91 |
videoElement.addEventListener("pause", handlePause);
|
| 92 |
}
|
| 93 |
};
|
| 94 |
-
|
| 95 |
const detachEventListeners = (videoElement) => {
|
| 96 |
if (videoElement) {
|
| 97 |
-
videoElement.removeEventListener("timeupdate", () =>
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
videoElement.removeEventListener("play", handlePlay);
|
| 101 |
videoElement.removeEventListener("pause", handlePause);
|
| 102 |
}
|
| 103 |
};
|
| 104 |
-
|
| 105 |
useEffect(() => {
|
| 106 |
const videoElement = videoRef.current;
|
| 107 |
-
|
| 108 |
// Attach event listeners
|
| 109 |
attachEventListeners(videoElement);
|
| 110 |
-
|
| 111 |
return () => {
|
| 112 |
// Detach event listeners
|
| 113 |
detachEventListeners(videoElement);
|
| 114 |
};
|
| 115 |
}, [videoRef, currentSrc, nowPlaying, didDestroy]);
|
| 116 |
-
|
| 117 |
useEffect(() => {
|
| 118 |
if (src !== currentSrc) {
|
| 119 |
setCurrentSrc(src);
|
| 120 |
}
|
| 121 |
}, [src, currentSrc]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
|
|
|
| 123 |
useEffect(() => {
|
| 124 |
if (showControls) {
|
| 125 |
if (overlayTimeout.current) {
|
|
@@ -127,10 +201,10 @@ export default function MusicPlayer() {
|
|
| 127 |
}
|
| 128 |
overlayTimeout.current = setTimeout(() => setShowControls(false), 3000);
|
| 129 |
}
|
| 130 |
-
|
| 131 |
return () => clearTimeout(overlayTimeout.current);
|
| 132 |
}, [showControls]);
|
| 133 |
-
|
| 134 |
const togglePlayPause = () => {
|
| 135 |
if (videoRef.current) {
|
| 136 |
if (isPlaying) {
|
|
@@ -141,20 +215,20 @@ export default function MusicPlayer() {
|
|
| 141 |
setIsPlaying(!isPlaying);
|
| 142 |
}
|
| 143 |
};
|
| 144 |
-
|
| 145 |
const handleVolumeChange = (event) => {
|
| 146 |
let volumeValue = parseFloat(event.target.value);
|
| 147 |
-
|
| 148 |
// Ensure the volume is a finite number between 0 and 1
|
| 149 |
volumeValue = Math.max(0, Math.min(1, volumeValue));
|
| 150 |
-
|
| 151 |
setVolume(volumeValue);
|
| 152 |
if (videoRef.current) {
|
| 153 |
videoRef.current.volume = volumeValue;
|
| 154 |
}
|
| 155 |
setIsMuted(volumeValue === 0);
|
| 156 |
};
|
| 157 |
-
|
| 158 |
const toggleMute = () => {
|
| 159 |
if (isMuted) {
|
| 160 |
videoRef.current.volume = volume;
|
|
@@ -164,35 +238,36 @@ export default function MusicPlayer() {
|
|
| 164 |
setIsMuted(true);
|
| 165 |
}
|
| 166 |
};
|
| 167 |
-
|
| 168 |
const toggleFullscreen = () => {
|
| 169 |
const doc = window.document;
|
| 170 |
const docEl = doc.documentElement;
|
| 171 |
-
|
| 172 |
const requestFullscreen =
|
| 173 |
docEl.requestFullscreen ||
|
| 174 |
docEl.mozRequestFullScreen ||
|
| 175 |
docEl.webkitRequestFullscreen ||
|
| 176 |
docEl.msRequestFullscreen;
|
| 177 |
-
|
| 178 |
const exitFullscreen =
|
| 179 |
doc.exitFullscreen ||
|
| 180 |
doc.mozCancelFullScreen ||
|
| 181 |
docEl.webkitExitFullscreen ||
|
| 182 |
doc.msExitFullscreen;
|
| 183 |
-
|
| 184 |
if (!isFullscreen) {
|
| 185 |
requestFullscreen.call(docEl);
|
| 186 |
} else {
|
| 187 |
exitFullscreen.call(doc);
|
| 188 |
}
|
| 189 |
-
|
| 190 |
setIsFullscreen(!isFullscreen);
|
| 191 |
};
|
| 192 |
-
|
| 193 |
const destroyPlayer = () => {
|
| 194 |
if (videoRef.current) {
|
| 195 |
videoRef.current.pause();
|
|
|
|
| 196 |
videoRef.current.removeAttribute("src"); // Clear the source
|
| 197 |
videoRef.current.load(); // Reload to reset duration/currentTime
|
| 198 |
setNowPlaying("");
|
|
@@ -207,7 +282,7 @@ export default function MusicPlayer() {
|
|
| 207 |
console.log("setting didDestroy to true");
|
| 208 |
}
|
| 209 |
};
|
| 210 |
-
|
| 211 |
const handleProgressClick = (e) => {
|
| 212 |
const progressBar = e.currentTarget;
|
| 213 |
const rect = progressBar.getBoundingClientRect();
|
|
@@ -218,7 +293,7 @@ export default function MusicPlayer() {
|
|
| 218 |
setCurrentTime(newTime);
|
| 219 |
}
|
| 220 |
};
|
| 221 |
-
|
| 222 |
const handleFastForward = () => {
|
| 223 |
if (videoRef.current) {
|
| 224 |
videoRef.current.currentTime = Math.min(
|
|
@@ -227,7 +302,7 @@ export default function MusicPlayer() {
|
|
| 227 |
);
|
| 228 |
}
|
| 229 |
};
|
| 230 |
-
|
| 231 |
const handleRewind = () => {
|
| 232 |
if (videoRef.current) {
|
| 233 |
videoRef.current.currentTime = Math.max(
|
|
@@ -236,13 +311,13 @@ export default function MusicPlayer() {
|
|
| 236 |
);
|
| 237 |
}
|
| 238 |
};
|
| 239 |
-
|
| 240 |
const handleMouseMove = () => {
|
| 241 |
setShowControls(true);
|
| 242 |
};
|
| 243 |
-
|
| 244 |
if (!isPlayerVisible || !currentSrc) return null;
|
| 245 |
-
|
| 246 |
return (
|
| 247 |
<div
|
| 248 |
className={
|
|
@@ -309,15 +384,17 @@ export default function MusicPlayer() {
|
|
| 309 |
onChange={handleVolumeChange}
|
| 310 |
/>
|
| 311 |
<button
|
| 312 |
-
onClick={
|
| 313 |
className="previous-btn player-min-button"
|
|
|
|
| 314 |
>
|
| 315 |
<TbPlayerSkipBack />
|
| 316 |
</button>
|
| 317 |
|
| 318 |
<button
|
| 319 |
-
onClick={
|
| 320 |
className="next-btn player-min-button"
|
|
|
|
| 321 |
>
|
| 322 |
<TbPlayerSkipForward />
|
| 323 |
</button>
|
|
@@ -357,7 +434,7 @@ export default function MusicPlayer() {
|
|
| 357 |
<video
|
| 358 |
ref={videoRef}
|
| 359 |
src={currentSrc}
|
| 360 |
-
poster=
|
| 361 |
preload="metadata"
|
| 362 |
className="video-element"
|
| 363 |
autoPlay
|
|
@@ -372,6 +449,14 @@ export default function MusicPlayer() {
|
|
| 372 |
</button>
|
| 373 |
</div>
|
| 374 |
<div className="player-mini-control-center">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
<button className="player-min-button" onClick={handleRewind}>
|
| 376 |
<TbRewindBackward5 />
|
| 377 |
</button>
|
|
@@ -381,6 +466,14 @@ export default function MusicPlayer() {
|
|
| 381 |
<button className="player-min-button" onClick={handleFastForward}>
|
| 382 |
<TbRewindForward5 />
|
| 383 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
<button className="player-min-button" onClick={destroyPlayer}>
|
| 385 |
<TbPlayerStop />
|
| 386 |
</button>
|
|
|
|
| 29 |
title,
|
| 30 |
setNowPlaying,
|
| 31 |
nowPlaying,
|
| 32 |
+
didDestroy,
|
| 33 |
setDidDestroy,
|
| 34 |
+
playNext,
|
| 35 |
+
playPrevious,
|
| 36 |
+
canPlayPrevious,
|
| 37 |
+
canPlayNext,
|
| 38 |
} = useMusicPlayer();
|
| 39 |
+
|
| 40 |
const [currentSrc, setCurrentSrc] = useState(src);
|
| 41 |
const [currentTime, setCurrentTime] = useState(0);
|
| 42 |
const [duration, setDuration] = useState(0);
|
|
|
|
| 50 |
const [isFullscreen, setIsFullscreen] = useState(false);
|
| 51 |
const overlayTimeout = useRef(null);
|
| 52 |
const seekTime = 5;
|
| 53 |
+
const poster = "https://dlcdnwebimgs.asus.com/gain/4BB18AEF-347E-4DB6-B78C-C0FFE1F20385/w750/h470"
|
| 54 |
+
|
| 55 |
// Event Handlers
|
| 56 |
const handleTimeUpdate = (videoElement) => {
|
| 57 |
if (videoElement) {
|
|
|
|
| 59 |
setProgress((videoElement.currentTime / videoElement.duration) * 100);
|
| 60 |
}
|
| 61 |
};
|
| 62 |
+
|
| 63 |
const handleLoadedMetadata = (videoElement) => {
|
| 64 |
if (videoElement) {
|
| 65 |
setDuration(videoElement.duration);
|
|
|
|
| 68 |
videoElement.play();
|
| 69 |
}
|
| 70 |
};
|
| 71 |
+
|
| 72 |
const handleProgress = (videoElement) => {
|
| 73 |
if (videoElement && videoElement.buffered.length > 0) {
|
| 74 |
+
const bufferEnd = videoElement.buffered.end(
|
| 75 |
+
videoElement.buffered.length - 1
|
| 76 |
+
);
|
| 77 |
const bufferValue = (bufferEnd / videoElement.duration) * 100;
|
| 78 |
setBufferProgress(bufferValue);
|
| 79 |
}
|
| 80 |
};
|
| 81 |
+
|
| 82 |
const handlePlay = () => {
|
| 83 |
setIsPlaying(true);
|
| 84 |
setNowPlaying(title);
|
| 85 |
};
|
| 86 |
+
|
| 87 |
const handlePause = () => {
|
| 88 |
setIsPlaying(false);
|
| 89 |
setNowPlaying("");
|
| 90 |
};
|
| 91 |
+
|
| 92 |
const attachEventListeners = (videoElement) => {
|
| 93 |
if (videoElement) {
|
| 94 |
+
videoElement.addEventListener("timeupdate", () =>
|
| 95 |
+
handleTimeUpdate(videoElement)
|
| 96 |
+
);
|
| 97 |
+
videoElement.addEventListener("loadedmetadata", () =>
|
| 98 |
+
handleLoadedMetadata(videoElement)
|
| 99 |
+
);
|
| 100 |
+
videoElement.addEventListener("progress", () =>
|
| 101 |
+
handleProgress(videoElement)
|
| 102 |
+
);
|
| 103 |
videoElement.addEventListener("play", handlePlay);
|
| 104 |
videoElement.addEventListener("pause", handlePause);
|
| 105 |
}
|
| 106 |
};
|
| 107 |
+
|
| 108 |
const detachEventListeners = (videoElement) => {
|
| 109 |
if (videoElement) {
|
| 110 |
+
videoElement.removeEventListener("timeupdate", () =>
|
| 111 |
+
handleTimeUpdate(videoElement)
|
| 112 |
+
);
|
| 113 |
+
videoElement.removeEventListener("loadedmetadata", () =>
|
| 114 |
+
handleLoadedMetadata(videoElement)
|
| 115 |
+
);
|
| 116 |
+
videoElement.removeEventListener("progress", () =>
|
| 117 |
+
handleProgress(videoElement)
|
| 118 |
+
);
|
| 119 |
videoElement.removeEventListener("play", handlePlay);
|
| 120 |
videoElement.removeEventListener("pause", handlePause);
|
| 121 |
}
|
| 122 |
};
|
| 123 |
+
|
| 124 |
useEffect(() => {
|
| 125 |
const videoElement = videoRef.current;
|
| 126 |
+
|
| 127 |
// Attach event listeners
|
| 128 |
attachEventListeners(videoElement);
|
| 129 |
+
|
| 130 |
return () => {
|
| 131 |
// Detach event listeners
|
| 132 |
detachEventListeners(videoElement);
|
| 133 |
};
|
| 134 |
}, [videoRef, currentSrc, nowPlaying, didDestroy]);
|
| 135 |
+
|
| 136 |
useEffect(() => {
|
| 137 |
if (src !== currentSrc) {
|
| 138 |
setCurrentSrc(src);
|
| 139 |
}
|
| 140 |
}, [src, currentSrc]);
|
| 141 |
+
|
| 142 |
+
useEffect(() => {
|
| 143 |
+
if ('mediaSession' in navigator) {
|
| 144 |
+
navigator.mediaSession.metadata = new MediaMetadata({
|
| 145 |
+
title: title,
|
| 146 |
+
artwork: [
|
| 147 |
+
{ src: poster } // Ensure 'poster' is defined in your component
|
| 148 |
+
]
|
| 149 |
+
});
|
| 150 |
+
|
| 151 |
+
navigator.mediaSession.setActionHandler('play', () => {
|
| 152 |
+
videoRef.current.play();
|
| 153 |
+
});
|
| 154 |
+
|
| 155 |
+
navigator.mediaSession.setActionHandler('pause', () => {
|
| 156 |
+
videoRef.current.pause();
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
navigator.mediaSession.setActionHandler('seekbackward', (details) => {
|
| 160 |
+
videoRef.current.currentTime = Math.max(videoRef.current.currentTime - (details.seekOffset || 10), 0);
|
| 161 |
+
});
|
| 162 |
+
|
| 163 |
+
navigator.mediaSession.setActionHandler('seekforward', (details) => {
|
| 164 |
+
videoRef.current.currentTime = Math.min(videoRef.current.currentTime + (details.seekOffset || 10), videoRef.current.duration);
|
| 165 |
+
});
|
| 166 |
+
|
| 167 |
+
navigator.mediaSession.setActionHandler('stop', () => {
|
| 168 |
+
destroyPlayer();
|
| 169 |
+
});
|
| 170 |
+
|
| 171 |
+
// New action handlers for previous and next
|
| 172 |
+
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
| 173 |
+
playPrevious(); // Assuming playPrevious is a function that plays the previous track
|
| 174 |
+
});
|
| 175 |
+
|
| 176 |
+
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
| 177 |
+
playNext(); // Assuming playNext is a function that plays the next track
|
| 178 |
+
});
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// Cleanup function to reset media session when component unmounts
|
| 182 |
+
return () => {
|
| 183 |
+
if ('mediaSession' in navigator) {
|
| 184 |
+
navigator.mediaSession.metadata = null;
|
| 185 |
+
navigator.mediaSession.setActionHandler('play', null);
|
| 186 |
+
navigator.mediaSession.setActionHandler('pause', null);
|
| 187 |
+
navigator.mediaSession.setActionHandler('seekbackward', null);
|
| 188 |
+
navigator.mediaSession.setActionHandler('seekforward', null);
|
| 189 |
+
navigator.mediaSession.setActionHandler('stop', null);
|
| 190 |
+
navigator.mediaSession.setActionHandler('previoustrack', null);
|
| 191 |
+
navigator.mediaSession.setActionHandler('nexttrack', null);
|
| 192 |
+
}
|
| 193 |
+
};
|
| 194 |
+
}, [title, poster, playPrevious, playNext]);
|
| 195 |
|
| 196 |
+
|
| 197 |
useEffect(() => {
|
| 198 |
if (showControls) {
|
| 199 |
if (overlayTimeout.current) {
|
|
|
|
| 201 |
}
|
| 202 |
overlayTimeout.current = setTimeout(() => setShowControls(false), 3000);
|
| 203 |
}
|
| 204 |
+
|
| 205 |
return () => clearTimeout(overlayTimeout.current);
|
| 206 |
}, [showControls]);
|
| 207 |
+
|
| 208 |
const togglePlayPause = () => {
|
| 209 |
if (videoRef.current) {
|
| 210 |
if (isPlaying) {
|
|
|
|
| 215 |
setIsPlaying(!isPlaying);
|
| 216 |
}
|
| 217 |
};
|
| 218 |
+
|
| 219 |
const handleVolumeChange = (event) => {
|
| 220 |
let volumeValue = parseFloat(event.target.value);
|
| 221 |
+
|
| 222 |
// Ensure the volume is a finite number between 0 and 1
|
| 223 |
volumeValue = Math.max(0, Math.min(1, volumeValue));
|
| 224 |
+
|
| 225 |
setVolume(volumeValue);
|
| 226 |
if (videoRef.current) {
|
| 227 |
videoRef.current.volume = volumeValue;
|
| 228 |
}
|
| 229 |
setIsMuted(volumeValue === 0);
|
| 230 |
};
|
| 231 |
+
|
| 232 |
const toggleMute = () => {
|
| 233 |
if (isMuted) {
|
| 234 |
videoRef.current.volume = volume;
|
|
|
|
| 238 |
setIsMuted(true);
|
| 239 |
}
|
| 240 |
};
|
| 241 |
+
|
| 242 |
const toggleFullscreen = () => {
|
| 243 |
const doc = window.document;
|
| 244 |
const docEl = doc.documentElement;
|
| 245 |
+
|
| 246 |
const requestFullscreen =
|
| 247 |
docEl.requestFullscreen ||
|
| 248 |
docEl.mozRequestFullScreen ||
|
| 249 |
docEl.webkitRequestFullscreen ||
|
| 250 |
docEl.msRequestFullscreen;
|
| 251 |
+
|
| 252 |
const exitFullscreen =
|
| 253 |
doc.exitFullscreen ||
|
| 254 |
doc.mozCancelFullScreen ||
|
| 255 |
docEl.webkitExitFullscreen ||
|
| 256 |
doc.msExitFullscreen;
|
| 257 |
+
|
| 258 |
if (!isFullscreen) {
|
| 259 |
requestFullscreen.call(docEl);
|
| 260 |
} else {
|
| 261 |
exitFullscreen.call(doc);
|
| 262 |
}
|
| 263 |
+
|
| 264 |
setIsFullscreen(!isFullscreen);
|
| 265 |
};
|
| 266 |
+
|
| 267 |
const destroyPlayer = () => {
|
| 268 |
if (videoRef.current) {
|
| 269 |
videoRef.current.pause();
|
| 270 |
+
videoRef.current.currentTime = 0;
|
| 271 |
videoRef.current.removeAttribute("src"); // Clear the source
|
| 272 |
videoRef.current.load(); // Reload to reset duration/currentTime
|
| 273 |
setNowPlaying("");
|
|
|
|
| 282 |
console.log("setting didDestroy to true");
|
| 283 |
}
|
| 284 |
};
|
| 285 |
+
|
| 286 |
const handleProgressClick = (e) => {
|
| 287 |
const progressBar = e.currentTarget;
|
| 288 |
const rect = progressBar.getBoundingClientRect();
|
|
|
|
| 293 |
setCurrentTime(newTime);
|
| 294 |
}
|
| 295 |
};
|
| 296 |
+
|
| 297 |
const handleFastForward = () => {
|
| 298 |
if (videoRef.current) {
|
| 299 |
videoRef.current.currentTime = Math.min(
|
|
|
|
| 302 |
);
|
| 303 |
}
|
| 304 |
};
|
| 305 |
+
|
| 306 |
const handleRewind = () => {
|
| 307 |
if (videoRef.current) {
|
| 308 |
videoRef.current.currentTime = Math.max(
|
|
|
|
| 311 |
);
|
| 312 |
}
|
| 313 |
};
|
| 314 |
+
|
| 315 |
const handleMouseMove = () => {
|
| 316 |
setShowControls(true);
|
| 317 |
};
|
| 318 |
+
|
| 319 |
if (!isPlayerVisible || !currentSrc) return null;
|
| 320 |
+
|
| 321 |
return (
|
| 322 |
<div
|
| 323 |
className={
|
|
|
|
| 384 |
onChange={handleVolumeChange}
|
| 385 |
/>
|
| 386 |
<button
|
| 387 |
+
onClick={playPrevious}
|
| 388 |
className="previous-btn player-min-button"
|
| 389 |
+
disabled={!canPlayPrevious}
|
| 390 |
>
|
| 391 |
<TbPlayerSkipBack />
|
| 392 |
</button>
|
| 393 |
|
| 394 |
<button
|
| 395 |
+
onClick={playNext}
|
| 396 |
className="next-btn player-min-button"
|
| 397 |
+
disabled={!canPlayNext}
|
| 398 |
>
|
| 399 |
<TbPlayerSkipForward />
|
| 400 |
</button>
|
|
|
|
| 434 |
<video
|
| 435 |
ref={videoRef}
|
| 436 |
src={currentSrc}
|
| 437 |
+
poster={poster}
|
| 438 |
preload="metadata"
|
| 439 |
className="video-element"
|
| 440 |
autoPlay
|
|
|
|
| 449 |
</button>
|
| 450 |
</div>
|
| 451 |
<div className="player-mini-control-center">
|
| 452 |
+
<button
|
| 453 |
+
onClick={playPrevious}
|
| 454 |
+
className="previous-btn player-min-button"
|
| 455 |
+
disabled={!canPlayPrevious}
|
| 456 |
+
>
|
| 457 |
+
<TbPlayerSkipBack />
|
| 458 |
+
</button>
|
| 459 |
+
|
| 460 |
<button className="player-min-button" onClick={handleRewind}>
|
| 461 |
<TbRewindBackward5 />
|
| 462 |
</button>
|
|
|
|
| 466 |
<button className="player-min-button" onClick={handleFastForward}>
|
| 467 |
<TbRewindForward5 />
|
| 468 |
</button>
|
| 469 |
+
|
| 470 |
+
<button
|
| 471 |
+
onClick={playNext}
|
| 472 |
+
className="next-btn player-min-button"
|
| 473 |
+
disabled={!canPlayNext}
|
| 474 |
+
>
|
| 475 |
+
<TbPlayerSkipForward />
|
| 476 |
+
</button>
|
| 477 |
<button className="player-min-button" onClick={destroyPlayer}>
|
| 478 |
<TbPlayerStop />
|
| 479 |
</button>
|
frontend/src/components/Playlist.css
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.playlist-container {
|
| 2 |
+
padding: 20px;
|
| 3 |
+
background-color: var(--foreground-1);
|
| 4 |
+
border-radius: 10px;
|
| 5 |
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
| 6 |
+
width: 100%;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
.playlist {
|
| 10 |
+
list-style: none;
|
| 11 |
+
padding: 0;
|
| 12 |
+
margin: 0;
|
| 13 |
+
max-height: 30vh;
|
| 14 |
+
overflow-y: auto;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/* For WebKit browsers (Chrome, Edge, Safari) */
|
| 18 |
+
.playlist::-webkit-scrollbar {
|
| 19 |
+
width: 8px; /* Width of the scrollbar */
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.playlist::-webkit-scrollbar-track {
|
| 23 |
+
background: #222; /* Track color */
|
| 24 |
+
border-radius: 4px;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.playlist::-webkit-scrollbar-thumb {
|
| 28 |
+
background-color: #888; /* Scrollbar thumb color */
|
| 29 |
+
border-radius: 4px;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.playlist::-webkit-scrollbar-thumb:hover {
|
| 33 |
+
background-color: #555; /* Thumb color on hover */
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.playlist-item {
|
| 37 |
+
display: flex;
|
| 38 |
+
justify-content: space-between;
|
| 39 |
+
align-items: center;
|
| 40 |
+
padding: 10px;
|
| 41 |
+
border-bottom: 1px solid var(--border-color);
|
| 42 |
+
transition: background-color 0.3s; /* Smooth transition for background change */
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.playlist-item.playing {
|
| 46 |
+
background-color: var(--highlight-color);
|
| 47 |
+
color: var(--text-highlight);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.track-title {
|
| 51 |
+
cursor: pointer;
|
| 52 |
+
font-size: 1rem;
|
| 53 |
+
color: var(--text-color);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.remove-button,
|
| 57 |
+
.save-button,
|
| 58 |
+
.clear-button {
|
| 59 |
+
background: none;
|
| 60 |
+
border: none;
|
| 61 |
+
cursor: pointer;
|
| 62 |
+
color: var(--foreground-secondary);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.remove-button:hover,
|
| 66 |
+
.clear-button:hover {
|
| 67 |
+
color: red; /* Change color on hover */
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.playlist-action-container{
|
| 71 |
+
display: flex;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.save-button {
|
| 75 |
+
margin-left: 10px; /* Add some spacing */
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.clear-button {
|
| 79 |
+
margin-left: 10px; /* Add some spacing */
|
| 80 |
+
display: flex;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.modal {
|
| 84 |
+
position: fixed;
|
| 85 |
+
top: 0;
|
| 86 |
+
left: 0;
|
| 87 |
+
right: 0;
|
| 88 |
+
bottom: 0;
|
| 89 |
+
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent background */
|
| 90 |
+
display: flex;
|
| 91 |
+
align-items: center;
|
| 92 |
+
justify-content: center;
|
| 93 |
+
z-index: 1000; /* Ensure modal is on top */
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.modal-content {
|
| 97 |
+
background: var(--foreground-5);
|
| 98 |
+
padding: 20px;
|
| 99 |
+
border-radius: 10px;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.modal-close-button {
|
| 103 |
+
background: none;
|
| 104 |
+
border: none;
|
| 105 |
+
color: var(--foreground-secondary);
|
| 106 |
+
cursor: pointer;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.save-confirm-button {
|
| 110 |
+
background-color: var(--primary-color);
|
| 111 |
+
color: white;
|
| 112 |
+
border: none;
|
| 113 |
+
padding: 5px 10px;
|
| 114 |
+
border-radius: 5px;
|
| 115 |
+
cursor: pointer;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.save-confirm-button:hover {
|
| 119 |
+
background-color: var(--primary-color-dark); /* Darker shade on hover */
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.playlist-name-input {
|
| 123 |
+
background: var(--foreground-3);
|
| 124 |
+
padding: 5px;
|
| 125 |
+
border-radius: 5px;
|
| 126 |
+
}
|
frontend/src/components/Playlist.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
import React, { useState } from "react";
|
| 3 |
+
import { useMusicPlayer } from "@/context/MusicPlayerContext";
|
| 4 |
+
import { formatTitle } from "@/lib/utils";
|
| 5 |
+
import { FaTrash } from "react-icons/fa";
|
| 6 |
+
import { TbPlaylistX } from "react-icons/tb";
|
| 7 |
+
import "./Playlist.css";
|
| 8 |
+
|
| 9 |
+
const Playlist = () => {
|
| 10 |
+
const { playlist, initializePlayer, removeFromPlaylist, currentIndex, setPlaylist } = useMusicPlayer();
|
| 11 |
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
| 12 |
+
const [playlistName, setPlaylistName] = useState("");
|
| 13 |
+
|
| 14 |
+
const handlePlayTrack = (src, title) => {
|
| 15 |
+
initializePlayer(src, title);
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
const handleClearPlaylist = () => {
|
| 19 |
+
if (window.confirm("Are you sure you want to clear the playlist?")) {
|
| 20 |
+
setPlaylist([]); // Clear the playlist and trigger a re-render
|
| 21 |
+
}
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
const handleSavePlaylist = () => {
|
| 25 |
+
if (!playlistName.trim()) return;
|
| 26 |
+
const existingPlaylists = JSON.parse(localStorage.getItem("playlists")) || {};
|
| 27 |
+
existingPlaylists[playlistName] = playlist;
|
| 28 |
+
localStorage.setItem("playlists", JSON.stringify(existingPlaylists));
|
| 29 |
+
setIsModalOpen(false);
|
| 30 |
+
setPlaylistName("");
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
return (
|
| 34 |
+
<div className="playlist-container">
|
| 35 |
+
<h2>Your Playlist</h2>
|
| 36 |
+
<div className="playlist-action-container">
|
| 37 |
+
<button onClick={() => setIsModalOpen(true)} className="save-button">
|
| 38 |
+
Save Playlist
|
| 39 |
+
</button>
|
| 40 |
+
<button onClick={handleClearPlaylist} className="clear-button">
|
| 41 |
+
<TbPlaylistX /> Clear Playlist
|
| 42 |
+
</button>
|
| 43 |
+
</div>
|
| 44 |
+
{isModalOpen && (
|
| 45 |
+
<div className="modal">
|
| 46 |
+
<div className="modal-content">
|
| 47 |
+
<h3>Save Playlist</h3>
|
| 48 |
+
<input
|
| 49 |
+
className="playlist-name-input"
|
| 50 |
+
type="text"
|
| 51 |
+
placeholder="Enter playlist name"
|
| 52 |
+
value={playlistName}
|
| 53 |
+
onChange={(e) => setPlaylistName(e.target.value)}
|
| 54 |
+
/>
|
| 55 |
+
<button onClick={handleSavePlaylist} className="save-confirm-button">
|
| 56 |
+
Save
|
| 57 |
+
</button>
|
| 58 |
+
<button onClick={() => setIsModalOpen(false)} className="modal-close-button">
|
| 59 |
+
Cancel
|
| 60 |
+
</button>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
)}
|
| 64 |
+
|
| 65 |
+
{playlist.length === 0 ? (
|
| 66 |
+
<p>No tracks in the playlist</p>
|
| 67 |
+
) : (
|
| 68 |
+
<ul className="playlist">
|
| 69 |
+
{playlist.map((track, index) => (
|
| 70 |
+
<li
|
| 71 |
+
key={index}
|
| 72 |
+
className={`playlist-item ${currentIndex === index ? "playing" : ""}`}
|
| 73 |
+
>
|
| 74 |
+
<div className="track-info">
|
| 75 |
+
<span
|
| 76 |
+
className="track-title"
|
| 77 |
+
onClick={() => handlePlayTrack(track.source, track.title)}
|
| 78 |
+
>
|
| 79 |
+
{formatTitle(track.source)}
|
| 80 |
+
</span>
|
| 81 |
+
</div>
|
| 82 |
+
<button className="remove-button" onClick={() => removeFromPlaylist(track.source)}>
|
| 83 |
+
<FaTrash />
|
| 84 |
+
</button>
|
| 85 |
+
</li>
|
| 86 |
+
))}
|
| 87 |
+
</ul>
|
| 88 |
+
)}
|
| 89 |
+
</div>
|
| 90 |
+
);
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
export default Playlist;
|
frontend/src/components/SavedPlaylists.css
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.saved-playlists-container {
|
| 2 |
+
padding: 20px;
|
| 3 |
+
background-color: var(--foreground-1);
|
| 4 |
+
border-radius: 10px;
|
| 5 |
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
.saved-playlists {
|
| 9 |
+
list-style: none;
|
| 10 |
+
padding: 0;
|
| 11 |
+
margin: 0;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
.saved-playlist-item {
|
| 15 |
+
display: flex;
|
| 16 |
+
justify-content: space-between;
|
| 17 |
+
align-items: center;
|
| 18 |
+
padding: 10px;
|
| 19 |
+
border-bottom: 1px solid var(--border-color);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.playlist-name {
|
| 23 |
+
font-size: 1rem;
|
| 24 |
+
color: var(--text-color);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.playlist-controls {
|
| 28 |
+
display: flex;
|
| 29 |
+
gap: 5px;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.play-button,
|
| 33 |
+
.edit-button,
|
| 34 |
+
.remove-button {
|
| 35 |
+
background: none;
|
| 36 |
+
border: none;
|
| 37 |
+
cursor: pointer;
|
| 38 |
+
color: var(--foreground-secondary);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.play-button:hover {
|
| 42 |
+
color: green; /* Change color on hover */
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.edit-button:hover {
|
| 46 |
+
color: blue; /* Change color on hover */
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.remove-button:hover {
|
| 50 |
+
color: red; /* Change color on hover */
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.delete-all-button {
|
| 54 |
+
margin-bottom: 10px;
|
| 55 |
+
background-color: red;
|
| 56 |
+
color: white;
|
| 57 |
+
border: none;
|
| 58 |
+
padding: 5px 10px;
|
| 59 |
+
border-radius: 5px;
|
| 60 |
+
cursor: pointer;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.delete-all-button:hover {
|
| 64 |
+
background-color: darkred; /* Darker shade on hover */
|
| 65 |
+
}
|
frontend/src/components/SavedPlaylists.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
import React, { useState } from "react";
|
| 3 |
+
import { useMusicPlayer } from "@/context/MusicPlayerContext";
|
| 4 |
+
import { formatTitle } from "@/lib/utils";
|
| 5 |
+
import { FaTrash, FaEdit } from "react-icons/fa"; // Import icons for edit and delete
|
| 6 |
+
import "./SavedPlaylists.css"; // Import a CSS file for styling
|
| 7 |
+
|
| 8 |
+
const SavedPlaylists = () => {
|
| 9 |
+
const { initializePlayer, addToPlaylist } = useMusicPlayer();
|
| 10 |
+
const [playlists, setPlaylists] = useState(() => JSON.parse(localStorage.getItem("playlists")) || {});
|
| 11 |
+
|
| 12 |
+
const handlePlayPlaylist = (tracks) => {
|
| 13 |
+
// Check if there's at least one track to play
|
| 14 |
+
if (tracks.length > 0) {
|
| 15 |
+
// Initialize the player with the first track
|
| 16 |
+
initializePlayer(tracks[0].source, tracks[0].title); // Start playing the first track
|
| 17 |
+
|
| 18 |
+
// Add the rest of the tracks to the playlist
|
| 19 |
+
tracks.slice(1).forEach(track => {
|
| 20 |
+
addToPlaylist({ source: track.source, title: track.title });
|
| 21 |
+
});
|
| 22 |
+
}
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
const handleDeletePlaylist = (name) => {
|
| 26 |
+
const updatedPlaylists = { ...playlists };
|
| 27 |
+
delete updatedPlaylists[name];
|
| 28 |
+
setPlaylists(updatedPlaylists);
|
| 29 |
+
localStorage.setItem("playlists", JSON.stringify(updatedPlaylists));
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
const handleDeleteAllPlaylists = () => {
|
| 33 |
+
setPlaylists({});
|
| 34 |
+
localStorage.removeItem("playlists");
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
const handleEditPlaylist = (name) => {
|
| 38 |
+
// Logic to edit the playlist can be implemented as needed
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
return (
|
| 42 |
+
<div className="saved-playlists-container">
|
| 43 |
+
<h2>Saved Playlists</h2>
|
| 44 |
+
<button onClick={handleDeleteAllPlaylists} className="delete-all-button">Delete All Playlists</button>
|
| 45 |
+
{Object.keys(playlists).length === 0 ? (
|
| 46 |
+
<p>No saved playlists</p>
|
| 47 |
+
) : (
|
| 48 |
+
<ul className="saved-playlists">
|
| 49 |
+
{Object.entries(playlists).map(([name, tracks]) => (
|
| 50 |
+
<li key={name} className="saved-playlist-item">
|
| 51 |
+
<span className="playlist-name">{name}</span>
|
| 52 |
+
<div className="playlist-controls">
|
| 53 |
+
<button onClick={() => handlePlayPlaylist(tracks)} className="play-button">Play</button>
|
| 54 |
+
<button onClick={() => handleEditPlaylist(name)} className="edit-button"><FaEdit /></button>
|
| 55 |
+
<button onClick={() => handleDeletePlaylist(name)} className="remove-button"><FaTrash /></button>
|
| 56 |
+
</div>
|
| 57 |
+
</li>
|
| 58 |
+
))}
|
| 59 |
+
</ul>
|
| 60 |
+
)}
|
| 61 |
+
</div>
|
| 62 |
+
);
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
export default SavedPlaylists;
|
frontend/src/context/MusicPlayerContext.js
CHANGED
|
@@ -10,14 +10,33 @@ export const MusicPlayerProvider = ({ children }) => {
|
|
| 10 |
const [isPlayerVisible, setIsPlayerVisible] = useState(false);
|
| 11 |
const [isPlayerMaximized, setIsPlayerMaximized] = useState(false);
|
| 12 |
const [loadingProgress, setLoadingProgress] = useState(null);
|
| 13 |
-
const [abortController, setAbortController] = useState(null);
|
| 14 |
const [didDestroy, setDidDestroy] = useState(false);
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
const initializePlayer = async (source, title) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
if (abortController) {
|
| 18 |
abortController.abort();
|
| 19 |
}
|
| 20 |
-
|
| 21 |
const newAbortController = new AbortController();
|
| 22 |
setAbortController(newAbortController);
|
| 23 |
|
|
@@ -26,7 +45,7 @@ export const MusicPlayerProvider = ({ children }) => {
|
|
| 26 |
if (videoRef.current) {
|
| 27 |
videoRef.current.pause();
|
| 28 |
}
|
| 29 |
-
|
| 30 |
try {
|
| 31 |
const response = await fetch(`/api/get/music/${encodeURIComponent(extractedFileName)}`, {
|
| 32 |
signal: newAbortController.signal,
|
|
@@ -97,7 +116,6 @@ export const MusicPlayerProvider = ({ children }) => {
|
|
| 97 |
|
| 98 |
const startPlayer = (source) => {
|
| 99 |
setDidDestroy(false);
|
| 100 |
-
console.log("setting didDestroy to false");
|
| 101 |
setSrc(source);
|
| 102 |
setIsPlayerVisible(true);
|
| 103 |
if (videoRef.current) {
|
|
@@ -108,6 +126,58 @@ export const MusicPlayerProvider = ({ children }) => {
|
|
| 108 |
|
| 109 |
const togglePlayerSize = () => setIsPlayerMaximized(!isPlayerMaximized);
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
return (
|
| 112 |
<MusicPlayerContext.Provider
|
| 113 |
value={{
|
|
@@ -124,6 +194,16 @@ export const MusicPlayerProvider = ({ children }) => {
|
|
| 124 |
setNowPlaying,
|
| 125 |
didDestroy,
|
| 126 |
setDidDestroy,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
}}
|
| 128 |
>
|
| 129 |
{children}
|
|
|
|
| 10 |
const [isPlayerVisible, setIsPlayerVisible] = useState(false);
|
| 11 |
const [isPlayerMaximized, setIsPlayerMaximized] = useState(false);
|
| 12 |
const [loadingProgress, setLoadingProgress] = useState(null);
|
| 13 |
+
const [abortController, setAbortController] = useState(null);
|
| 14 |
const [didDestroy, setDidDestroy] = useState(false);
|
| 15 |
|
| 16 |
+
// Playlist and index
|
| 17 |
+
const [playlist, setPlaylist] = useState([]);
|
| 18 |
+
const [currentIndex, setCurrentIndex] = useState(0);
|
| 19 |
+
|
| 20 |
+
const canPlayPrevious = currentIndex > 0;
|
| 21 |
+
const canPlayNext = currentIndex < playlist.length - 1;
|
| 22 |
+
|
| 23 |
const initializePlayer = async (source, title) => {
|
| 24 |
+
// Check if song already exists in playlist
|
| 25 |
+
const songIndex = playlist.findIndex(song => song.source === source);
|
| 26 |
+
|
| 27 |
+
if (songIndex !== -1) {
|
| 28 |
+
setCurrentIndex(songIndex); // Set to existing song index
|
| 29 |
+
} else {
|
| 30 |
+
// Add new song and set to that new index
|
| 31 |
+
const newSong = { source, title };
|
| 32 |
+
setPlaylist((prev) => [...prev, newSong]);
|
| 33 |
+
setCurrentIndex(playlist.length); // Set index to the end (new song position)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
if (abortController) {
|
| 37 |
abortController.abort();
|
| 38 |
}
|
| 39 |
+
|
| 40 |
const newAbortController = new AbortController();
|
| 41 |
setAbortController(newAbortController);
|
| 42 |
|
|
|
|
| 45 |
if (videoRef.current) {
|
| 46 |
videoRef.current.pause();
|
| 47 |
}
|
| 48 |
+
|
| 49 |
try {
|
| 50 |
const response = await fetch(`/api/get/music/${encodeURIComponent(extractedFileName)}`, {
|
| 51 |
signal: newAbortController.signal,
|
|
|
|
| 116 |
|
| 117 |
const startPlayer = (source) => {
|
| 118 |
setDidDestroy(false);
|
|
|
|
| 119 |
setSrc(source);
|
| 120 |
setIsPlayerVisible(true);
|
| 121 |
if (videoRef.current) {
|
|
|
|
| 126 |
|
| 127 |
const togglePlayerSize = () => setIsPlayerMaximized(!isPlayerMaximized);
|
| 128 |
|
| 129 |
+
const addToPlaylist = (song) => {
|
| 130 |
+
const songIndex = playlist.findIndex(track => track.source === song.source);
|
| 131 |
+
if (songIndex === -1) {
|
| 132 |
+
setPlaylist((prev) => [...prev, song]);
|
| 133 |
+
}
|
| 134 |
+
};
|
| 135 |
+
|
| 136 |
+
const removeFromPlaylist = (source) => {
|
| 137 |
+
setPlaylist((prev) => prev.filter(track => track.source !== source));
|
| 138 |
+
if (currentIndex >= playlist.length) {
|
| 139 |
+
setCurrentIndex(playlist.length - 1);
|
| 140 |
+
}
|
| 141 |
+
};
|
| 142 |
+
|
| 143 |
+
const playNext = () => {
|
| 144 |
+
if (canPlayNext) {
|
| 145 |
+
const nextIndex = currentIndex + 1;
|
| 146 |
+
setCurrentIndex(nextIndex);
|
| 147 |
+
const { source, title } = playlist[nextIndex];
|
| 148 |
+
initializePlayer(source, title);
|
| 149 |
+
}
|
| 150 |
+
};
|
| 151 |
+
|
| 152 |
+
const playPrevious = () => {
|
| 153 |
+
if (canPlayPrevious) {
|
| 154 |
+
const prevIndex = currentIndex - 1;
|
| 155 |
+
setCurrentIndex(prevIndex);
|
| 156 |
+
const { source, title } = playlist[prevIndex];
|
| 157 |
+
initializePlayer(source, title);
|
| 158 |
+
}
|
| 159 |
+
};
|
| 160 |
+
|
| 161 |
+
const playAtIndex = (index) => {
|
| 162 |
+
if (index >= 0 && index < playlist.length) {
|
| 163 |
+
setCurrentIndex(index);
|
| 164 |
+
const { source, title } = playlist[index];
|
| 165 |
+
initializePlayer(source, title);
|
| 166 |
+
}
|
| 167 |
+
};
|
| 168 |
+
|
| 169 |
+
useEffect(() => {
|
| 170 |
+
const handleEnded = () => playNext();
|
| 171 |
+
if (videoRef.current) {
|
| 172 |
+
videoRef.current.addEventListener("ended", handleEnded);
|
| 173 |
+
}
|
| 174 |
+
return () => {
|
| 175 |
+
if (videoRef.current) {
|
| 176 |
+
videoRef.current.removeEventListener("ended", handleEnded);
|
| 177 |
+
}
|
| 178 |
+
};
|
| 179 |
+
}, [currentIndex, playlist]);
|
| 180 |
+
|
| 181 |
return (
|
| 182 |
<MusicPlayerContext.Provider
|
| 183 |
value={{
|
|
|
|
| 194 |
setNowPlaying,
|
| 195 |
didDestroy,
|
| 196 |
setDidDestroy,
|
| 197 |
+
playlist,
|
| 198 |
+
setPlaylist,
|
| 199 |
+
addToPlaylist,
|
| 200 |
+
removeFromPlaylist,
|
| 201 |
+
playNext,
|
| 202 |
+
playPrevious,
|
| 203 |
+
playAtIndex,
|
| 204 |
+
currentIndex,
|
| 205 |
+
canPlayPrevious,
|
| 206 |
+
canPlayNext,
|
| 207 |
}}
|
| 208 |
>
|
| 209 |
{children}
|