From 0ae781475cf7f146e236043cd723636ba6154ee4 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 11 Aug 2024 18:21:14 +0800 Subject: [PATCH] :sparkles: feat: add multiselect mod Signed-off-by: SimonShiki --- src/components/base/card.tsx | 2 + src/components/base/checkbox.tsx | 4 +- src/components/now-playing.tsx | 1 + src/components/song-item.tsx | 11 ++- src/jotais/library.ts | 12 ++- src/pages/ncm.tsx | 12 ++- src/pages/settings.tsx | 9 +++ src/pages/song.tsx | 124 ++++++++++++++++++++++++------- src/pages/songlist.tsx | 51 +++++++++++-- src/storages/ncm.ts | 45 +++++++++++ 10 files changed, 228 insertions(+), 43 deletions(-) diff --git a/src/components/base/card.tsx b/src/components/base/card.tsx index cdf9913..0388800 100644 --- a/src/components/base/card.tsx +++ b/src/components/base/card.tsx @@ -3,6 +3,7 @@ import { forwardRef, MouseEventHandler, PropsWithChildren } from 'react'; interface CardProps extends PropsWithChildren { className?: string; onClick?: MouseEventHandler; + onClickCapture?: MouseEventHandler; onContextMenu?: MouseEventHandler; onDoubleClick?: MouseEventHandler; } @@ -13,6 +14,7 @@ export default forwardRef(function Card (props, ref) className={`rounded-1.5 border-(1 solid outline-pri) dark:border-outline-dark-pri bg-white bg-op-60 dark:bg-bg-dark-pri p-4 ${props.onClick ? 'hover:border-outline-sec dark:hover:border-outline-dark-sec' : ''} ${props.className ?? ''}`} ref={ref} onClick={props.onClick} + onClickCapture={props.onClickCapture} onDoubleClick={props.onDoubleClick} onContextMenu={props.onContextMenu} > diff --git a/src/components/base/checkbox.tsx b/src/components/base/checkbox.tsx index 5990ca9..c4ba3a9 100644 --- a/src/components/base/checkbox.tsx +++ b/src/components/base/checkbox.tsx @@ -30,7 +30,7 @@ const Checkbox = forwardRef(function Checkbox ( disabled={disabled} ref={ref} /> -
(function Checkbox ( ? 'border-outline-ter dark:border-outline-dark-ter' : 'hover:border-fg-pri dark:hover:border-fg-dark-pri'}`}> diff --git a/src/components/now-playing.tsx b/src/components/now-playing.tsx index bfb8f47..ba9d914 100644 --- a/src/components/now-playing.tsx +++ b/src/components/now-playing.tsx @@ -127,6 +127,7 @@ export default function NowPlaying () {
)} className='flex w-5 h-5' + placement='top-right' trigger='click' > diff --git a/src/components/song-item.tsx b/src/components/song-item.tsx index e1c22aa..f6e16a3 100644 --- a/src/components/song-item.tsx +++ b/src/components/song-item.tsx @@ -6,11 +6,15 @@ import { useCallback } from 'react'; import { useAtom } from 'jotai'; import { songlistsJotai } from '../jotais/library'; import * as player from '../utils/player'; +import Checkbox from './base/checkbox'; interface SongItemProps { song: AbstractSong; + selectMode?: boolean; + select?: boolean; onClick (song: AbstractSong): void; hideBg?: boolean; + onSelect? (checked: boolean): void; } export default function SongItem (props: SongItemProps) { @@ -63,8 +67,13 @@ export default function SongItem (props: SongItemProps) { return ( { + if (props.selectMode) return; props.onClick(props.song); - }} 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' : ''}`}> + }} onClickCapture={() => { + if (!props.selectMode) return; + props.onSelect?.(!props.select); + }} className={`flex flex-row items-center ${props.selectMode ? '' : '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.selectMode && ()} {props.song.name}
{props.song.name} diff --git a/src/jotais/library.ts b/src/jotais/library.ts index 12e8ea6..ae545fc 100644 --- a/src/jotais/library.ts +++ b/src/jotais/library.ts @@ -2,6 +2,7 @@ import { atom } from 'jotai'; import { scannedJotai, Song, storagesJotai } from './storage'; import sharedStore from './shared-store'; import { atomWithStorage } from 'jotai/utils'; +import { backendStorage } from '../utils/local-utitity'; export const libraryJotai = atom[]>([]); sharedStore.sub(scannedJotai, () => { @@ -84,4 +85,13 @@ export interface Songlist { songs: Song[]; } -export const songlistsJotai = atomWithStorage('songlist', []); +export const songlistsJotai = atom([]); +backendStorage.get('songlists').then((songlists: Songlist[] | undefined) => { + if (!songlists) return; + sharedStore.set(songlistsJotai, songlists); +}); + +sharedStore.sub(songlistsJotai, () => { + const songlists = sharedStore.get(songlistsJotai); + backendStorage.set('songlists', songlists); +}); diff --git a/src/pages/ncm.tsx b/src/pages/ncm.tsx index 543a3c2..ab9bf00 100644 --- a/src/pages/ncm.tsx +++ b/src/pages/ncm.tsx @@ -1,9 +1,9 @@ import { focusAtom } from 'jotai-optics'; import { storagesConfigJotai } from '../jotais/settings'; -import { useAtomValue } from 'jotai'; +import { useAtomValue, WritableAtom } from 'jotai'; import { NCMConfig, NCMSonglist } from '../storages/ncm'; import Input from '../components/base/input'; -import { useCallback, useEffect, useState } from 'react'; +import { SetStateAction, useCallback, useEffect, useState } from 'react'; import { Virtuoso } from 'react-virtuoso'; import { nowPlayingBarJotai } from '../jotais/play'; import { storagesJotai, type Song as AbstractSong } from '../jotais/storage'; @@ -20,8 +20,9 @@ interface NCMProfile { avatarUrl: string; } -const ncmStorageConfigJotai = focusAtom(storagesConfigJotai, (optic) => optic.prop('ncm')); +const ncmStorageConfigJotai = focusAtom(storagesConfigJotai, (optic) => optic.prop('ncm')) as unknown as WritableAtom], void>; const ncmStorageJotai = focusAtom(storagesJotai, (optic) => optic.prop('ncm')); +const profileJotai = focusAtom(ncmStorageConfigJotai, (optic) => optic.prop('profile')); export default function NCM () { const ncmConfig = useAtomValue(ncmStorageConfigJotai) as NCMConfig; @@ -29,7 +30,7 @@ export default function NCM () { const barOpen = useAtomValue(nowPlayingBarJotai); const [songlist, setSonglist] = useState([]); - const [profile, setProfile] = useState(null); + const profile = useAtomValue(profileJotai); const [searchText, setSearchText] = useState(''); const [searchResult, setSearchResult] = useState[]>([]); const [hasMore, setHasMore] = useState(false); @@ -46,9 +47,6 @@ export default function NCM () { useEffect(() => { if (ncmConfig.loggedIn) { ncmInstance.getRemoteSonglist().then(setSonglist); - ncmInstance.getProfile().then(setProfile); - } else { - setProfile(null); } }, [ncmConfig, ncmInstance]); diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index c51c623..77f3cef 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -32,6 +32,7 @@ const ncmStorageConfigJotai = focusAtom(storagesConfigJotai, (optic) => optic.pr const ncmCookieJotai = focusAtom(ncmStorageConfigJotai, (optic) => optic.prop('cookie')); const ncmLoggedInJotai = focusAtom(ncmStorageConfigJotai, (optic) => optic.prop('loggedIn')); const ncmApiJotai = focusAtom(ncmStorageConfigJotai, (optic) => optic.prop('api')); +const ncmProfileJotai = focusAtom(ncmStorageConfigJotai, (optic) => optic.prop('profile')); const localFoldersJotai = focusAtom(localStorageConfigJotai, (optic) => optic.prop('folders')); const localAutoScanJotai = focusAtom(localStorageConfigJotai, (optic) => optic.prop('autoScanBehavior')); const localStorageJotai = focusAtom(storagesJotai, (optic) => optic.prop('local')); @@ -53,6 +54,7 @@ export default function Settings () { const setNCMCookie = useSetAtom(ncmCookieJotai); const [ncmLoggedIn, setNCMloggedIn] = useAtom(ncmLoggedInJotai); const [streaming, setStreaming] = useAtom(streamingJotai); + const [ncmProfile, setNCMProfile] = useAtom(ncmProfileJotai); const [ncmAPI, setNcmAPI] = useAtom(ncmApiJotai); const barOpen = useAtomValue(nowPlayingBarJotai); @@ -267,9 +269,16 @@ export default function Settings () { + {ncmProfile && ( +
+ + {ncmProfile.nickname} +
+ )} - {!scanned && ( -
- - - - -
- )} - { - setKeyword(e.target.value); - }} after={} className='m-l-auto' /> - - - - { + setKeyword(e.target.value); + }} after={} /> + + + + { + setSortBy(option); + }} value={sortBy} />
| null; + djStatus: number; + locationStatus: number; + vipType: number; + followed: boolean; + mutual: boolean; + authenticated: boolean; + lastLoginTime: number; + lastLoginIP: string; + remarkName: string | null; + viptypeVersion: number; + authenticationTypes: number; + avatarDetail: null; + anchor: boolean; +} + type NCMQuality = 'standard' | 'higher' | 'exhigh' | 'lossless' | 'hires' | 'jyeffect' | 'sky' | 'jymaster'; export interface NCMConfig extends StorageConfig<'ncm'> { cookie?: string; loggedIn?: boolean; uid?: number; + profile?: NCMProfile, api: string; syncSonglist: boolean; defaultQuality: NCMQuality; @@ -375,8 +416,12 @@ export class NCM implements AbstractStorage { async getProfile () { if (!this.config.loggedIn) throw 'not logged in'; + if (this.config.profile) return this.config.profile; + const res = await fetch(`${this.config.api}user/account?cookie=${this.config.cookie}`); const { profile } = await res.json(); + const profileJotai = focusAtom(this.ncmStorageConfigJotai, (optic) => optic.prop('profile')); + sharedStore.set(profileJotai, profile); return profile; }