feat: updates

This commit is contained in:
qier222 2022-04-02 18:46:08 +08:00
parent 3ef7675696
commit 744247143b
No known key found for this signature in database
GPG key ID: 9C85007ED905F14D
11 changed files with 195 additions and 68 deletions

View file

@ -33,7 +33,7 @@ const PlayingTrack = () => {
<img <img
onClick={toAlbum} onClick={toAlbum}
className='aspect-square h-full rounded-md shadow-md' className='aspect-square h-full rounded-md shadow-md'
src={resizeImage(track?.al?.picUrl ?? '', 'sm')} src={resizeImage(track.al.picUrl, 'xs')}
/> />
)} )}
{!track?.al?.picUrl && ( {!track?.al?.picUrl && (

View file

@ -49,19 +49,22 @@ const Track = memo(
isLiked = false, isLiked = false,
isSkeleton = false, isSkeleton = false,
isHighlight = false, isHighlight = false,
subtitle = undefined,
onClick, onClick,
}: { }: {
track: Track track: Track
isLiked?: boolean isLiked?: boolean
isSkeleton?: boolean isSkeleton?: boolean
isHighlight?: boolean isHighlight?: boolean
subtitle?: string
onClick: (e: React.MouseEvent<HTMLElement>, trackID: number) => void onClick: (e: React.MouseEvent<HTMLElement>, trackID: number) => void
}) => { }) => {
if (enableRenderLog) if (enableRenderLog)
console.debug(`Rendering TracksAlbum.tsx Track ${track.name}`) console.debug(`Rendering TracksAlbum.tsx Track ${track.name}`)
const subtitle = useMemo(
() => track.tns?.at(0) ?? track.alia?.at(0),
[track.alia, track.tns]
)
return ( return (
<div <div
onClick={e => onClick(e, track.id)} onClick={e => onClick(e, track.id)}
@ -125,7 +128,6 @@ const Track = memo(
)} )}
{subtitle && ( {subtitle && (
<span <span
title={subtitle}
className={classNames( className={classNames(
'ml-1', 'ml-1',
isHighlight ? 'text-brand-500/[.8]' : 'text-gray-400' isHighlight ? 'text-brand-500/[.8]' : 'text-gray-400'
@ -259,7 +261,6 @@ const TracksAlbum = ({
isLiked={userLikedSongs?.ids?.includes(track.id) ?? false} isLiked={userLikedSongs?.ids?.includes(track.id) ?? false}
isSkeleton={false} isSkeleton={false}
isHighlight={track.id === playingTrack?.id} isHighlight={track.id === playingTrack?.id}
subtitle={track.tns?.at(0) ?? track.alia?.at(0)}
/> />
))} ))}
</div> </div>

View file

@ -1,5 +1,6 @@
import ArtistInline from '@/components/ArtistsInline' import ArtistInline from '@/components/ArtistsInline'
import Skeleton from '@/components/Skeleton' import Skeleton from '@/components/Skeleton'
import { player } from '@/store'
import { resizeImage } from '@/utils/common' import { resizeImage } from '@/utils/common'
import SvgIcon from './SvgIcon' import SvgIcon from './SvgIcon'
@ -7,13 +8,16 @@ const Track = ({
track, track,
isSkeleton = false, isSkeleton = false,
isHighlight = false, isHighlight = false,
onClick,
}: { }: {
track: Track track: Track
isSkeleton: boolean isSkeleton?: boolean
isHighlight: boolean isHighlight?: boolean
onClick: (e: React.MouseEvent<HTMLElement>, trackID: number) => void
}) => { }) => {
return ( return (
<div <div
onClick={e => onClick(e, track.id)}
className={classNames( className={classNames(
'group grid w-full rounded-xl after:scale-[.98] after:rounded-xl ', 'group grid w-full rounded-xl after:scale-[.98] after:rounded-xl ',
'grid-cols-1 py-1.5 px-2', 'grid-cols-1 py-1.5 px-2',
@ -27,30 +31,30 @@ const Track = ({
<div className='grid grid-cols-[3rem_auto] items-center'> <div className='grid grid-cols-[3rem_auto] items-center'>
{/* Cover */} {/* Cover */}
<div> <div>
{!isSkeleton && ( {isSkeleton ? (
<Skeleton className='mr-4 h-9 w-9 rounded-md border border-gray-100' />
) : (
<img <img
src={resizeImage(track.al.picUrl, 'xs')} src={resizeImage(track.al.picUrl, 'xs')}
className='box-content h-9 w-9 rounded-md border border-black border-opacity-[.03]' className='box-content h-9 w-9 rounded-md border border-black border-opacity-[.03]'
/> />
)} )}
{isSkeleton && (
<Skeleton className='mr-4 h-9 w-9 rounded-md border border-gray-100' />
)}
</div> </div>
{/* Track name & Artists */} {/* Track name & Artists */}
<div className='flex flex-col justify-center'> <div className='flex flex-col justify-center'>
{!isSkeleton && ( {isSkeleton ? (
<Skeleton className='text-base '>PLACEHOLDER12345</Skeleton>
) : (
<div <div
v-if='!isSkeleton' className={classNames(
className='line-clamp-1 break-all text-base font-semibold dark:text-white' 'line-clamp-1 break-all text-base font-semibold ',
isHighlight ? 'text-brand-500' : 'text-black dark:text-white'
)}
> >
{track.name} {track.name}
</div> </div>
)} )}
{isSkeleton && (
<Skeleton className='text-base '>PLACEHOLDER12345</Skeleton>
)}
<div className='text-xs text-gray-500 dark:text-gray-400'> <div className='text-xs text-gray-500 dark:text-gray-400'>
{isSkeleton ? ( {isSkeleton ? (
@ -60,10 +64,23 @@ const Track = ({
{track.mark === 1318912 && ( {track.mark === 1318912 && (
<SvgIcon <SvgIcon
name='explicit' name='explicit'
className='mr-1 h-3 w-3 text-gray-300 dark:text-gray-500' className={classNames(
'mr-1 h-3 w-3',
isHighlight
? 'text-brand-500'
: 'text-gray-300 dark:text-gray-500'
)}
/> />
)} )}
<ArtistInline artists={track.ar} disableLink={true} /> <ArtistInline
artists={track.ar}
disableLink={true}
className={
isHighlight
? 'text-brand-500'
: 'text-gray-600 dark:text-gray-400'
}
/>
</span> </span>
)} )}
</div> </div>
@ -84,6 +101,16 @@ const TrackGrid = ({
onTrackDoubleClick?: (trackID: number) => void onTrackDoubleClick?: (trackID: number) => void
cols?: number cols?: number
}) => { }) => {
const handleClick = (e: React.MouseEvent<HTMLElement>, trackID: number) => {
if (e.detail === 2) onTrackDoubleClick?.(trackID)
}
const playerSnapshot = useSnapshot(player)
const playingTrack = useMemo(
() => playerSnapshot.track,
[playerSnapshot.track]
)
return ( return (
<div <div
className='grid gap-x-2' className='grid gap-x-2'
@ -93,10 +120,11 @@ const TrackGrid = ({
> >
{tracks.map((track, index) => ( {tracks.map((track, index) => (
<Track <Track
onClick={handleClick}
key={track.id} key={track.id}
track={track} track={track}
isSkeleton={isSkeleton} isSkeleton={isSkeleton}
isHighlight={false} isHighlight={track.id === playingTrack?.id}
/> />
))} ))}
</div> </div>

View file

@ -13,17 +13,20 @@ const Track = memo(
track, track,
isLiked = false, isLiked = false,
isSkeleton = false, isSkeleton = false,
isPlaying = false, isHighlight = false,
subtitle = undefined,
onClick, onClick,
}: { }: {
track: Track track: Track
isLiked?: boolean isLiked?: boolean
isSkeleton?: boolean isSkeleton?: boolean
isPlaying?: boolean isHighlight?: boolean
subtitle?: string
onClick: (e: React.MouseEvent<HTMLElement>, trackID: number) => void onClick: (e: React.MouseEvent<HTMLElement>, trackID: number) => void
}) => { }) => {
const subtitle = useMemo(
() => track.tns?.at(0) ?? track.alia?.at(0),
[track.alia, track.tns]
)
return ( return (
<div <div
onClick={e => onClick(e, track.id)} onClick={e => onClick(e, track.id)}
@ -31,9 +34,9 @@ const Track = memo(
'group grid w-full rounded-xl after:scale-[.98] after:rounded-xl ', 'group grid w-full rounded-xl after:scale-[.98] after:rounded-xl ',
'grid-cols-12 p-2 pr-4', 'grid-cols-12 p-2 pr-4',
!isSkeleton && !isSkeleton &&
!isPlaying && !isHighlight &&
'btn-hover-animation after:bg-gray-100 dark:after:bg-white/10', 'btn-hover-animation after:bg-gray-100 dark:after:bg-white/10',
!isSkeleton && isPlaying && 'bg-brand-50 dark:bg-gray-800' !isSkeleton && isHighlight && 'bg-brand-50 dark:bg-gray-800'
)} )}
> >
{/* Track info */} {/* Track info */}
@ -58,7 +61,7 @@ const Track = memo(
<div <div
className={classNames( className={classNames(
'line-clamp-1 break-all text-lg font-semibold', 'line-clamp-1 break-all text-lg font-semibold',
isPlaying ? 'text-brand-500' : 'text-black dark:text-white' isHighlight ? 'text-brand-500' : 'text-black dark:text-white'
)} )}
> >
<span>{track.name}</span> <span>{track.name}</span>
@ -67,7 +70,7 @@ const Track = memo(
title={subtitle} title={subtitle}
className={classNames( className={classNames(
'ml-1', 'ml-1',
isPlaying ? 'text-brand-500/[.8]' : 'text-gray-400' isHighlight ? 'text-brand-500/[.8]' : 'text-gray-400'
)} )}
> >
({subtitle}) ({subtitle})
@ -79,7 +82,7 @@ const Track = memo(
<div <div
className={classNames( className={classNames(
'text-sm', 'text-sm',
isPlaying isHighlight
? 'text-brand-500' ? 'text-brand-500'
: 'text-gray-600 dark:text-gray-400' : 'text-gray-600 dark:text-gray-400'
)} )}
@ -111,7 +114,7 @@ const Track = memo(
to={`/album/${track.al.id}`} to={`/album/${track.al.id}`}
className={classNames( className={classNames(
'hover:underline', 'hover:underline',
isPlaying && 'text-brand-500' isHighlight && 'text-brand-500'
)} )}
> >
{track.al.name} {track.al.name}
@ -147,7 +150,7 @@ const Track = memo(
<div <div
className={classNames( className={classNames(
'min-w-[2.5rem] text-right', 'min-w-[2.5rem] text-right',
isPlaying isHighlight
? 'text-brand-500' ? 'text-brand-500'
: 'text-gray-600 dark:text-gray-400' : 'text-gray-600 dark:text-gray-400'
)} )}

View file

@ -29,6 +29,16 @@ export default function useAlbum(params: FetchAlbumParams, noCache?: boolean) {
) )
} }
export function fetchAlbumWithReactQuery(params: FetchAlbumParams) {
return reactQueryClient.fetchQuery(
[AlbumApiNames.FETCH_ALBUM, params.id],
() => fetch(params),
{
staleTime: Infinity,
}
)
}
export async function prefetchAlbum(params: FetchAlbumParams) { export async function prefetchAlbum(params: FetchAlbumParams) {
await reactQueryClient.prefetchQuery( await reactQueryClient.prefetchQuery(
[AlbumApiNames.FETCH_ALBUM, params.id], [AlbumApiNames.FETCH_ALBUM, params.id],

View file

@ -28,6 +28,16 @@ export default function usePlaylist(
) )
} }
export function fetchPlaylistWithReactQuery(params: FetchPlaylistParams) {
return reactQueryClient.fetchQuery(
[PlaylistApiNames.FETCH_PLAYLIST, params],
() => fetch(params),
{
staleTime: 3600000,
}
)
}
export async function prefetchPlaylist(params: FetchPlaylistParams) { export async function prefetchPlaylist(params: FetchPlaylistParams) {
await reactQueryClient.prefetchQuery( await reactQueryClient.prefetchQuery(
[PlaylistApiNames.FETCH_PLAYLIST, params], [PlaylistApiNames.FETCH_PLAYLIST, params],

View file

@ -296,12 +296,11 @@ const Album = () => {
}) })
const handlePlay = async (trackID: number | null = null) => { const handlePlay = async (trackID: number | null = null) => {
const realAlbum = album?.album if (!album?.album.id) {
if (!realAlbum) { toast('无法播放专辑,该专辑不存在')
toast('Failed to play album')
return return
} }
await player.playAlbum(realAlbum, trackID) await player.playAlbum(album.album.id, trackID)
} }
return ( return (

View file

@ -9,6 +9,7 @@ import TracksGrid from '@/components/TracksGrid'
import CoverRow, { Subtitle } from '@/components/CoverRow' import CoverRow, { Subtitle } from '@/components/CoverRow'
import Skeleton from '@/components/Skeleton' import Skeleton from '@/components/Skeleton'
import useTracks from '@/hooks/useTracks' import useTracks from '@/hooks/useTracks'
import { player } from '@/store'
const Header = ({ artist }: { artist: Artist | undefined }) => { const Header = ({ artist }: { artist: Artist | undefined }) => {
const coverImage = resizeImage(artist?.img1v1Url || '', 'md') const coverImage = resizeImage(artist?.img1v1Url || '', 'md')
@ -51,18 +52,27 @@ const LatestRelease = ({
album: Album | undefined album: Album | undefined
isLoading: boolean isLoading: boolean
}) => { }) => {
const navigate = useNavigate()
const toAlbum = () => navigate(`/album/${album?.id}`)
return ( return (
<div> <div>
<div className='mb-6 text-2xl font-semibold text-gray-800 dark:text-white'> <div className='mb-6 text-2xl font-semibold text-gray-800 dark:text-white'>
</div> </div>
<div className='flex-grow rounded-xl '> <div className='flex-grow rounded-xl'>
{isLoading ? ( {isLoading ? (
<Skeleton className='aspect-square w-full rounded-xl'></Skeleton> <Skeleton className='aspect-square w-full rounded-xl'></Skeleton>
) : ( ) : (
<Cover imageUrl={album?.picUrl ?? ''} showPlayButton={true} /> <Cover
imageUrl={album?.picUrl ?? ''}
showPlayButton={true}
onClick={toAlbum}
/>
)} )}
<div className='line-clamp-2 line-clamp-1 mt-2 font-semibold leading-tight decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-200'> <div
onClick={toAlbum}
className='line-clamp-2 line-clamp-1 mt-2 font-semibold leading-tight decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-200'
>
{album?.name} {album?.name}
</div> </div>
<div className='text-[12px] text-gray-500 dark:text-gray-400'> <div className='text-[12px] text-gray-500 dark:text-gray-400'>
@ -84,6 +94,20 @@ const PopularTracks = ({
ids: tracks?.slice(0, 10)?.map(t => t.id) ?? [], ids: tracks?.slice(0, 10)?.map(t => t.id) ?? [],
}) })
const handlePlay = useCallback(
(trackID: number | null = null) => {
if (!tracks?.length) {
toast('无法播放歌单')
return
}
player.playAList(
tracks.map(t => t.id),
trackID
)
},
[tracks]
)
return ( return (
<div> <div>
<div className='mb-6 text-2xl font-semibold text-gray-800 dark:text-white'> <div className='mb-6 text-2xl font-semibold text-gray-800 dark:text-white'>
@ -93,6 +117,7 @@ const PopularTracks = ({
<TracksGrid <TracksGrid
tracks={tracksWithExtraInfo?.songs ?? tracks?.slice(0, 10) ?? []} tracks={tracksWithExtraInfo?.songs ?? tracks?.slice(0, 10) ?? []}
isSkeleton={isLoadingArtist} isSkeleton={isLoadingArtist}
onTrackDoubleClick={handlePlay}
/> />
</div> </div>
</div> </div>

View file

@ -215,11 +215,11 @@ const Playlist = () => {
const handlePlay = useCallback( const handlePlay = useCallback(
(trackID: number | null = null) => { (trackID: number | null = null) => {
if (!playlist) { if (!playlist?.playlist?.id) {
toast('Failed to play playlist') toast('无法播放歌单')
return return
} }
player.playPlaylist(playlist.playlist, trackID) player.playPlaylist(playlist.playlist.id, trackID)
}, },
[playlist] [playlist]
) )

View file

@ -6,6 +6,7 @@ import {
} from '@/api/search' } from '@/api/search'
import Cover from '@/components/Cover' import Cover from '@/components/Cover'
import TrackGrid from '@/components/TracksGrid' import TrackGrid from '@/components/TracksGrid'
import { player } from '@/store'
import { resizeImage } from '@/utils/common' import { resizeImage } from '@/utils/common'
const Artists = ({ artists }: { artists: Artist[] }) => { const Artists = ({ artists }: { artists: Artist[] }) => {
@ -98,6 +99,33 @@ const Search = () => {
() => search({ keywords, type: searchType }) () => search({ keywords, type: searchType })
) )
const handlePlayTracks = useCallback(
(trackID: number | null = null) => {
const tracks = searchResult?.result.song.songs
if (!tracks?.length) {
toast('无法播放歌单')
return
}
player.playAList(
tracks.map(t => t.id),
trackID
)
},
[searchResult?.result.song.songs]
)
const navigate = useNavigate()
const navigateBestMatch = (match: Artist | Album) => {
if ((match as Artist).albumSize !== undefined) {
navigate(`/artist/${match.id}`)
return
}
if ((match as Album).artist !== undefined) {
navigate(`/album/${match.id}`)
return
}
}
return ( return (
<div> <div>
<div className='mt-6 mb-8 text-4xl font-semibold dark:text-white'> <div className='mt-6 mb-8 text-4xl font-semibold dark:text-white'>
@ -111,6 +139,7 @@ const Search = () => {
<div className='grid grid-cols-2'> <div className='grid grid-cols-2'>
{bestMatch.map(match => ( {bestMatch.map(match => (
<div <div
onClick={() => navigateBestMatch(match)}
key={`${match.id}${match.picUrl}`} key={`${match.id}${match.picUrl}`}
className='btn-hover-animation flex items-center p-3 after:rounded-xl after:bg-gray-100 dark:after:bg-white/10' className='btn-hover-animation flex items-center p-3 after:rounded-xl after:bg-gray-100 dark:after:bg-white/10'
> >
@ -154,7 +183,11 @@ const Search = () => {
{searchResult?.result.song.songs && ( {searchResult?.result.song.songs && (
<div className='col-span-2'> <div className='col-span-2'>
<div className='mb-2 text-sm font-medium text-gray-400'></div> <div className='mb-2 text-sm font-medium text-gray-400'></div>
<TrackGrid tracks={searchResult.result.song.songs} cols={3} /> <TrackGrid
tracks={searchResult.result.song.songs}
cols={3}
onTrackDoubleClick={handlePlayTracks}
/>
</div> </div>
)} )}
</div> </div>

View file

@ -9,6 +9,8 @@ import { cacheAudio } from '@/api/yesplaymusic'
import { clamp } from 'lodash-es' import { clamp } from 'lodash-es'
import axios from 'axios' import axios from 'axios'
import { resizeImage } from './common' import { resizeImage } from './common'
import { fetchPlaylistWithReactQuery } from '@/hooks/usePlaylist'
import { fetchAlbumWithReactQuery } from '@/hooks/useAlbum'
type TrackID = number type TrackID = number
enum TrackListSourceType { enum TrackListSourceType {
@ -114,7 +116,7 @@ export class Player {
* Get/Set progress of current track * Get/Set progress of current track
*/ */
get progress(): number { get progress(): number {
return this._progress return this.state === State.LOADING ? 0 : this._progress
} }
set progress(value) { set progress(value) {
this._progress = value this._progress = value
@ -179,9 +181,10 @@ export class Player {
* Play audio via howler * Play audio via howler
*/ */
private async _playAudio() { private async _playAudio() {
this._progress = 0
const { audio, id } = await this._fetchAudioSource(this.trackID) const { audio, id } = await this._fetchAudioSource(this.trackID)
if (!audio) { if (!audio) {
toast('Failed to load audio source') toast('无法播放此歌曲')
return return
} }
Howler.unload() Howler.unload()
@ -228,7 +231,7 @@ export class Player {
const loadMoreTracks = async () => { const loadMoreTracks = async () => {
if (this.fmTrackList.length <= 5) { if (this.fmTrackList.length <= 5) {
const response = await fetchPersonalFMWithReactQuery() const response = await fetchPersonalFMWithReactQuery()
this.fmTrackList.push(...(response?.data?.map(r => r.id) ?? {})) this.fmTrackList.push(...(response?.data?.map(r => r.id) ?? []))
} }
} }
const prefetchNextTrack = async () => { const prefetchNextTrack = async () => {
@ -291,6 +294,7 @@ export class Player {
* Play previous track * Play previous track
*/ */
prevTrack() { prevTrack() {
this._progress = 0
if (this.mode === Mode.FM) { if (this.mode === Mode.FM) {
toast('Personal FM not support previous track') toast('Personal FM not support previous track')
return return
@ -307,13 +311,14 @@ export class Player {
* Play next track * Play next track
*/ */
nextTrack(forceFM: boolean = false) { nextTrack(forceFM: boolean = false) {
this._progress = 0
if (forceFM || this.mode === Mode.FM) { if (forceFM || this.mode === Mode.FM) {
this.mode = Mode.FM this.mode = Mode.FM
this._nextFMTrack() this._nextFMTrack()
return return
} }
if (this._nextTrackIndex === undefined) { if (this._nextTrackIndex === undefined) {
toast('No next track') toast('没有下一首了')
this.pause() this.pause()
return return
} }
@ -322,41 +327,54 @@ export class Player {
} }
/** /**
* Play a playlist * track id列表
* @param {Playlist} playlist * @param {number[]} list
* @param {null|number=} autoPlayTrackID * @param {null|number} autoPlayTrackID
*/ */
async playPlaylist(playlist: Playlist, autoPlayTrackID?: null | number) { playAList(list: TrackID[], autoPlayTrackID?: null | number) {
if (!playlist?.trackIds?.length) return
this.trackListSource = {
type: TrackListSourceType.PLAYLIST,
id: playlist.id,
}
this.mode = Mode.PLAYLIST this.mode = Mode.PLAYLIST
this.trackList = playlist.trackIds.map(t => t.id) this.trackList = list
this._trackIndex = autoPlayTrackID this._trackIndex = autoPlayTrackID
? playlist.trackIds.findIndex(t => t.id === autoPlayTrackID) ? list.findIndex(t => t === autoPlayTrackID)
: 0 : 0
this._playTrack() this._playTrack()
} }
/** /**
* Play am album * Play a playlist
* @param {Album} album * @param {number} playlistID
* @param {null|number=} autoPlayTrackID * @param {null|number=} autoPlayTrackID
*/ */
async playAlbum(album: Album, autoPlayTrackID?: null | number) { async playPlaylist(playlistID: number, autoPlayTrackID?: null | number) {
const playlist = await fetchPlaylistWithReactQuery({ id: playlistID })
if (!playlist?.playlist?.trackIds?.length) return
this.trackListSource = {
type: TrackListSourceType.PLAYLIST,
id: playlistID,
}
this.playAList(
playlist.playlist.trackIds.map(t => t.id),
autoPlayTrackID
)
}
/**
* Play am album
* @param {number} albumID
* @param {null|number=} autoPlayTrackID
*/
async playAlbum(albumID: number, autoPlayTrackID?: null | number) {
const album = await fetchAlbumWithReactQuery({ id: albumID })
if (!album?.songs?.length) return if (!album?.songs?.length) return
this.trackListSource = { this.trackListSource = {
type: TrackListSourceType.ALBUM, type: TrackListSourceType.ALBUM,
id: album.id, id: albumID,
} }
this.mode = Mode.PLAYLIST
this.trackList = album.songs.map(t => t.id)
this._trackIndex = autoPlayTrackID
? album.songs.findIndex(t => t.id === autoPlayTrackID)
: 0
this._playTrack() this._playTrack()
this.playAList(
album.songs.map(t => t.id),
autoPlayTrackID
)
} }
/** /**
@ -380,7 +398,7 @@ export class Player {
*/ */
async initFM() { async initFM() {
const response = await fetchPersonalFMWithReactQuery() const response = await fetchPersonalFMWithReactQuery()
this.fmTrackList.push(...(response?.data?.map(r => r.id) ?? {})) this.fmTrackList.push(...(response?.data?.map(r => r.id) ?? []))
const trackId = this.fmTrackList[0] const trackId = this.fmTrackList[0]
const track = await this._fetchTrack(trackId) const track = await this._fetchTrack(trackId)
@ -401,7 +419,7 @@ export class Player {
*/ */
async playTrack(trackID: TrackID) { async playTrack(trackID: TrackID) {
const index = this.trackList.findIndex(t => t === trackID) const index = this.trackList.findIndex(t => t === trackID)
if (!index) toast('Failed to play: This track is not in the playlist') if (!index) toast('播放失败,歌曲不在列表内')
this._trackIndex = index this._trackIndex = index
this._playTrack() this._playTrack()
} }