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
onClick={toAlbum}
className='aspect-square h-full rounded-md shadow-md'
src={resizeImage(track?.al?.picUrl ?? '', 'sm')}
src={resizeImage(track.al.picUrl, 'xs')}
/>
)}
{!track?.al?.picUrl && (

View file

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

View file

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

View file

@ -13,17 +13,20 @@ const Track = memo(
track,
isLiked = false,
isSkeleton = false,
isPlaying = false,
subtitle = undefined,
isHighlight = false,
onClick,
}: {
track: Track
isLiked?: boolean
isSkeleton?: boolean
isPlaying?: boolean
subtitle?: string
isHighlight?: boolean
onClick: (e: React.MouseEvent<HTMLElement>, trackID: number) => void
}) => {
const subtitle = useMemo(
() => track.tns?.at(0) ?? track.alia?.at(0),
[track.alia, track.tns]
)
return (
<div
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 ',
'grid-cols-12 p-2 pr-4',
!isSkeleton &&
!isPlaying &&
!isHighlight &&
'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 */}
@ -58,7 +61,7 @@ const Track = memo(
<div
className={classNames(
'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>
@ -67,7 +70,7 @@ const Track = memo(
title={subtitle}
className={classNames(
'ml-1',
isPlaying ? 'text-brand-500/[.8]' : 'text-gray-400'
isHighlight ? 'text-brand-500/[.8]' : 'text-gray-400'
)}
>
({subtitle})
@ -79,7 +82,7 @@ const Track = memo(
<div
className={classNames(
'text-sm',
isPlaying
isHighlight
? 'text-brand-500'
: 'text-gray-600 dark:text-gray-400'
)}
@ -111,7 +114,7 @@ const Track = memo(
to={`/album/${track.al.id}`}
className={classNames(
'hover:underline',
isPlaying && 'text-brand-500'
isHighlight && 'text-brand-500'
)}
>
{track.al.name}
@ -147,7 +150,7 @@ const Track = memo(
<div
className={classNames(
'min-w-[2.5rem] text-right',
isPlaying
isHighlight
? 'text-brand-500'
: '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) {
await reactQueryClient.prefetchQuery(
[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) {
await reactQueryClient.prefetchQuery(
[PlaylistApiNames.FETCH_PLAYLIST, params],

View file

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

View file

@ -9,6 +9,7 @@ import TracksGrid from '@/components/TracksGrid'
import CoverRow, { Subtitle } from '@/components/CoverRow'
import Skeleton from '@/components/Skeleton'
import useTracks from '@/hooks/useTracks'
import { player } from '@/store'
const Header = ({ artist }: { artist: Artist | undefined }) => {
const coverImage = resizeImage(artist?.img1v1Url || '', 'md')
@ -51,18 +52,27 @@ const LatestRelease = ({
album: Album | undefined
isLoading: boolean
}) => {
const navigate = useNavigate()
const toAlbum = () => navigate(`/album/${album?.id}`)
return (
<div>
<div className='mb-6 text-2xl font-semibold text-gray-800 dark:text-white'>
</div>
<div className='flex-grow rounded-xl '>
<div className='flex-grow rounded-xl'>
{isLoading ? (
<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}
</div>
<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) ?? [],
})
const handlePlay = useCallback(
(trackID: number | null = null) => {
if (!tracks?.length) {
toast('无法播放歌单')
return
}
player.playAList(
tracks.map(t => t.id),
trackID
)
},
[tracks]
)
return (
<div>
<div className='mb-6 text-2xl font-semibold text-gray-800 dark:text-white'>
@ -93,6 +117,7 @@ const PopularTracks = ({
<TracksGrid
tracks={tracksWithExtraInfo?.songs ?? tracks?.slice(0, 10) ?? []}
isSkeleton={isLoadingArtist}
onTrackDoubleClick={handlePlay}
/>
</div>
</div>

View file

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

View file

@ -6,6 +6,7 @@ import {
} from '@/api/search'
import Cover from '@/components/Cover'
import TrackGrid from '@/components/TracksGrid'
import { player } from '@/store'
import { resizeImage } from '@/utils/common'
const Artists = ({ artists }: { artists: Artist[] }) => {
@ -98,6 +99,33 @@ const Search = () => {
() => 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 (
<div>
<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'>
{bestMatch.map(match => (
<div
onClick={() => navigateBestMatch(match)}
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'
>
@ -154,7 +183,11 @@ const Search = () => {
{searchResult?.result.song.songs && (
<div className='col-span-2'>
<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>

View file

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