feat: 支持收藏歌单和专辑

This commit is contained in:
qier222 2022-04-05 21:23:55 +08:00
parent db5730dfdd
commit 49bb849982
No known key found for this signature in database
GPG key ID: 9C85007ED905F14D
12 changed files with 242 additions and 46 deletions

View file

@ -39,7 +39,7 @@ Object.entries(neteaseApi).forEach(([name, handler]) => {
try { try {
const result = await handler({ const result = await handler({
...req.query, ...req.query,
cookie: `MUSIC_U=${req.cookies['MUSIC_U']}`, cookie: req.cookies,
}) })
setCache(name, result.body, req.query) setCache(name, result.body, req.query)

View file

@ -27,3 +27,23 @@ export function fetchAlbum(
params: { ...params, ...otherParams }, params: { ...params, ...otherParams },
}) })
} }
export interface LikeAAlbumParams {
t: 1 | 2
id: number
}
export interface LikeAAlbumResponse {
code: number
}
export function likeAAlbum(
params: LikeAAlbumParams
): Promise<LikeAAlbumResponse> {
return request({
url: '/album/sub',
method: 'post',
params: {
...params,
timestamp: Date.now(),
},
})
}

View file

@ -4,6 +4,7 @@ export enum PlaylistApiNames {
FETCH_PLAYLIST = 'fetchPlaylist', FETCH_PLAYLIST = 'fetchPlaylist',
FETCH_RECOMMENDED_PLAYLISTS = 'fetchRecommendedPlaylists', FETCH_RECOMMENDED_PLAYLISTS = 'fetchRecommendedPlaylists',
FETCH_DAILY_RECOMMEND_PLAYLISTS = 'fetchDailyRecommendPlaylists', FETCH_DAILY_RECOMMEND_PLAYLISTS = 'fetchDailyRecommendPlaylists',
LIKE_A_PLAYLIST = 'likeAPlaylist',
} }
// 歌单详情 // 歌单详情
@ -70,3 +71,23 @@ export function fetchDailyRecommendPlaylists(): Promise<FetchDailyRecommendPlayl
method: 'get', method: 'get',
}) })
} }
export interface LikeAPlaylistParams {
t: 1 | 2
id: number
}
export interface LikeAPlaylistResponse {
code: number
}
export function likeAPlaylist(
params: LikeAPlaylistParams
): Promise<LikeAPlaylistResponse> {
return request({
url: '/playlist/subscribe',
method: 'post',
params: {
...params,
timestamp: Date.now(),
},
})
}

View file

@ -130,7 +130,9 @@ export interface LikeATrackResponse {
playlistId: number playlistId: number
songs: Track[] songs: Track[]
} }
export function likeATrack(params: LikeATrackParams) { export function likeATrack(
params: LikeATrackParams
): Promise<LikeATrackResponse> {
return request({ return request({
url: '/like', url: '/like',
method: 'post', method: 'post',

View file

@ -164,7 +164,9 @@ export interface FetchUserAlbumsResponse {
count: number count: number
data: Album[] data: Album[]
} }
export function fetchUserAlbums(params: FetchUserAlbumsParams) { export function fetchUserAlbums(
params: FetchUserAlbumsParams
): Promise<FetchUserAlbumsResponse> {
return request({ return request({
url: '/album/sublist', url: '/album/sublist',
method: 'get', method: 'get',

View file

@ -60,11 +60,7 @@ const PrimaryTabs = () => {
} }
const Playlists = () => { const Playlists = () => {
const { data: user } = useUser() const { data: playlists } = useUserPlaylists()
const { data: playlists } = useUserPlaylists({
uid: user?.account?.id ?? 0,
offset: 0,
})
return ( return (
<div className='mb-16 overflow-auto pb-2'> <div className='mb-16 overflow-auto pb-2'>

View file

@ -1,12 +1,16 @@
import { likeAAlbum } from '@/api/album'
import type { FetchUserAlbumsParams, FetchUserAlbumsResponse } from '@/api/user' import type { FetchUserAlbumsParams, FetchUserAlbumsResponse } from '@/api/user'
import { UserApiNames, fetchUserAlbums } from '@/api/user' import { UserApiNames, fetchUserAlbums } from '@/api/user'
import { useQueryClient } from 'react-query'
import useUser from './useUser'
export default function useUserAlbums(params: FetchUserAlbumsParams) { export default function useUserAlbums(params: FetchUserAlbumsParams = {}) {
const { data: user } = useUser()
return useQuery( return useQuery(
[UserApiNames.FETCH_USER_ALBUMS, params], [UserApiNames.FETCH_USER_ALBUMS, user?.profile?.userId ?? 0],
() => fetchUserAlbums(params), () => fetchUserAlbums(params),
{ {
placeholderData: (): FetchUserAlbumsResponse => placeholderData: (): FetchUserAlbumsResponse | undefined =>
window.ipcRenderer?.sendSync('getApiCacheSync', { window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'album/sublist', api: 'album/sublist',
query: params, query: params,
@ -14,3 +18,56 @@ export default function useUserAlbums(params: FetchUserAlbumsParams) {
} }
) )
} }
export const useMutationLikeAAlbum = () => {
const queryClient = useQueryClient()
const { data: user } = useUser()
const { data: userAlbums } = useUserAlbums({ limit: 2000 })
const uid = user?.account?.id ?? 0
const key = [UserApiNames.FETCH_USER_ALBUMS, uid]
return useMutation(
async (album: Album) => {
if (!album.id || userAlbums?.data === undefined) {
throw new Error('album id is required or userAlbums is undefined')
}
const response = await likeAAlbum({
id: album.id,
t: userAlbums?.data.findIndex(a => a.id === album.id) > -1 ? 2 : 1,
})
if (response.code !== 200) throw new Error((response as any).msg)
return response
},
{
onMutate: async album => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(key)
// Snapshot the previous value
const previousData = queryClient.getQueryData(key)
// Optimistically update to the new value
queryClient.setQueryData(key, old => {
const userAlbums = old as FetchUserAlbumsResponse
const albums = userAlbums.data
const newAlbums =
albums.findIndex(a => a.id === album.id) > -1
? albums.filter(a => a.id !== album.id)
: [...albums, album]
return {
...userAlbums,
data: newAlbums,
}
})
// Return a context object with the snapshotted value
return { previousData }
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (err, trackID, context) => {
queryClient.setQueryData(key, (context as any).previousData)
toast((err as any).toString())
},
}
)
}

View file

@ -33,14 +33,16 @@ export const useMutationLikeATrack = () => {
const key = [UserApiNames.FETCH_USER_LIKED_TRACKS_IDS, uid] const key = [UserApiNames.FETCH_USER_LIKED_TRACKS_IDS, uid]
return useMutation( return useMutation(
(trackID: number) => { async (trackID: number) => {
if (!trackID || userLikedSongs?.ids === undefined) { if (!trackID || userLikedSongs?.ids === undefined) {
throw new Error('trackID is required or userLikedSongs is undefined') throw new Error('trackID is required or userLikedSongs is undefined')
} }
return likeATrack({ const response = await likeATrack({
id: trackID, id: trackID,
like: !userLikedSongs.ids.includes(trackID), like: !userLikedSongs.ids.includes(trackID),
}) })
if (response.code !== 200) throw new Error((response as any).msg)
return response
}, },
{ {
onMutate: async trackID => { onMutate: async trackID => {
@ -57,7 +59,6 @@ export const useMutationLikeATrack = () => {
const newIds = ids.includes(trackID) const newIds = ids.includes(trackID)
? ids.filter(id => id !== trackID) ? ids.filter(id => id !== trackID)
: [...ids, trackID] : [...ids, trackID]
console.log(trackID, ids.includes(trackID), ids, newIds)
return { return {
...likedSongs, ...likedSongs,
ids: newIds, ids: newIds,
@ -70,10 +71,7 @@ export const useMutationLikeATrack = () => {
// If the mutation fails, use the context returned from onMutate to roll back // If the mutation fails, use the context returned from onMutate to roll back
onError: (err, trackID, context) => { onError: (err, trackID, context) => {
queryClient.setQueryData(key, (context as any).previousData) queryClient.setQueryData(key, (context as any).previousData)
}, toast((err as any).toString())
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries(key)
}, },
} }
) )

View file

@ -1,13 +1,25 @@
import type { import { likeAPlaylist } from '@/api/playlist'
FetchUserPlaylistsParams, import type { FetchUserPlaylistsResponse } from '@/api/user'
FetchUserPlaylistsResponse,
} from '@/api/user'
import { UserApiNames, fetchUserPlaylists } from '@/api/user' import { UserApiNames, fetchUserPlaylists } from '@/api/user'
import { useQueryClient } from 'react-query'
import useUser from './useUser'
export default function useUserPlaylists() {
const { data: user } = useUser()
const uid = user?.profile?.userId ?? 0
const params = {
uid: uid,
offset: 0,
limit: 2000,
}
export default function useUserPlaylists(params: FetchUserPlaylistsParams) {
return useQuery( return useQuery(
[UserApiNames.FETCH_USER_PLAYLISTS, params], [UserApiNames.FETCH_USER_PLAYLISTS, uid],
async () => { async () => {
if (!params.uid) {
throw new Error('请登录后再请求用户收藏的歌单')
}
const data = await fetchUserPlaylists(params) const data = await fetchUserPlaylists(params)
return data return data
}, },
@ -27,3 +39,59 @@ export default function useUserPlaylists(params: FetchUserPlaylistsParams) {
} }
) )
} }
export const useMutationLikeAPlaylist = () => {
const queryClient = useQueryClient()
const { data: user } = useUser()
const { data: userPlaylists } = useUserPlaylists()
const uid = user?.account?.id ?? 0
const key = [UserApiNames.FETCH_USER_PLAYLISTS, uid]
return useMutation(
async (playlist: Playlist) => {
if (!playlist.id || userPlaylists?.playlist === undefined) {
throw new Error('playlist id is required or userPlaylists is undefined')
}
const response = await likeAPlaylist({
id: playlist.id,
t:
userPlaylists.playlist.findIndex(p => p.id === playlist.id) > -1
? 2
: 1,
})
if (response.code !== 200) throw new Error((response as any).msg)
return response
},
{
onMutate: async playlist => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(key)
// Snapshot the previous value
const previousData = queryClient.getQueryData(key)
// Optimistically update to the new value
queryClient.setQueryData(key, old => {
const userPlaylists = old as FetchUserPlaylistsResponse
const playlists = userPlaylists.playlist
const newPlaylists =
playlists.findIndex(p => p.id === playlist.id) > -1
? playlists.filter(p => p.id !== playlist.id)
: [...playlists, playlist]
return {
...userPlaylists,
playlist: newPlaylists,
}
})
// Return a context object with the snapshotted value
return { previousData }
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (err, trackID, context) => {
queryClient.setQueryData(key, (context as any).previousData)
toast((err as any).toString())
},
}
)
}

View file

@ -16,6 +16,8 @@ import {
scrollToTop, scrollToTop,
} from '@/utils/common' } from '@/utils/common'
import useTracks from '@/hooks/useTracks' import useTracks from '@/hooks/useTracks'
import useUserAlbums, { useMutationLikeAAlbum } from '@/hooks/useUserAlbums'
import useUser from '@/hooks/useUser'
const PlayButton = ({ const PlayButton = ({
album, album,
@ -79,6 +81,13 @@ const Header = ({
const [isCoverError, setCoverError] = useState(false) const [isCoverError, setCoverError] = useState(false)
const { data: userAlbums } = useUserAlbums()
const isThisAlbumLiked = useMemo(() => {
if (!album) return false
return !!userAlbums?.data?.find(a => a.id === album.id)
}, [album, userAlbums?.data])
const mutationLikeAAlbum = useMutationLikeAAlbum()
return ( return (
<> <>
{/* Header background */} {/* Header background */}
@ -189,11 +198,16 @@ const Header = ({
<Button <Button
color={ButtonColor.Gray} color={ButtonColor.Gray}
iconColor={ButtonColor.Gray} iconColor={
isThisAlbumLiked ? ButtonColor.Primary : ButtonColor.Gray
}
isSkelton={isLoading} isSkelton={isLoading}
onClick={() => toast('施工中...')} onClick={() => album?.id && mutationLikeAAlbum.mutate(album)}
> >
<SvgIcon name='heart-outline' className='h-6 w-6' /> <SvgIcon
name={isThisAlbumLiked ? 'heart' : 'heart-outline'}
className='h-6 w-6'
/>
</Button> </Button>
<Button <Button

View file

@ -12,12 +12,8 @@ import useUserArtists from '@/hooks/useUserArtists'
const LikedTracksCard = ({ className }: { className?: string }) => { const LikedTracksCard = ({ className }: { className?: string }) => {
const navigate = useNavigate() const navigate = useNavigate()
const { data: user } = useUser()
const { data: playlists } = useUserPlaylists({ const { data: playlists } = useUserPlaylists()
uid: user?.account?.id ?? 0,
offset: 0,
})
const { data: likedSongsPlaylist } = usePlaylist({ const { data: likedSongsPlaylist } = usePlaylist({
id: playlists?.playlist?.[0].id ?? 0, id: playlists?.playlist?.[0].id ?? 0,
@ -126,12 +122,7 @@ const OtherCard = ({
} }
const Playlists = () => { const Playlists = () => {
const { data: user } = useUser() const { data: playlists } = useUserPlaylists()
const { data: playlists } = useUserPlaylists({
uid: user?.account?.id ?? 0,
offset: 0,
})
return ( return (
<div> <div>
<CoverRow <CoverRow

View file

@ -8,6 +8,10 @@ import useScroll from '@/hooks/useScroll'
import useTracksInfinite from '@/hooks/useTracksInfinite' import useTracksInfinite from '@/hooks/useTracksInfinite'
import { player } from '@/store' import { player } from '@/store'
import { formatDate, resizeImage } from '@/utils/common' import { formatDate, resizeImage } from '@/utils/common'
import useUserPlaylists, {
useMutationLikeAPlaylist,
} from '@/hooks/useUserPlaylists'
import useUser from '@/hooks/useUser'
const enableRenderLog = true const enableRenderLog = true
@ -24,6 +28,20 @@ const Header = memo(
if (enableRenderLog) console.debug('Rendering Playlist.tsx Header') if (enableRenderLog) console.debug('Rendering Playlist.tsx Header')
const coverUrl = resizeImage(playlist?.coverImgUrl || '', 'lg') const coverUrl = resizeImage(playlist?.coverImgUrl || '', 'lg')
const mutationLikeAPlaylist = useMutationLikeAPlaylist()
const { data: userPlaylists } = useUserPlaylists()
const isThisPlaylistLiked = useMemo(() => {
if (!playlist) return false
return !!userPlaylists?.playlist?.find(p => p.id === playlist.id)
}, [playlist, userPlaylists?.playlist])
const { data: user } = useUser()
const isThisPlaylistCreatedByCurrentUser = useMemo(() => {
if (!playlist || !user) return false
return playlist.creator.userId === user?.profile?.userId
}, [playlist, user])
return ( return (
<> <>
{/* Header background */} {/* Header background */}
@ -114,14 +132,23 @@ const Header = memo(
</Button> </Button>
{!isThisPlaylistCreatedByCurrentUser && (
<Button <Button
color={ButtonColor.Gray} color={ButtonColor.Gray}
iconColor={ButtonColor.Gray} iconColor={
isThisPlaylistLiked ? ButtonColor.Primary : ButtonColor.Gray
}
isSkelton={isLoading} isSkelton={isLoading}
onClick={() => toast('施工中...')} onClick={() =>
playlist?.id && mutationLikeAPlaylist.mutate(playlist)
}
> >
<SvgIcon name='heart-outline' className='h-6 w-6' /> <SvgIcon
name={isThisPlaylistLiked ? 'heart' : 'heart-outline'}
className='h-6 w-6'
/>
</Button> </Button>
)}
<Button <Button
color={ButtonColor.Gray} color={ButtonColor.Gray}