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 {
const result = await handler({
...req.query,
cookie: `MUSIC_U=${req.cookies['MUSIC_U']}`,
cookie: req.cookies,
})
setCache(name, result.body, req.query)

View File

@ -27,3 +27,23 @@ export function fetchAlbum(
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_RECOMMENDED_PLAYLISTS = 'fetchRecommendedPlaylists',
FETCH_DAILY_RECOMMEND_PLAYLISTS = 'fetchDailyRecommendPlaylists',
LIKE_A_PLAYLIST = 'likeAPlaylist',
}
// 歌单详情
@ -70,3 +71,23 @@ export function fetchDailyRecommendPlaylists(): Promise<FetchDailyRecommendPlayl
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
songs: Track[]
}
export function likeATrack(params: LikeATrackParams) {
export function likeATrack(
params: LikeATrackParams
): Promise<LikeATrackResponse> {
return request({
url: '/like',
method: 'post',

View File

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

View File

@ -60,11 +60,7 @@ const PrimaryTabs = () => {
}
const Playlists = () => {
const { data: user } = useUser()
const { data: playlists } = useUserPlaylists({
uid: user?.account?.id ?? 0,
offset: 0,
})
const { data: playlists } = useUserPlaylists()
return (
<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 { 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(
[UserApiNames.FETCH_USER_ALBUMS, params],
[UserApiNames.FETCH_USER_ALBUMS, user?.profile?.userId ?? 0],
() => fetchUserAlbums(params),
{
placeholderData: (): FetchUserAlbumsResponse =>
placeholderData: (): FetchUserAlbumsResponse | undefined =>
window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'album/sublist',
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]
return useMutation(
(trackID: number) => {
async (trackID: number) => {
if (!trackID || userLikedSongs?.ids === undefined) {
throw new Error('trackID is required or userLikedSongs is undefined')
}
return likeATrack({
const response = await likeATrack({
id: trackID,
like: !userLikedSongs.ids.includes(trackID),
})
if (response.code !== 200) throw new Error((response as any).msg)
return response
},
{
onMutate: async trackID => {
@ -57,7 +59,6 @@ export const useMutationLikeATrack = () => {
const newIds = ids.includes(trackID)
? ids.filter(id => id !== trackID)
: [...ids, trackID]
console.log(trackID, ids.includes(trackID), ids, newIds)
return {
...likedSongs,
ids: newIds,
@ -70,10 +71,7 @@ export const useMutationLikeATrack = () => {
// If the mutation fails, use the context returned from onMutate to roll back
onError: (err, trackID, context) => {
queryClient.setQueryData(key, (context as any).previousData)
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries(key)
toast((err as any).toString())
},
}
)

View File

@ -1,13 +1,25 @@
import type {
FetchUserPlaylistsParams,
FetchUserPlaylistsResponse,
} from '@/api/user'
import { likeAPlaylist } from '@/api/playlist'
import type { FetchUserPlaylistsResponse } 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(
[UserApiNames.FETCH_USER_PLAYLISTS, params],
[UserApiNames.FETCH_USER_PLAYLISTS, uid],
async () => {
if (!params.uid) {
throw new Error('请登录后再请求用户收藏的歌单')
}
const data = await fetchUserPlaylists(params)
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,
} from '@/utils/common'
import useTracks from '@/hooks/useTracks'
import useUserAlbums, { useMutationLikeAAlbum } from '@/hooks/useUserAlbums'
import useUser from '@/hooks/useUser'
const PlayButton = ({
album,
@ -79,6 +81,13 @@ const Header = ({
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 (
<>
{/* Header background */}
@ -189,11 +198,16 @@ const Header = ({
<Button
color={ButtonColor.Gray}
iconColor={ButtonColor.Gray}
iconColor={
isThisAlbumLiked ? ButtonColor.Primary : ButtonColor.Gray
}
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

View File

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

View File

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