diff --git a/src/app/Router.tsx b/src/app/Router.tsx
index eede8a2..02ee160 100644
--- a/src/app/Router.tsx
+++ b/src/app/Router.tsx
@@ -12,6 +12,7 @@ import ErrorScreen from "./pages/ErrorScreen";
import Account from "./pages/Account";
import Player from "./components/Player";
import Register from "./pages/Register";
+import PlayerMaximized from "./pages/PlayerMaximized";
export default function createRouter() {
const router = createRoutesFromElements(
@@ -47,6 +48,10 @@ export default function createRouter() {
path='/register'
element={}
/>
+ }
+ />
);
diff --git a/src/app/components/AddPlaylistButton.tsx b/src/app/components/AddPlaylistButton.tsx
index f13ccdb..4b25389 100644
--- a/src/app/components/AddPlaylistButton.tsx
+++ b/src/app/components/AddPlaylistButton.tsx
@@ -1,36 +1,76 @@
import { Add } from "@mui/icons-material";
-import { Box, BoxProps, Theme, Typography, useTheme } from "@mui/material";
-import React from "react";
+import { Box, BoxProps, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField, Theme, Typography, useTheme } from "@mui/material";
+import React, { FormEventHandler, useState } from "react";
import { MotionBox } from "./MotionComponents";
import { Variants } from "framer-motion";
-export default function AddPlaylistButton(props: BoxProps) {
+type AddPlaylistButtonProps = {
+ onCreate: (name: string) => void | any;
+};
+export default function AddPlaylistButton({ onCreate }: AddPlaylistButtonProps) {
const theme = useTheme();
+ const [open, setOpen] = useState(false);
+
+ const handleSubmit: FormEventHandler = (evt) => {
+ const playlistName = evt.currentTarget.elements.namedItem('playlistName') as HTMLInputElement;
+ evt.preventDefault();
+ const name = playlistName.value.trim();
+ if (name.length === 0) return;
+ onCreate(name);
+ setOpen(false);
+ };
+
return (
-
-
+
-
- New playlist
-
-
+
+ New playlist
+
+
+ Enter name for your new playlist
+
+
+
+
+
+
+
+ setOpen(true)}
+ >
+
+
+
+
+ New playlist
+
+
+ >
);
}
\ No newline at end of file
diff --git a/src/app/components/Page.tsx b/src/app/components/Page.tsx
index 77286c7..bba92fc 100644
--- a/src/app/components/Page.tsx
+++ b/src/app/components/Page.tsx
@@ -1,13 +1,32 @@
import { Box, BoxProps, useTheme } from "@mui/material";
+import { HTMLMotionProps } from "framer-motion";
import React from "react";
+import { MotionBox } from "./MotionComponents";
export default function Page(props: BoxProps) {
const theme = useTheme();
return (
+ );
+}
+
+export function MotionPage(props: BoxProps & HTMLMotionProps<'div'>) {
+ const theme = useTheme();
+
+ return (
+ (
{
initial: {
- bottom: theme.spacing(-12)
+ bottom: theme.spacing(-12),
+ opacity: 0.8
},
final: {
- bottom: theme.spacing(0)
+ bottom: theme.spacing(0),
+ opacity: 0.8
+ },
+ hover: {
+ opacity: 1
}
}
);
export default function Player() {
+ const navigate = useNavigate();
const theme = useTheme();
const queue = useQueue();
const audioRef = useRef(null);
@@ -28,6 +35,7 @@ export default function Player() {
const [canPlayPrev, setCanPlayPrev] = useState(false);
const [repeatOne, setRepeatOne] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
+
const songURL = useMemo(
() => getSongURL(song),
[song, queue]
@@ -77,6 +85,13 @@ export default function Player() {
setCurrentTime(audio.currentTime);
};
+ const handleSlider = (evt: Event, value: number | number[]) => {
+ const v = typeof value === 'number' ? value : value[0];
+ setCurrentTime(v);
+ if (audioRef.current)
+ audioRef.current.currentTime = v;
+ };
+
useEffect(() => {
const intervalId = setInterval(() => {
updateCurrentPlaying();
@@ -116,6 +131,7 @@ export default function Player() {
variants={variants(theme)}
initial='initial'
animate='final'
+ whileHover='hover'
>
{song && songURL && (
@@ -123,9 +139,9 @@ export default function Player() {
)}
-
+
- {song?.name}
+ {song?.name.slice(0, 16)}{song.name.length > 16 && '...'}
@@ -138,6 +154,13 @@ export default function Player() {
)}
+
+
+
+ navigate('/player')}>
+
+
diff --git a/src/app/components/SongCard.tsx b/src/app/components/SongCard.tsx
index 7a05e6f..d9fcd7a 100644
--- a/src/app/components/SongCard.tsx
+++ b/src/app/components/SongCard.tsx
@@ -1,5 +1,5 @@
import { Box, IconButton, Skeleton, Typography, useTheme } from "@mui/material";
-import React, { MouseEventHandler } from "react";
+import React, { MouseEventHandler, ReactNode } from "react";
import { LazyLoadImage } from 'react-lazy-load-image-component';
import { AddToQueue, Download as DownloadIcon, PlayArrow, PlaylistAdd } from '@mui/icons-material';
@@ -11,6 +11,7 @@ type SongCardProps = {
album: string;
saveSong?: () => void;
playSong?: () => void;
+ controls?: ReactNode;
};
export default function SongCard({ onClick, thumbnailUrl, artist, name, album }: SongCardProps) {
@@ -33,7 +34,7 @@ export default function SongCard({ onClick, thumbnailUrl, artist, name, album }:
);
}
-export function SearchSongCard({ onClick, thumbnailUrl, artist, name, album, saveSong, playSong }: SongCardProps) {
+export function SearchSongCard({ onClick, thumbnailUrl, artist, name, album, saveSong, playSong, controls }: SongCardProps) {
const theme = useTheme();
return (
@@ -82,6 +83,7 @@ export function SearchSongCard({ onClick, thumbnailUrl, artist, name, album, sav
)}
+ {controls}
);
diff --git a/src/app/hooks/usePlaylist.ts b/src/app/hooks/usePlaylist.ts
new file mode 100644
index 0000000..fb4f1f6
--- /dev/null
+++ b/src/app/hooks/usePlaylist.ts
@@ -0,0 +1,22 @@
+import React, { useState } from "react";
+import { addSong, getPlaylist, removeSong, Song } from "../utils/MusicServer";
+
+export default function usePlaylist(id: string) {
+ const [songs, setSongs] = useState([]);
+
+ getPlaylist(id).then((playlist) => {
+ setSongs(playlist.songs);
+ });
+
+ return {
+ songs,
+ addSong(song: Song) {
+ setSongs((songs) => [...songs, song]);
+ addSong(id, song);
+ },
+ removeSong(song: Song) {
+ setSongs((songs) => songs.filter(x => x.videoId !== song.videoId));
+ removeSong(id, song);
+ }
+ };
+}
\ No newline at end of file
diff --git a/src/app/hooks/useQueue.ts b/src/app/hooks/useQueue.ts
index e9c6860..b0ceda2 100644
--- a/src/app/hooks/useQueue.ts
+++ b/src/app/hooks/useQueue.ts
@@ -49,6 +49,7 @@ export class Queue {
}
removeSong(index: number) {
+ if (index < this.i) this.setI((i) => i - 1);
this.setSongs((prevSongs) => {
return prevSongs.filter((_, i) => i !== index); // создаем новый массив без удаленного элемента
});
diff --git a/src/app/hooks/useSong.ts b/src/app/hooks/useSong.ts
new file mode 100644
index 0000000..c77981c
--- /dev/null
+++ b/src/app/hooks/useSong.ts
@@ -0,0 +1,13 @@
+import React, { useEffect, useState } from "react";
+import { Song } from "../utils/MusicServer";
+import useQueue from "./useQueue";
+
+export default function useSong(): Song | undefined {
+ const queue = useQueue();
+ if (!queue) return undefined;
+ const [song, setSong] = useState();
+ useEffect(() => {
+ setSong(queue.getCurrent());
+ }, [queue]);
+ return song;
+}
\ No newline at end of file
diff --git a/src/app/pages/Landing.tsx b/src/app/pages/Landing.tsx
index 9e56c84..c22c3e4 100644
--- a/src/app/pages/Landing.tsx
+++ b/src/app/pages/Landing.tsx
@@ -9,13 +9,17 @@ import AddPlaylistButton from "../components/AddPlaylistButton";
export default function Landing() {
const theme = useTheme();
+ const handleCreatePlaylist = (name: string) => {
+ console.log(`Creating playlist ${name}`);
+ };
+
return (
Playlists
-
+
diff --git a/src/app/pages/PlayerMaximized.tsx b/src/app/pages/PlayerMaximized.tsx
new file mode 100644
index 0000000..1e0e7bf
--- /dev/null
+++ b/src/app/pages/PlayerMaximized.tsx
@@ -0,0 +1,138 @@
+import React, { ReactNode, useEffect, useState } from "react";
+import Page, { MotionPage, PageRoot } from "../components/Page";
+import { Alert, AppBar, Box, CircularProgress, IconButton, Tab, Tabs, Toolbar, Typography } from "@mui/material";
+import useSong from "../hooks/useSong";
+import { MotionBox } from "../components/MotionComponents";
+import useQueue from "../hooks/useQueue";
+import { getLyrics } from "../utils/MusicServer";
+import { SearchSongCard } from "../components/SongCard";
+import { Close } from "@mui/icons-material";
+
+function a11yProps(i: number) {
+ return {
+ id: `tab-control-${i}`,
+ 'aria-controls': `tab-${i}`
+ };
+}
+
+function QueueTab() {
+ const queue = useQueue();
+
+ useEffect(() => {
+
+ }, [queue]);
+
+ return (
+
+ {queue && queue.songs.length > 0 && queue.songs && (
+ queue.songs.map(
+ (song, i) => (
+
+ queue.removeSong(i)}
+ >
+
+
+ >
+ }
+ />
+ )
+ )
+ )}
+
+ );
+}
+
+function LyricsTab() {
+ const song = useSong();
+ const [lyrics, setLyrics] = useState([]);
+
+ useEffect(() => {
+ if (!song) {
+ setLyrics([
+
+ There are no any songs in the queue.
+
+ ]);
+ return;
+ }
+ setLyrics([
+
+ ]);
+ getLyrics(song).then(
+ lyrics => {
+ setLyrics(lyrics.map((line) => (
+
+ {line}
+
+ )));
+ }
+ ).catch(
+ err => {
+ setLyrics([
+
+ Could not load lyrics: {err.message}
+
+ ]);
+ }
+ );
+ }, [song]);
+
+ return (
+
+
+ {lyrics}
+
+
+ );
+}
+
+export default function PlayerMaximized() {
+ const [selectedTab, setSelectedTab] = useState(0);
+ return (
+
+
+
+
+ Extera Music
+
+ history.back()}>
+
+
+
+
+
+
+ setSelectedTab(v)}
+ >
+
+
+
+
+ {selectedTab === 0 && }
+ {selectedTab === 1 && }
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/utils/MusicServer.ts b/src/app/utils/MusicServer.ts
index 720a10b..f271735 100644
--- a/src/app/utils/MusicServer.ts
+++ b/src/app/utils/MusicServer.ts
@@ -28,6 +28,11 @@ export type Song = {
album: Album;
};
+export type Playlist = {
+ owner: string;
+ songs: Song[];
+};
+
async function doPost(p: string, json: any) {
const jsonString = JSON.stringify(json);
const response = await fetch(API_BASE + p, {
@@ -47,6 +52,14 @@ async function doGet(p: string) {
return await response.json();
}
+async function doDelete(p: string) {
+ const response = await fetch(API_BASE + p, {
+ method: 'DELETE'
+ });
+ if (!response.ok) throw await response.json();
+ return await response.json();
+}
+
export async function searchSongs(q: string): Promise {
return await doGet(`/music/search?q=${encodeURIComponent(q)}`);
}
@@ -64,6 +77,40 @@ export async function login(username?: string, password?: string): Promise {
+ return await doGet(`/music/lyrics?id=${song.videoId}`);
+}
+
+export async function createPlaylist(name: string): Promise {
+ const { id } = await doPost('/playlists/new', {
+ name
+ });
+
+ return id;
+}
+
+export async function getPlaylists(): Promise {
+ return await doGet('/playlists/list');
+}
+
+export async function getPlaylist(id: string): Promise {
+ return await doGet(`/playlists/${id}`);
+}
+
+export async function deletePlaylist(id: string) {
+ await doDelete(`/playlists/${id}`);
+}
+
+export async function addSong(playlistId: string, song: Song) {
+ await doPost(`/playlists/${playlistId}`, {
+ songId: song.videoId
+ });
+}
+
+export async function removeSong(playlistId: string, song: Song) {
+ await doDelete(`/playlists/${playlistId}/${song.videoId}`);
+}
+
export function saveSong(song: Song) {
saveAs(getSongURL(song)!, `${song.artist.name} - ${song.name}.mp3`);
}
\ No newline at end of file