prefetch(item.id)}
- className='grid gap-x-[24px] gap-y-7'
+ className='grid gap-x-6 gap-y-7'
>
{/* Cover */}
diff --git a/packages/renderer/src/components/Router.tsx b/packages/renderer/src/components/Router.tsx
index fe3f909..9272695 100644
--- a/packages/renderer/src/components/Router.tsx
+++ b/packages/renderer/src/components/Router.tsx
@@ -5,6 +5,7 @@ import Album from '@/pages/Album'
import Home from '@/pages/Home'
import Login from '@/pages/Login'
import Playlist from '@/pages/Playlist'
+import Artist from '@/pages/Artist'
const routes: RouteObject[] = [
{
@@ -23,11 +24,15 @@ const routes: RouteObject[] = [
path: '/album/:id',
element:
,
},
+ {
+ path: '/artist/:id',
+ element:
,
+ },
]
-const router = () => {
+const Router = () => {
const element = useRoutes(routes)
return
{element}
}
-export default router
+export default Router
diff --git a/packages/renderer/src/components/Sidebar.tsx b/packages/renderer/src/components/Sidebar.tsx
index 6dc6625..c337027 100644
--- a/packages/renderer/src/components/Sidebar.tsx
+++ b/packages/renderer/src/components/Sidebar.tsx
@@ -7,7 +7,7 @@ import { prefetchPlaylist } from '@/hooks/usePlaylist'
interface Tab {
name: string
- icon?: string
+ icon: string
route: string
}
interface PrimaryTab extends Tab {
@@ -41,7 +41,7 @@ const PrimaryTabs = () => {
onClick={() => scrollToTop()}
key={tab.route}
to={tab.route}
- className={({ isActive }: { isActive: boolean }) =>
+ className={({ isActive }) =>
classNames(
'btn-hover-animation mx-3 flex cursor-default items-center rounded-lg px-3 py-2 transition-colors duration-200 after:scale-[0.97] after:bg-black/[.06] dark:after:bg-white/20',
!isActive && 'text-gray-700 dark:text-white',
diff --git a/packages/renderer/src/components/Slider.tsx b/packages/renderer/src/components/Slider.tsx
index 5c75b25..bad8604 100644
--- a/packages/renderer/src/components/Slider.tsx
+++ b/packages/renderer/src/components/Slider.tsx
@@ -131,7 +131,7 @@ const Slider = ({
diff --git a/packages/renderer/src/components/TracksGrid.tsx b/packages/renderer/src/components/TracksGrid.tsx
index ea72635..395be76 100644
--- a/packages/renderer/src/components/TracksGrid.tsx
+++ b/packages/renderer/src/components/TracksGrid.tsx
@@ -2,18 +2,25 @@ import ArtistInline from '@/components/ArtistsInline'
import Skeleton from '@/components/Skeleton'
import { resizeImage } from '@/utils/common'
-const TrackListGrid = ({
+const Track = ({
track,
isSkeleton = false,
+ isHighlight = false,
}: {
track: Track
isSkeleton: boolean
+ isHighlight: boolean
}) => {
return (
@@ -35,17 +42,19 @@ const TrackListGrid = ({
{!isSkeleton && (
{track.name}
)}
{isSkeleton && (
-
PLACEHOLDER12345
+
PLACEHOLDER12345
)}
-
- {!isSkeleton &&
}
+
+ {!isSkeleton && (
+
+ )}
{isSkeleton && (
PLACE
)}
@@ -56,4 +65,22 @@ const TrackListGrid = ({
)
}
-export default TrackListGrid
+const TrackGrid = ({
+ tracks,
+ isSkeleton = false,
+ onTrackDoubleClick,
+}: {
+ tracks: Track[]
+ isSkeleton?: boolean
+ onTrackDoubleClick?: (trackID: number) => void
+}) => {
+ return (
+
+ {tracks.map((track, index) => (
+
+ ))}
+
+ )
+}
+
+export default TrackGrid
diff --git a/packages/renderer/src/hooks/useArtist.ts b/packages/renderer/src/hooks/useArtist.ts
index f3075ff..d49ef1e 100644
--- a/packages/renderer/src/hooks/useArtist.ts
+++ b/packages/renderer/src/hooks/useArtist.ts
@@ -1,14 +1,24 @@
import { fetchArtist } from '@/api/artist'
import { ArtistApiNames } from '@/api/artist'
-import type { FetchArtistParams } from '@/api/artist'
+import type { FetchArtistParams, FetchArtistResponse } from '@/api/artist'
-export default function useArtist(params: FetchArtistParams, noCache: boolean) {
+export default function useArtist(
+ params: FetchArtistParams,
+ noCache?: boolean
+) {
return useQuery(
[ArtistApiNames.FETCH_ARTIST, params],
- () => fetchArtist(params, noCache),
+ () => fetchArtist(params, !!noCache),
{
enabled: !!params.id && params.id > 0 && !isNaN(Number(params.id)),
- staleTime: 3600000,
+ staleTime: 5 * 60 * 1000, // 5 mins
+ placeholderData: (): FetchArtistResponse =>
+ window.ipcRenderer.sendSync('getApiCacheSync', {
+ api: 'artists',
+ query: {
+ id: params.id,
+ },
+ }),
}
)
}
diff --git a/packages/renderer/src/pages/Album.tsx b/packages/renderer/src/pages/Album.tsx
index 34eb818..0bc8f12 100644
--- a/packages/renderer/src/pages/Album.tsx
+++ b/packages/renderer/src/pages/Album.tsx
@@ -146,7 +146,7 @@ const Header = ({
Album by{' '}
{album?.artist.name}
diff --git a/packages/renderer/src/pages/Artist.tsx b/packages/renderer/src/pages/Artist.tsx
new file mode 100644
index 0000000..3fe8ded
--- /dev/null
+++ b/packages/renderer/src/pages/Artist.tsx
@@ -0,0 +1,148 @@
+import Button, { Color as ButtonColor } from '@/components/Button'
+import SvgIcon from '@/components/SvgIcon'
+import Cover from '@/components/Cover'
+import useArtist from '@/hooks/useArtist'
+import useArtistAlbums from '@/hooks/useArtistAlbums'
+import { resizeImage } from '@/utils/common'
+import dayjs from 'dayjs'
+import TracksGrid from '@/components/TracksGrid'
+import CoverRow, { Subtitle } from '@/components/CoverRow'
+import Skeleton from '@/components/Skeleton'
+import { Fragment } from 'react'
+
+const Artist = () => {
+ const params = useParams()
+
+ const { data: artist, isLoading } = useArtist({
+ id: Number(params.id) || 0,
+ })
+
+ const { data: albumsRaw, isLoading: isLoadingAlbum } = useArtistAlbums({
+ id: Number(params.id) || 0,
+ limit: 1000,
+ })
+
+ const albums = useMemo(() => {
+ if (!albumsRaw?.hotAlbums) return []
+ return albumsRaw.hotAlbums.filter(
+ album =>
+ album.type === '专辑' &&
+ ['混音版', '精选集', 'Remix'].includes(album.subType) === false &&
+ album.size > 1
+ )
+ }, [albumsRaw?.hotAlbums])
+
+ const singles = useMemo(() => {
+ if (!albumsRaw?.hotAlbums) return []
+ return albumsRaw.hotAlbums.filter(
+ album =>
+ album.type !== '专辑' ||
+ ['混音版', '精选集', 'Remix'].includes(album.subType) ||
+ album.size === 1
+ )
+ }, [albumsRaw?.hotAlbums])
+
+ const latestAlbum = useMemo(() => {
+ if (!albumsRaw || !albumsRaw.hotAlbums) return
+ return albumsRaw.hotAlbums[0]
+ }, [albumsRaw])
+
+ const coverImage = resizeImage(artist?.artist?.img1v1Url || '', 'md')
+
+ return (
+
+
+ {coverImage && (
+
+
+
+
+ )}
+
+
+
+ {/* Header */}
+
+
+
+
+
+
+
+ {artist?.artist.name}
+
+
+
+
+
+ {/* Latest release */}
+
+
+ 最新发行
+
+
+ {isLoadingAlbum ? (
+
+ ) : (
+
+ )}
+
+ {latestAlbum?.name}
+
+
+ {latestAlbum?.type} ·{' '}
+ {dayjs(latestAlbum?.publishTime || 0).year()}
+
+
+
+
+ {/* Popular tracks */}
+
+
+
+ {/* Albums */}
+
+
+ {/* Singles/EP */}
+
+
+ )
+}
+
+export default Artist
diff --git a/packages/renderer/src/pages/Login.tsx b/packages/renderer/src/pages/Login.tsx
index e2d61df..e4f5d71 100644
--- a/packages/renderer/src/pages/Login.tsx
+++ b/packages/renderer/src/pages/Login.tsx
@@ -22,7 +22,9 @@ const EmailInput = ({
}) => {
return (
-
Email
+
+ Email
+
setEmail(e.target.value)}
@@ -96,7 +98,7 @@ const PasswordInput = ({