diff --git a/package.json b/package.json index 6aa41e2..1bbfee4 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "repository": "github:qier222/YesPlayMusic", "main": "dist/main/index.js", "scripts": { + "install": "node scripts/build.sqlite3.mjs", "dev": "concurrently -n=vite,main -c=#646cff,#74b1be \"npm run dev:renderer\" \"node scripts/build.main.mjs --watch\"", "dev:renderer": "vite dev", "build:main": "node scripts/build.main.mjs", diff --git a/src/main/rendererPreload.ts b/src/main/rendererPreload.ts index 274a1bc..c2dd10f 100644 --- a/src/main/rendererPreload.ts +++ b/src/main/rendererPreload.ts @@ -1,4 +1,9 @@ const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('ipcRenderer', ipcRenderer) -contextBridge.exposeInMainWorld('isElectron', true) +contextBridge.exposeInMainWorld('env', { + isElectron: true, + isLinux: process.platform === 'linux', + isMac: process.platform === 'darwin', + isWin: process.platform === 'win32', +}) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 3ef507d..03e29bc 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -5,10 +5,13 @@ import Player from '@/components/Player' import Sidebar from '@/components/Sidebar' import reactQueryClient from '@/utils/reactQueryClient' import Main from './components/Main' +import TitleBar from './components/Titlebar' const App = () => { return ( + {window.env?.isWin && } +
diff --git a/src/renderer/api/track.ts b/src/renderer/api/track.ts index b4df467..6a08b3a 100644 --- a/src/renderer/api/track.ts +++ b/src/renderer/api/track.ts @@ -3,6 +3,7 @@ import request from '@/utils/request' export enum TrackApiNames { FETCH_TRACKS = 'fetchTracks', FETCH_AUDIO_SOURCE = 'fetchAudioSource', + FETCH_LYRIC = 'fetchLyric', } // 获取歌曲详情 @@ -71,3 +72,51 @@ export function fetchAudioSource( params, }) } + +// 获取歌词 +export interface FetchLyricParams { + id: number +} +export interface FetchLyricResponse { + code: number + sgc: boolean + sfy: boolean + qfy: boolean + lyricUser?: { + id: number + status: number + demand: number + userid: number + nickname: string + uptime: number + } + transUser?: { + id: number + status: number + demand: number + userid: number + nickname: string + uptime: number + } + lrc: { + version: number + lyric: string + } + klyric?: { + version: number + lyric: string + } + tlyric?: { + version: number + lyric: string + } +} +export function fetchLyric( + params: FetchLyricParams +): Promise { + return request({ + url: '/lyric', + method: 'get', + params, + }) +} diff --git a/src/renderer/assets/icons/heart-outline.svg b/src/renderer/assets/icons/heart-outline.svg index 8e9ad38..24f30e1 100644 --- a/src/renderer/assets/icons/heart-outline.svg +++ b/src/renderer/assets/icons/heart-outline.svg @@ -1,4 +1,3 @@ - - + diff --git a/src/renderer/assets/icons/heart.svg b/src/renderer/assets/icons/heart.svg index 31a6186..50426fd 100644 --- a/src/renderer/assets/icons/heart.svg +++ b/src/renderer/assets/icons/heart.svg @@ -1,5 +1,3 @@ - - - + diff --git a/src/renderer/assets/icons/windows-close.svg b/src/renderer/assets/icons/windows-close.svg new file mode 100644 index 0000000..cc821a9 --- /dev/null +++ b/src/renderer/assets/icons/windows-close.svg @@ -0,0 +1 @@ + diff --git a/src/renderer/assets/icons/windows-max.svg b/src/renderer/assets/icons/windows-max.svg new file mode 100644 index 0000000..79aa709 --- /dev/null +++ b/src/renderer/assets/icons/windows-max.svg @@ -0,0 +1 @@ + diff --git a/src/renderer/assets/icons/windows-min.svg b/src/renderer/assets/icons/windows-min.svg new file mode 100644 index 0000000..0604039 --- /dev/null +++ b/src/renderer/assets/icons/windows-min.svg @@ -0,0 +1 @@ + diff --git a/src/renderer/assets/icons/windows-unmax.svg b/src/renderer/assets/icons/windows-unmax.svg new file mode 100644 index 0000000..4e08199 --- /dev/null +++ b/src/renderer/assets/icons/windows-unmax.svg @@ -0,0 +1 @@ + diff --git a/src/renderer/components/FMCard.tsx b/src/renderer/components/FMCard.tsx index 79d6aa1..168f829 100644 --- a/src/renderer/components/FMCard.tsx +++ b/src/renderer/components/FMCard.tsx @@ -5,7 +5,6 @@ import { resizeImage } from '@/utils/common' import SvgIcon from '@/components/SvgIcon' import ArtistInline from '@/components/ArtistsInline' import { State as PlayerState, Mode as PlayerMode } from '@/utils/player' -import Skeleton from './Skeleton' const MediaControls = () => { const classes = @@ -66,8 +65,9 @@ const FMCard = () => { ) useEffect(() => { - if (coverUrl) { - average(coverUrl, { amount: 1, format: 'hex', sample: 1 }).then(color => { + const cover = resizeImage(playerSnapshot.fmTrack?.al?.picUrl ?? '', 'xs') + if (cover) { + average(cover, { amount: 1, format: 'hex', sample: 1 }).then(color => { let c = colord(color as string) if (c.isLight()) c = c.darken(0.15) else if (c.isDark()) c = c.lighten(0.1) @@ -75,7 +75,7 @@ const FMCard = () => { setBackground(`linear-gradient(to bottom right, ${c.toHex()}, ${to})`) }) } - }, [coverUrl]) + }, [playerSnapshot.fmTrack?.al?.picUrl]) return (
{ toast('施工中...')}> , }, + { + path: '/library', + element: , + }, { path: '/login', element: , diff --git a/src/renderer/components/TitleBar.tsx b/src/renderer/components/TitleBar.tsx new file mode 100644 index 0000000..e9a9f5d --- /dev/null +++ b/src/renderer/components/TitleBar.tsx @@ -0,0 +1,22 @@ +import SvgIcon from './SvgIcon' + +const TitleBar = () => { + return ( +
+
YesPlayMusic
+
+ + + +
+
+ ) +} + +export default TitleBar diff --git a/src/renderer/global.d.ts b/src/renderer/global.d.ts index 78350dd..b62c23e 100644 --- a/src/renderer/global.d.ts +++ b/src/renderer/global.d.ts @@ -4,7 +4,12 @@ declare global { interface Window { // Expose some Api through preload script ipcRenderer?: import('electron').IpcRenderer - isElectron?: boolean + env?: { + isElectron: boolean + isLinux: boolean + isMac: boolean + isWin: boolean + } } } diff --git a/src/renderer/hooks/useLyric.ts b/src/renderer/hooks/useLyric.ts new file mode 100644 index 0000000..6bb049e --- /dev/null +++ b/src/renderer/hooks/useLyric.ts @@ -0,0 +1,40 @@ +import { TrackApiNames, fetchLyric } from '@/api/track' +import type { FetchLyricParams, FetchLyricResponse } from '@/api/track' +import reactQueryClient from '@/utils/reactQueryClient' + +export default function useLyric(params: FetchLyricParams) { + return useQuery( + [TrackApiNames.FETCH_LYRIC, params], + () => { + return fetchLyric(params) + }, + { + enabled: !!params.id && params.id !== 0, + refetchInterval: false, + staleTime: Infinity, + initialData: (): FetchLyricResponse | undefined => + window.ipcRenderer?.sendSync('getApiCacheSync', { + api: 'lyric', + query: { + id: params.id, + }, + }), + } + ) +} + +export function fetchTracksWithReactQuery(params: FetchLyricParams) { + return reactQueryClient.fetchQuery( + [TrackApiNames.FETCH_LYRIC, params], + () => { + return fetchLyric(params) + }, + { + retry: 4, + retryDelay: (retryCount: number) => { + return retryCount * 500 + }, + staleTime: Infinity, + } + ) +} diff --git a/src/renderer/pages/Home.tsx b/src/renderer/pages/Home.tsx index c2f845e..8cffdb9 100644 --- a/src/renderer/pages/Home.tsx +++ b/src/renderer/pages/Home.tsx @@ -40,9 +40,11 @@ export default function Home() { ) const playlists = [ - ...(dailyRecommendPlaylists?.recommend ?? []), + ...(dailyRecommendPlaylists?.recommend?.slice(1) ?? []), ...(recommendedPlaylists?.result ?? []), - ].slice(0, 10) + ] + .slice(0, 10) + .reverse() return (
diff --git a/src/renderer/pages/Library.tsx b/src/renderer/pages/Library.tsx new file mode 100644 index 0000000..6f835c4 --- /dev/null +++ b/src/renderer/pages/Library.tsx @@ -0,0 +1,155 @@ +import SvgIcon from '@/components/SvgIcon' +import useLyric from '@/hooks/useLyric' +import usePlaylist from '@/hooks/usePlaylist' +import useUser from '@/hooks/useUser' +import useUserPlaylists from '@/hooks/useUserPlaylists' +import { player } from '@/store' +import { resizeImage } from '@/utils/common' +import { sample, chunk } from 'lodash-es' + +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: likedSongsPlaylist } = usePlaylist({ + id: playlists?.playlist?.[0].id ?? 0, + }) + + // Lyric + const [trackID, setTrackID] = useState(0) + + useEffect(() => { + if (trackID === 0) { + setTrackID( + sample(likedSongsPlaylist?.playlist.trackIds?.map(t => t.id) ?? []) ?? 0 + ) + } + }, [likedSongsPlaylist?.playlist.trackIds, trackID]) + + const { data: lyric } = useLyric({ + id: trackID, + }) + + const lyricLines = useMemo(() => { + return ( + sample( + chunk( + lyric?.lrc.lyric + ?.split('\n') + ?.map(l => l.split(']')[1]?.trim()) + ?.filter( + l => + l && + !l.includes('作词') && + !l.includes('作曲') && + !l.includes('纯音乐,请欣赏') + ), + 3 + ) + ) ?? [] + ) + }, [lyric]) + + const handlePlay = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation() + if (!likedSongsPlaylist?.playlist.id) { + toast('无法播放歌单') + return + } + player.playPlaylist(likedSongsPlaylist.playlist.id) + }, + [likedSongsPlaylist?.playlist.id] + ) + + return ( +
+ likedSongsPlaylist?.playlist.id && + navigate(`/playlist/${likedSongsPlaylist.playlist.id}`) + } + className={classNames( + 'relative flex h-full w-full flex-col justify-between rounded-2xl bg-brand-50 py-5 px-6 text-brand-600 ', + className + )} + > +
+ {lyricLines.map((line, index) => ( +
{line}
+ ))} +
+
+
我喜欢的音乐
+
+ {likedSongsPlaylist?.playlist.trackCount ?? 0} 首歌 +
+
+ + +
+ ) +} + +const OtherCard = ({ + name, + icon, + className, +}: { + name: string + icon: string + className?: string +}) => { + return ( +
+ + {name} +
+ ) +} + +const Tabs = () => { + return
+} + +const Library = () => { + const { data: user } = useUser() + + const avatarUrl = useMemo( + () => resizeImage(user?.profile?.avatarUrl ?? '', 'sm'), + [user?.profile?.avatarUrl] + ) + + return ( +
+
+ + {user?.profile?.nickname}的音乐库 +
+ +
+ + + + + +
+
+ ) +} + +export default Library diff --git a/src/renderer/utils/player.ts b/src/renderer/utils/player.ts index 4d36bd5..32490d9 100644 --- a/src/renderer/utils/player.ts +++ b/src/renderer/utils/player.ts @@ -244,7 +244,10 @@ export class Player { const prefetchNextTrack = async () => { const prefetchTrackID = this.fmTrackList[1] const track = await this._fetchTrack(prefetchTrackID) - if (track?.al.picUrl) axios.get(resizeImage(track.al.picUrl, 'md')) + if (track?.al.picUrl) { + axios.get(resizeImage(track.al.picUrl, 'md')) + axios.get(resizeImage(track.al.picUrl, 'xs')) + } } if (this.fmTrackList.length === 0) await loadMoreTracks()