From 3b014256cfeb642e02a88996d8cbdb4aeea23180 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 11 Aug 2024 15:54:21 +0800 Subject: [PATCH] :sparkles: feat: add songlist Signed-off-by: SimonShiki --- src/components/song-item.tsx | 21 ++++- src/components/songlist-item.tsx | 46 ++++++++-- src/jotais/library.ts | 8 ++ src/jotais/playlist.ts | 7 -- src/pages/album.tsx | 2 +- src/pages/ncm.tsx | 2 +- src/pages/songlist.tsx | 145 +++++++++++++++++++++++++++++++ src/router.tsx | 4 +- 8 files changed, 216 insertions(+), 19 deletions(-) delete mode 100644 src/jotais/playlist.ts create mode 100644 src/pages/songlist.tsx diff --git a/src/components/song-item.tsx b/src/components/song-item.tsx index da4c270..e1c22aa 100644 --- a/src/components/song-item.tsx +++ b/src/components/song-item.tsx @@ -3,6 +3,9 @@ import defaultCover from '../assets/default-cover.png'; import Card from './base/card'; import { IconMenuItem, Menu, MenuItem, NativeIcon, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu'; import { useCallback } from 'react'; +import { useAtom } from 'jotai'; +import { songlistsJotai } from '../jotais/library'; +import * as player from '../utils/player'; interface SongItemProps { song: AbstractSong; @@ -11,13 +14,14 @@ interface SongItemProps { } export default function SongItem (props: SongItemProps) { + const [songlists, setSonglists] = useAtom(songlistsJotai); const showContextMenu = useCallback(async () => { const menu = await Menu.new({ items: [ await MenuItem.new({ text: 'Play', action: () => { - props.onClick(props.song); + player.setCurrentSong(props.song); } }), await MenuItem.new({ @@ -31,7 +35,18 @@ export default function SongItem (props: SongItemProps) { }), await Submenu.new({ text: 'Add to', - items: [] + items: await Promise.all(songlists.map((songlist, index) => (MenuItem.new({ + text: songlist.name, + action: () => { + const newSonglist = { + ...songlist, + songs: [...songlist.songs, props.song] + }; + const newSonglists = [...songlists]; + newSonglists[index] = newSonglist; + setSonglists(newSonglists); + } + })))) }), await IconMenuItem.new({ icon: NativeIcon.Remove, @@ -44,7 +59,7 @@ export default function SongItem (props: SongItemProps) { ] }); menu.popup(); - }, []); + }, [songlists]); return ( { diff --git a/src/components/songlist-item.tsx b/src/components/songlist-item.tsx index 28c4fda..460a5f7 100644 --- a/src/components/songlist-item.tsx +++ b/src/components/songlist-item.tsx @@ -1,23 +1,57 @@ +import { FormattedMessage } from 'react-intl'; import defaultCover from '../assets/default-cover.png'; import Card from './base/card'; +import { Menu, MenuItem } from '@tauri-apps/api/menu'; +import { useCallback } from 'react'; interface SonglistItemProps { id: string | number; index?: number; + total?: number; name: string; cover?: string; - onClick? (id: string | number, index?: number): void; + onClick (id: string | number, index?: number): void; + onPlayAll (id: string | number, index?: number): void; + onDelete? (id: string | number, index?: number): void; hideBg?: boolean; } export default function SonglistItem (props: SonglistItemProps) { - + const showContextMenu = useCallback(async () => { + const items = [ + await MenuItem.new({ + text: 'Play', + action: () => { + props.onPlayAll(props.id, props.index); + } + }) + ]; + if (props.onDelete) { + items.push( + await MenuItem.new({ + text: 'Delete', + action: () => { + props.onDelete!(props.id, props.index); + } + }) + ); + } + const menu = await Menu.new({items}); + menu.popup(); + }, [props.id, props.onPlayAll, props.onDelete, props.index]); return ( - { - props.onClick?.(props.id, props.index); - }} className={`flex flex-row items-center active:scale-99 py-2 gap-2 hover:!bg-black cursor-pointer hover:!bg-op-5 transition-all ${props.hideBg ? '!border-none !bg-transparent' : ''}`}> + { + props.onClick(props.id, props.index); + }} className={`!hover:border-outline-pri flex flex-row items-center active:scale-99 py-2 gap-2 hover:!bg-black cursor-pointer hover:!bg-op-5 transition-all ${props.hideBg ? '!border-none !bg-transparent' : ''}`}> {props.name} - {props.name} +
+ {props.name} + {props.total !== undefined ? ( + + + + ) : null} +
); } diff --git a/src/jotais/library.ts b/src/jotais/library.ts index c268bdc..12e8ea6 100644 --- a/src/jotais/library.ts +++ b/src/jotais/library.ts @@ -1,6 +1,7 @@ import { atom } from 'jotai'; import { scannedJotai, Song, storagesJotai } from './storage'; import sharedStore from './shared-store'; +import { atomWithStorage } from 'jotai/utils'; export const libraryJotai = atom[]>([]); sharedStore.sub(scannedJotai, () => { @@ -77,3 +78,10 @@ sharedStore.sub(libraryJotai, () => { sharedStore.set(artistsJotai, Object.values(artists)); }); + +export interface Songlist { + name: string; + songs: Song[]; +} + +export const songlistsJotai = atomWithStorage('songlist', []); diff --git a/src/jotais/playlist.ts b/src/jotais/playlist.ts deleted file mode 100644 index cd4d4ba..0000000 --- a/src/jotais/playlist.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Song } from './storage'; - -export interface Playlist { - name: string; - cover?: string; - songs: Song[]; -} diff --git a/src/pages/album.tsx b/src/pages/album.tsx index c287146..4ef5646 100644 --- a/src/pages/album.tsx +++ b/src/pages/album.tsx @@ -10,7 +10,7 @@ import * as player from '../utils/player'; import Button from '../components/base/button'; import { FormattedMessage } from 'react-intl'; -export default function AlbumPage() { +export default function AlbumPage () { const scanned = useAtomValue(scannedJotai); const albums = useAtomValue(albumsJotai); const [currentAlbum, setCurrentAlbum] = useState(null); diff --git a/src/pages/ncm.tsx b/src/pages/ncm.tsx index fa1785f..543a3c2 100644 --- a/src/pages/ncm.tsx +++ b/src/pages/ncm.tsx @@ -197,7 +197,7 @@ export default function NCM () { return
; } const list = songlist[index]; - return ; + return ; }} /> ) : ( diff --git a/src/pages/songlist.tsx b/src/pages/songlist.tsx new file mode 100644 index 0000000..95b481f --- /dev/null +++ b/src/pages/songlist.tsx @@ -0,0 +1,145 @@ +import { useAtom, useAtomValue } from 'jotai'; +import { scannedJotai, Song } from '../jotais/storage'; +import Spinner from '../components/base/spinner'; +import { Virtuoso } from 'react-virtuoso'; +import { Songlist, songlistsJotai } from '../jotais/library'; +import { useCallback, useState } from 'react'; +import SongItem from '../components/song-item'; +import * as player from '../utils/player'; +import { FormattedMessage, useIntl } from 'react-intl'; +import SonglistItem from '../components/songlist-item'; +import Button from '../components/base/button'; +import Tooltip from '../components/base/tooltip'; +import Input from '../components/base/input'; + +export default function SonglistPage () { + const scanned = useAtomValue(scannedJotai); + const intl = useIntl(); + const [songlists, setSonglists] = useAtom(songlistsJotai); + const [songlistName, setSonglistName] = useState(''); + const [currentSonglist, setCurrentSonglist] = useState(null); + const handleClickSong = useCallback((song: Song) => { + if (!currentSonglist) return; + player.clearPlaylist(); + player.addToPlaylist(...currentSonglist.songs); + player.setCurrentSong(song); + }, [currentSonglist]); + const handleClickSonglist = useCallback((index: number) => { + const songlist = songlists[index]; + if (!songlist.songs.length) return; + player.clearPlaylist(); + player.addToPlaylist(...songlist.songs); + player.setCurrentSong(songlist.songs[0]); + }, [songlists]); + const handleCreateSonglist = useCallback>((e) => { + e.preventDefault(); + e.stopPropagation(); + setSonglists([...songlists, { + name: songlistName, + songs: [] + }]); + setSonglistName(''); + }, [songlists, songlistName]); + const handleDeleteSonglist = useCallback((index: number) => { + const newSonglists = [...songlists]; + newSonglists.splice(index, 1); + setSonglists(newSonglists); + }, [songlists]); + + return ( +
+
+ + + +
+ + { + setSonglistName(e.target.value); + }} placeholder={intl.formatMessage({ defaultMessage: 'Name'})} /> + + + )}> + + + {!scanned && ( +
+ + + + +
+ )} +
+
+ {songlists.length > 0 ? ( +
+
+ { + const songlist = songlists[index]; + return ( + { + setCurrentSonglist(songlists[i]); + }} + /> + ); + }} + /> +
+
+ {currentSonglist && ( +
+
+ +

{currentSonglist.name}

+
+ { + const song = currentSonglist.songs[index]; + return ; + }} + /> +
+ )} +
+
+ ) : ( + scanned ? ( +
+ Empty +
+ ) : ( +
+ +
+ ) + )} +
+ ); +} diff --git a/src/router.tsx b/src/router.tsx index f6dd3b7..985476b 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -6,6 +6,7 @@ import NotFound from './pages/404'; import Settings from './pages/settings'; import NCM from './pages/ncm'; import Album from './pages/album'; +import Songlist from './pages/songlist'; import Artist from './pages/artist'; const router = createBrowserRouter([ @@ -19,7 +20,8 @@ const router = createBrowserRouter([ { path: 'artist', element: }, { path: 'local', element: }, { path: 'ncm', element: }, - { path: 'settings', element: } + { path: 'settings', element: }, + { path: 'songlist', element: }, ], }, ]);