mirror of
https://github.com/qier222/YesPlayMusic.git
synced 2025-02-22 04:32:51 +08:00
feat: 支持收藏歌单和专辑
This commit is contained in:
parent
db5730dfdd
commit
49bb849982
@ -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)
|
||||
|
@ -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(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -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(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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'>
|
||||
|
@ -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())
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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())
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@ -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())
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
Loading…
x
Reference in New Issue
Block a user