mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-17 05:38:04 +00:00
feat: monorepo
This commit is contained in:
parent
4d54060a4f
commit
42089d4996
200 changed files with 1530 additions and 1521 deletions
56
packages/web/hooks/useAlbum.ts
Normal file
56
packages/web/hooks/useAlbum.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { fetchAlbum } from '@/web/api/album'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import {
|
||||
FetchAlbumParams,
|
||||
AlbumApiNames,
|
||||
FetchAlbumResponse,
|
||||
} from '@/shared/api/Album'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
const fetch = async (params: FetchAlbumParams, noCache?: boolean) => {
|
||||
const album = await fetchAlbum(params, !!noCache)
|
||||
if (album?.album?.songs) {
|
||||
album.album.songs = album.songs
|
||||
}
|
||||
return album
|
||||
}
|
||||
|
||||
export default function useAlbum(params: FetchAlbumParams, noCache?: boolean) {
|
||||
return useQuery(
|
||||
[AlbumApiNames.FetchAlbum, params.id],
|
||||
() => fetch(params, noCache),
|
||||
{
|
||||
enabled: !!params.id,
|
||||
staleTime: 24 * 60 * 60 * 1000, // 24 hours
|
||||
placeholderData: (): FetchAlbumResponse =>
|
||||
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||
api: APIs.Album,
|
||||
query: {
|
||||
id: params.id,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAlbumWithReactQuery(params: FetchAlbumParams) {
|
||||
return reactQueryClient.fetchQuery(
|
||||
[AlbumApiNames.FetchAlbum, params.id],
|
||||
() => fetch(params),
|
||||
{
|
||||
staleTime: Infinity,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function prefetchAlbum(params: FetchAlbumParams) {
|
||||
await reactQueryClient.prefetchQuery(
|
||||
[AlbumApiNames.FetchAlbum, params.id],
|
||||
() => fetch(params),
|
||||
{
|
||||
staleTime: Infinity,
|
||||
}
|
||||
)
|
||||
}
|
||||
30
packages/web/hooks/useArtist.ts
Normal file
30
packages/web/hooks/useArtist.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { fetchArtist } from '@/web/api/artist'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import {
|
||||
FetchArtistParams,
|
||||
ArtistApiNames,
|
||||
FetchArtistResponse,
|
||||
} from '@/shared/api/Artist'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
export default function useArtist(
|
||||
params: FetchArtistParams,
|
||||
noCache?: boolean
|
||||
) {
|
||||
return useQuery(
|
||||
[ArtistApiNames.FetchArtist, params],
|
||||
() => fetchArtist(params, !!noCache),
|
||||
{
|
||||
enabled: !!params.id && params.id > 0 && !isNaN(Number(params.id)),
|
||||
staleTime: 5 * 60 * 1000, // 5 mins
|
||||
placeholderData: (): FetchArtistResponse =>
|
||||
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||
api: APIs.Artist,
|
||||
query: {
|
||||
id: params.id,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
}
|
||||
30
packages/web/hooks/useArtistAlbums.ts
Normal file
30
packages/web/hooks/useArtistAlbums.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { fetchArtistAlbums } from '@/web/api/artist'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import {
|
||||
FetchArtistAlbumsParams,
|
||||
ArtistApiNames,
|
||||
FetchArtistAlbumsResponse,
|
||||
} from '@/shared/api/Artist'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
export default function useUserAlbums(params: FetchArtistAlbumsParams) {
|
||||
return useQuery(
|
||||
[ArtistApiNames.FetchArtistAlbums, params],
|
||||
async () => {
|
||||
const data = await fetchArtistAlbums(params)
|
||||
return data
|
||||
},
|
||||
{
|
||||
enabled: !!params.id && params.id !== 0,
|
||||
staleTime: 3600000,
|
||||
placeholderData: (): FetchArtistAlbumsResponse =>
|
||||
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||
api: APIs.ArtistAlbum,
|
||||
query: {
|
||||
id: params.id,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
}
|
||||
17
packages/web/hooks/useCoverColor.ts
Normal file
17
packages/web/hooks/useCoverColor.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { colord } from 'colord'
|
||||
import { getCoverColor } from '../utils/common'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function useCoverColor(url: string) {
|
||||
const [color, setColor] = useState({ from: '#fff', to: '#fff' })
|
||||
|
||||
useEffect(() => {
|
||||
getCoverColor(url || '').then(color => {
|
||||
if (!color) return
|
||||
const to = colord(color).darken(0.15).rotate(-5).toHex()
|
||||
setColor({ from: color, to })
|
||||
})
|
||||
}, [url])
|
||||
|
||||
return color
|
||||
}
|
||||
13
packages/web/hooks/useIpcRenderer.ts
Normal file
13
packages/web/hooks/useIpcRenderer.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { IpcChannelsParams, IpcChannelsReturns } from '@/shared/IpcChannels'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const useIpcRenderer = <T extends keyof IpcChannelsParams>(
|
||||
channcel: T,
|
||||
listener: (event: any, value: IpcChannelsReturns[T]) => void
|
||||
) => {
|
||||
useEffect(() => {
|
||||
return window.ipcRenderer?.on(channcel, listener)
|
||||
}, [])
|
||||
}
|
||||
|
||||
export default useIpcRenderer
|
||||
47
packages/web/hooks/useLyric.ts
Normal file
47
packages/web/hooks/useLyric.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { fetchLyric } from '@/web/api/track'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
import {
|
||||
FetchLyricParams,
|
||||
FetchLyricResponse,
|
||||
TrackApiNames,
|
||||
} from '@/shared/api/Track'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
export default function useLyric(params: FetchLyricParams) {
|
||||
return useQuery(
|
||||
[TrackApiNames.FetchLyric, params],
|
||||
() => {
|
||||
return fetchLyric(params)
|
||||
},
|
||||
{
|
||||
enabled: !!params.id && params.id !== 0,
|
||||
refetchInterval: false,
|
||||
staleTime: Infinity,
|
||||
initialData: (): FetchLyricResponse | undefined =>
|
||||
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||
api: APIs.Lyric,
|
||||
query: {
|
||||
id: params.id,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchTracksWithReactQuery(params: FetchLyricParams) {
|
||||
return reactQueryClient.fetchQuery(
|
||||
[TrackApiNames.FetchLyric, params],
|
||||
() => {
|
||||
return fetchLyric(params)
|
||||
},
|
||||
{
|
||||
retry: 4,
|
||||
retryDelay: (retryCount: number) => {
|
||||
return retryCount * 500
|
||||
},
|
||||
staleTime: Infinity,
|
||||
}
|
||||
)
|
||||
}
|
||||
18
packages/web/hooks/usePersonalFM.ts
Normal file
18
packages/web/hooks/usePersonalFM.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { fetchPersonalFM, PersonalFMApiNames } from '@/web/api/personalFM'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
|
||||
export function fetchPersonalFMWithReactQuery() {
|
||||
return reactQueryClient.fetchQuery(
|
||||
PersonalFMApiNames.FetchPersonalFm,
|
||||
async () => {
|
||||
const data = await fetchPersonalFM()
|
||||
if (!data.data?.length) {
|
||||
throw new Error('No data')
|
||||
}
|
||||
return data
|
||||
},
|
||||
{
|
||||
retry: 3,
|
||||
}
|
||||
)
|
||||
}
|
||||
55
packages/web/hooks/usePlaylist.ts
Normal file
55
packages/web/hooks/usePlaylist.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { fetchPlaylist } from '@/web/api/playlist'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import {
|
||||
FetchPlaylistParams,
|
||||
PlaylistApiNames,
|
||||
FetchPlaylistResponse,
|
||||
} from '@/shared/api/Playlists'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
const fetch = (params: FetchPlaylistParams, noCache?: boolean) => {
|
||||
return fetchPlaylist(params, !!noCache)
|
||||
}
|
||||
|
||||
export default function usePlaylist(
|
||||
params: FetchPlaylistParams,
|
||||
noCache?: boolean
|
||||
) {
|
||||
return useQuery(
|
||||
[PlaylistApiNames.FetchPlaylist, params],
|
||||
() => fetch(params, noCache),
|
||||
{
|
||||
enabled: !!(params.id && params.id > 0 && !isNaN(Number(params.id))),
|
||||
refetchOnWindowFocus: true,
|
||||
placeholderData: (): FetchPlaylistResponse | undefined =>
|
||||
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||
api: APIs.Playlist,
|
||||
query: {
|
||||
id: params.id,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchPlaylistWithReactQuery(params: FetchPlaylistParams) {
|
||||
return reactQueryClient.fetchQuery(
|
||||
[PlaylistApiNames.FetchPlaylist, params],
|
||||
() => fetch(params),
|
||||
{
|
||||
staleTime: 3600000,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function prefetchPlaylist(params: FetchPlaylistParams) {
|
||||
await reactQueryClient.prefetchQuery(
|
||||
[PlaylistApiNames.FetchPlaylist, params],
|
||||
() => fetch(params),
|
||||
{
|
||||
staleTime: 3600000,
|
||||
}
|
||||
)
|
||||
}
|
||||
74
packages/web/hooks/useScroll.ts
Normal file
74
packages/web/hooks/useScroll.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// Inspired by https://github.com/vueuse/vueuse
|
||||
import { throttle as lodashThrottle } from 'lodash-es'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface ArrivedState {
|
||||
top: boolean
|
||||
bottom: boolean
|
||||
left: boolean
|
||||
right: boolean
|
||||
}
|
||||
|
||||
interface Offset {
|
||||
top?: number
|
||||
bottom?: number
|
||||
left?: number
|
||||
right?: number
|
||||
}
|
||||
|
||||
const useScroll = (
|
||||
ref: React.RefObject<HTMLDivElement> | HTMLElement | null,
|
||||
{ offset, throttle }: { offset?: Offset; throttle?: number } = {}
|
||||
) => {
|
||||
const [scroll, setScroll] = useState<{
|
||||
x: number
|
||||
y: number
|
||||
arrivedState: ArrivedState
|
||||
}>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
arrivedState: {
|
||||
top: true,
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref) return
|
||||
const handleScroll = (e: Event) => {
|
||||
if (!e.target) return
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
const arrivedState: ArrivedState = {
|
||||
left: target.scrollLeft <= 0 + (offset?.left || 0),
|
||||
right:
|
||||
target.scrollLeft + target.clientWidth >=
|
||||
target.scrollWidth - (offset?.right || 0),
|
||||
top: target.scrollTop <= 0 + (offset?.top || 0),
|
||||
bottom:
|
||||
target.scrollTop + target.clientHeight >=
|
||||
target.scrollHeight - (offset?.bottom || 0),
|
||||
}
|
||||
|
||||
setScroll({
|
||||
x: target.scrollLeft,
|
||||
y: target.scrollTop,
|
||||
arrivedState,
|
||||
})
|
||||
}
|
||||
|
||||
const readHandleScroll = throttle
|
||||
? lodashThrottle(handleScroll, throttle)
|
||||
: handleScroll
|
||||
|
||||
const element = 'current' in ref ? ref?.current : ref
|
||||
element?.addEventListener('scroll', readHandleScroll)
|
||||
return () => element?.removeEventListener('scroll', readHandleScroll)
|
||||
}, [offset?.bottom, offset?.left, offset?.right, offset?.top, ref, throttle])
|
||||
|
||||
return scroll
|
||||
}
|
||||
|
||||
export default useScroll
|
||||
62
packages/web/hooks/useTracks.ts
Normal file
62
packages/web/hooks/useTracks.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { fetchAudioSource, fetchTracks } from '@/web/api/track'
|
||||
import type {} from '@/web/api/track'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import {
|
||||
FetchAudioSourceParams,
|
||||
FetchTracksParams,
|
||||
FetchTracksResponse,
|
||||
TrackApiNames,
|
||||
} from '@/shared/api/Track'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
export default function useTracks(params: FetchTracksParams) {
|
||||
return useQuery(
|
||||
[TrackApiNames.FetchTracks, params],
|
||||
() => {
|
||||
return fetchTracks(params)
|
||||
},
|
||||
{
|
||||
enabled: params.ids.length !== 0,
|
||||
refetchInterval: false,
|
||||
staleTime: Infinity,
|
||||
initialData: (): FetchTracksResponse | undefined =>
|
||||
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||
api: APIs.Track,
|
||||
query: {
|
||||
ids: params.ids.join(','),
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchTracksWithReactQuery(params: FetchTracksParams) {
|
||||
return reactQueryClient.fetchQuery(
|
||||
[TrackApiNames.FetchTracks, params],
|
||||
() => {
|
||||
return fetchTracks(params)
|
||||
},
|
||||
{
|
||||
retry: 4,
|
||||
retryDelay: (retryCount: number) => {
|
||||
return retryCount * 500
|
||||
},
|
||||
staleTime: 86400000,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAudioSourceWithReactQuery(params: FetchAudioSourceParams) {
|
||||
return reactQueryClient.fetchQuery(
|
||||
[TrackApiNames.FetchAudioSource, params],
|
||||
() => {
|
||||
return fetchAudioSource(params)
|
||||
},
|
||||
{
|
||||
retry: 3,
|
||||
staleTime: 0, // TODO: Web版1小时缓存
|
||||
}
|
||||
)
|
||||
}
|
||||
33
packages/web/hooks/useTracksInfinite.ts
Normal file
33
packages/web/hooks/useTracksInfinite.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { FetchTracksParams, TrackApiNames } from '@/shared/api/Track'
|
||||
import { useInfiniteQuery } from 'react-query'
|
||||
import { fetchTracks } from '../api/track'
|
||||
|
||||
// 100 tracks each page
|
||||
const offset = 100
|
||||
|
||||
export default function useTracksInfinite(params: FetchTracksParams) {
|
||||
return useInfiniteQuery(
|
||||
[TrackApiNames.FetchTracks, params],
|
||||
({ pageParam = 0 }) => {
|
||||
const cursorStart = pageParam * offset
|
||||
const cursorEnd = cursorStart + offset
|
||||
const ids = params.ids.slice(cursorStart, cursorEnd)
|
||||
return fetchTracks({ ids })
|
||||
},
|
||||
{
|
||||
enabled: params.ids.length !== 0,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: 0,
|
||||
staleTime: Infinity,
|
||||
getNextPageParam: (lastPage, pages) => {
|
||||
// 当 return undefined 时,hasNextPage会等于false
|
||||
// 当 return 非 undefined 时,return 的数据会传入上面的fetchTracks函数中
|
||||
return pages.length * offset < params.ids.length // 判断是否还有下一页
|
||||
? pages.length
|
||||
: undefined
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
15
packages/web/hooks/useUser.ts
Normal file
15
packages/web/hooks/useUser.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { fetchUserAccount } from '@/web/api/user'
|
||||
import { UserApiNames, FetchUserAccountResponse } from '@/shared/api/User'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
export default function useUser() {
|
||||
return useQuery(UserApiNames.FetchUserAccount, fetchUserAccount, {
|
||||
refetchOnWindowFocus: true,
|
||||
placeholderData: (): FetchUserAccountResponse | undefined =>
|
||||
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||
api: APIs.UserAccount,
|
||||
}),
|
||||
})
|
||||
}
|
||||
80
packages/web/hooks/useUserAlbums.ts
Normal file
80
packages/web/hooks/useUserAlbums.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { likeAAlbum } from '@/web/api/album'
|
||||
import { useMutation, useQuery, useQueryClient } from 'react-query'
|
||||
import useUser from './useUser'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import {
|
||||
FetchUserAlbumsParams,
|
||||
UserApiNames,
|
||||
FetchUserAlbumsResponse,
|
||||
} from '@/shared/api/User'
|
||||
import { fetchUserAlbums } from '../api/user'
|
||||
|
||||
export default function useUserAlbums(params: FetchUserAlbumsParams = {}) {
|
||||
const { data: user } = useUser()
|
||||
return useQuery(
|
||||
[UserApiNames.FetchUserAlbums, user?.profile?.userId ?? 0],
|
||||
() => fetchUserAlbums(params),
|
||||
{
|
||||
refetchOnWindowFocus: true,
|
||||
placeholderData: (): FetchUserAlbumsResponse | undefined =>
|
||||
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||
api: APIs.UserAlbums,
|
||||
query: params,
|
||||
}),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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.FetchUserAlbums, 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())
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
15
packages/web/hooks/useUserArtists.ts
Normal file
15
packages/web/hooks/useUserArtists.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { fetchUserArtists } from '@/web/api/user'
|
||||
import { UserApiNames, FetchUserArtistsResponse } from '@/shared/api/User'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
export default function useUserArtists() {
|
||||
return useQuery([UserApiNames.FetchUserArtist], fetchUserArtists, {
|
||||
refetchOnWindowFocus: true,
|
||||
placeholderData: (): FetchUserArtistsResponse =>
|
||||
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||
api: APIs.UserArtists,
|
||||
}),
|
||||
})
|
||||
}
|
||||
85
packages/web/hooks/useUserLikedTracksIDs.ts
Normal file
85
packages/web/hooks/useUserLikedTracksIDs.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { likeATrack } from '@/web/api/track'
|
||||
import useUser from './useUser'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { fetchUserLikedTracksIDs } from '../api/user'
|
||||
import {
|
||||
FetchUserLikedTracksIDsResponse,
|
||||
UserApiNames,
|
||||
} from '@/shared/api/User'
|
||||
import { useQuery } from 'react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
export default function useUserLikedTracksIDs() {
|
||||
const { data: user } = useUser()
|
||||
const uid = user?.account?.id ?? 0
|
||||
|
||||
return useQuery(
|
||||
[UserApiNames.FetchUserLikedTracksIds, uid],
|
||||
() => fetchUserLikedTracksIDs({ uid }),
|
||||
{
|
||||
enabled: !!(uid && uid !== 0),
|
||||
refetchOnWindowFocus: true,
|
||||
placeholderData: (): FetchUserLikedTracksIDsResponse | undefined =>
|
||||
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||
api: APIs.Likelist,
|
||||
query: {
|
||||
uid,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const useMutationLikeATrack = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const { data: user } = useUser()
|
||||
const { data: userLikedSongs } = useUserLikedTracksIDs()
|
||||
const uid = user?.account?.id ?? 0
|
||||
const key = [UserApiNames.FetchUserLikedTracksIds, uid]
|
||||
|
||||
return useMutation(
|
||||
async (trackID: number) => {
|
||||
if (!trackID || userLikedSongs?.ids === undefined) {
|
||||
throw new Error('trackID is required or userLikedSongs is undefined')
|
||||
}
|
||||
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 => {
|
||||
// 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 likedSongs = old as FetchUserLikedTracksIDsResponse
|
||||
const ids = likedSongs.ids
|
||||
const newIds = ids.includes(trackID)
|
||||
? ids.filter(id => id !== trackID)
|
||||
: [...ids, trackID]
|
||||
return {
|
||||
...likedSongs,
|
||||
ids: newIds,
|
||||
}
|
||||
})
|
||||
|
||||
// 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())
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
100
packages/web/hooks/useUserPlaylists.ts
Normal file
100
packages/web/hooks/useUserPlaylists.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { likeAPlaylist } from '@/web/api/playlist'
|
||||
import { useMutation, useQuery, useQueryClient } from 'react-query'
|
||||
import useUser from './useUser'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { fetchUserPlaylists } from '@/web/api/user'
|
||||
import { FetchUserPlaylistsResponse, UserApiNames } from '@/shared/api/User'
|
||||
|
||||
export default function useUserPlaylists() {
|
||||
const { data: user } = useUser()
|
||||
const uid = user?.profile?.userId ?? 0
|
||||
|
||||
const params = {
|
||||
uid: uid,
|
||||
offset: 0,
|
||||
limit: 2000,
|
||||
}
|
||||
|
||||
return useQuery(
|
||||
[UserApiNames.FetchUserPlaylists, uid],
|
||||
async () => {
|
||||
if (!params.uid) {
|
||||
throw new Error('请登录后再请求用户收藏的歌单')
|
||||
}
|
||||
const data = await fetchUserPlaylists(params)
|
||||
return data
|
||||
},
|
||||
{
|
||||
enabled: !!(
|
||||
!!params.uid &&
|
||||
params.uid !== 0 &&
|
||||
params.offset !== undefined
|
||||
),
|
||||
refetchOnWindowFocus: true,
|
||||
placeholderData: (): FetchUserPlaylistsResponse =>
|
||||
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||
api: APIs.UserPlaylist,
|
||||
query: {
|
||||
uid: params.uid,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const useMutationLikeAPlaylist = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const { data: user } = useUser()
|
||||
const { data: userPlaylists } = useUserPlaylists()
|
||||
const uid = user?.account?.id ?? 0
|
||||
const key = [UserApiNames.FetchUserPlaylists, 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())
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue