From a32855ac46d62ba144cd901557a2f06047f1d05b Mon Sep 17 00:00:00 2001 From: OfficialDakari Date: Sat, 12 Oct 2024 20:19:44 +0500 Subject: [PATCH] a lot of things --- src/app/Router.tsx | 5 + src/app/components/AddPlaylistButton.tsx | 94 ++++++++++----- src/app/components/Page.tsx | 23 +++- src/app/components/Player.tsx | 38 ++++++- src/app/components/SongCard.tsx | 6 +- src/app/hooks/usePlaylist.ts | 22 ++++ src/app/hooks/useQueue.ts | 1 + src/app/hooks/useSong.ts | 13 +++ src/app/pages/Landing.tsx | 6 +- src/app/pages/PlayerMaximized.tsx | 138 +++++++++++++++++++++++ src/app/utils/MusicServer.ts | 47 ++++++++ 11 files changed, 355 insertions(+), 38 deletions(-) create mode 100644 src/app/hooks/usePlaylist.ts create mode 100644 src/app/hooks/useSong.ts create mode 100644 src/app/pages/PlayerMaximized.tsx 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 ( - - + setOpen(false)} > - - - - 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