mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-16 21:28:06 +00:00
feat: updates
This commit is contained in:
parent
d96bd2a547
commit
e3486ab550
23 changed files with 331 additions and 261 deletions
|
|
@ -1,23 +1,41 @@
|
||||||
import Realm from 'realm'
|
import Realm from 'realm'
|
||||||
import type { FetchTracksResponse } from '../renderer/src/api/track'
|
import type { FetchTracksResponse } from '../renderer/src/api/track'
|
||||||
|
import type { FetchAlbumResponse } from '../renderer/src/api/album'
|
||||||
|
|
||||||
enum ModelNames {
|
enum ModelNames {
|
||||||
TRACK = 'Track',
|
TRACK = 'Track',
|
||||||
|
ALBUM = 'Album',
|
||||||
|
ARTIST = 'Artist',
|
||||||
|
PLAYLIST = 'Playlist',
|
||||||
|
}
|
||||||
|
|
||||||
|
const universalProperties = {
|
||||||
|
id: 'int',
|
||||||
|
json: 'string',
|
||||||
|
updateAt: 'int',
|
||||||
}
|
}
|
||||||
|
|
||||||
const TrackSchema = {
|
const TrackSchema = {
|
||||||
name: ModelNames.TRACK,
|
name: ModelNames.TRACK,
|
||||||
properties: {
|
properties: universalProperties,
|
||||||
id: 'int',
|
primaryKey: 'id',
|
||||||
json: 'string',
|
}
|
||||||
updateAt: 'int',
|
|
||||||
},
|
const AlbumSchema = {
|
||||||
|
name: ModelNames.ALBUM,
|
||||||
|
properties: universalProperties,
|
||||||
|
primaryKey: 'id',
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlaylistSchema = {
|
||||||
|
name: ModelNames.PLAYLIST,
|
||||||
|
properties: universalProperties,
|
||||||
primaryKey: 'id',
|
primaryKey: 'id',
|
||||||
}
|
}
|
||||||
|
|
||||||
const realm = new Realm({
|
const realm = new Realm({
|
||||||
path: './dist/db.realm',
|
path: './.tmp/db.realm',
|
||||||
schema: [TrackSchema],
|
schema: [TrackSchema, AlbumSchema, PlaylistSchema],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const database = {
|
export const database = {
|
||||||
|
|
@ -29,7 +47,7 @@ export const database = {
|
||||||
model,
|
model,
|
||||||
{
|
{
|
||||||
id: key,
|
id: key,
|
||||||
updateAt: ~~(Date.now() / 1000),
|
updateAt: Date.now(),
|
||||||
json: JSON.stringify(value),
|
json: JSON.stringify(value),
|
||||||
},
|
},
|
||||||
'modified'
|
'modified'
|
||||||
|
|
@ -40,29 +58,43 @@ export const database = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTracks(data: FetchTracksResponse) {
|
export async function setTracks(data: FetchTracksResponse) {
|
||||||
const tracks = data.songs
|
|
||||||
if (!data.songs) return
|
if (!data.songs) return
|
||||||
const write = async () =>
|
const tracks = data.songs
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
tracks.forEach(track => {
|
tracks.forEach(track => {
|
||||||
database.set(ModelNames.TRACK, track.id, track)
|
database.set(ModelNames.TRACK, track.id, track)
|
||||||
})
|
|
||||||
})
|
})
|
||||||
write()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setCache(api: string, data: any) {
|
export async function setCache(api: string, data: any) {
|
||||||
switch (api) {
|
switch (api) {
|
||||||
case 'song_detail':
|
case 'song_detail': {
|
||||||
setTracks(data)
|
setTracks(data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case 'album': {
|
||||||
|
if (!data.album) return
|
||||||
|
realm.write(() => {
|
||||||
|
database.set(ModelNames.ALBUM, Number(data.album.id), data)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case 'playlist_detail': {
|
||||||
|
if (!data.playlist) return
|
||||||
|
realm.write(() => {
|
||||||
|
database.set(ModelNames.PLAYLIST, Number(data.playlist.id), data)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCache(api: string, query: any) {
|
export function getCache(api: string, query: any) {
|
||||||
switch (api) {
|
switch (api) {
|
||||||
case 'song_detail': {
|
case 'song_detail': {
|
||||||
const ids: string[] = query.ids.split(',')
|
const ids: string[] = query?.ids.split(',')
|
||||||
const idsQuery = ids.map(id => `id = ${id}`).join(' OR ')
|
const idsQuery = ids.map(id => `id = ${id}`).join(' OR ')
|
||||||
const tracksRaw = realm
|
const tracksRaw = realm
|
||||||
.objects(ModelNames.TRACK)
|
.objects(ModelNames.TRACK)
|
||||||
|
|
@ -78,5 +110,23 @@ export function getCache(api: string, query: any) {
|
||||||
privileges: {},
|
privileges: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'album': {
|
||||||
|
if (!query?.id) return
|
||||||
|
const album = realm.objectForPrimaryKey(
|
||||||
|
ModelNames.ALBUM,
|
||||||
|
Number(query?.id)
|
||||||
|
)?.json
|
||||||
|
if (album) return JSON.parse(album)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case 'playlist_detail': {
|
||||||
|
if (!query?.id) return
|
||||||
|
const playlist = realm.objectForPrimaryKey(
|
||||||
|
ModelNames.PLAYLIST,
|
||||||
|
Number(query?.id)
|
||||||
|
)?.json
|
||||||
|
if (playlist) return JSON.parse(playlist)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@ import Main from './components/Main'
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={reactQueryClient}>
|
<QueryClientProvider client={reactQueryClient}>
|
||||||
<div id="layout" className="grid select-none grid-cols-[16rem_auto]">
|
<div id='layout' className='grid select-none grid-cols-[16rem_auto]'>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<Main />
|
<Main />
|
||||||
<Player />
|
<Player />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Toaster position="bottom-center" containerStyle={{ bottom: '5rem' }} />
|
<Toaster position='bottom-center' containerStyle={{ bottom: '5rem' }} />
|
||||||
|
|
||||||
{/* Devtool */}
|
{/* Devtool */}
|
||||||
<ReactQueryDevtools
|
<ReactQueryDevtools
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const ArtistInline = ({
|
||||||
<div className={classNames('flex truncate', className)}>
|
<div className={classNames('flex truncate', className)}>
|
||||||
{artists.map((artist, index) => (
|
{artists.map((artist, index) => (
|
||||||
<span key={artist.id}>
|
<span key={artist.id}>
|
||||||
<span className="hover:underline">{artist.name}</span>
|
<span className='hover:underline'>{artist.name}</span>
|
||||||
{index < artists.length - 1 ? ', ' : ''}
|
{index < artists.length - 1 ? ', ' : ''}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const Cover = ({
|
||||||
const [isError, setIsError] = useState(false)
|
const [isError, setIsError] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onClick={onClick} className="group relative z-0">
|
<div onClick={onClick} className='group relative z-0'>
|
||||||
{/* Neon shadow */}
|
{/* Neon shadow */}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|
@ -27,8 +27,8 @@ const Cover = ({
|
||||||
|
|
||||||
{/* Cover */}
|
{/* Cover */}
|
||||||
{isError ? (
|
{isError ? (
|
||||||
<div className="box-content flex aspect-square h-full w-full items-center justify-center rounded-xl border border-black border-opacity-5 bg-gray-800 text-gray-300">
|
<div className='box-content flex aspect-square h-full w-full items-center justify-center rounded-xl border border-black border-opacity-5 bg-gray-800 text-gray-300'>
|
||||||
<SvgIcon name="music-note" className="h-1/2 w-1/2" />
|
<SvgIcon name='music-note' className='h-1/2 w-1/2' />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
|
|
@ -43,9 +43,9 @@ const Cover = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Play button */}
|
{/* Play button */}
|
||||||
<div className="absolute top-0 hidden h-full w-full place-content-center group-hover:grid">
|
<div className='absolute top-0 hidden h-full w-full place-content-center group-hover:grid'>
|
||||||
<button className="btn-pressed-animation grid h-11 w-11 cursor-default place-content-center rounded-full border border-white border-opacity-[.08] bg-white bg-opacity-[.14] text-white backdrop-blur backdrop-filter transition-all hover:bg-opacity-[.44]">
|
<button className='btn-pressed-animation grid h-11 w-11 cursor-default place-content-center rounded-full border border-white border-opacity-[.08] bg-white bg-opacity-[.14] text-white backdrop-blur backdrop-filter transition-all hover:bg-opacity-[.44]'>
|
||||||
<SvgIcon className="ml-1 h-4 w-4" name="play" />
|
<SvgIcon className='ml-1 h-4 w-4' name='play' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,12 @@ const Title = ({
|
||||||
seeMoreLink: string
|
seeMoreLink: string
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-baseline justify-between">
|
<div className='flex items-baseline justify-between'>
|
||||||
<div className="my-4 text-[28px] font-bold text-black dark:text-white">
|
<div className='my-4 text-[28px] font-bold text-black dark:text-white'>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
{seeMoreLink && (
|
{seeMoreLink && (
|
||||||
<div className="text-13px font-semibold text-gray-600 hover:underline">
|
<div className='text-13px font-semibold text-gray-600 hover:underline'>
|
||||||
See More
|
See More
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -83,6 +83,8 @@ const CoverRow = ({
|
||||||
seeMoreLink,
|
seeMoreLink,
|
||||||
isSkeleton,
|
isSkeleton,
|
||||||
className,
|
className,
|
||||||
|
rows = 2,
|
||||||
|
navigateCallback, // Callback function when click on the cover/title
|
||||||
}: {
|
}: {
|
||||||
title?: string
|
title?: string
|
||||||
albums?: Album[]
|
albums?: Album[]
|
||||||
|
|
@ -92,13 +94,15 @@ const CoverRow = ({
|
||||||
seeMoreLink?: string
|
seeMoreLink?: string
|
||||||
isSkeleton?: boolean
|
isSkeleton?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
rows?: number
|
||||||
|
navigateCallback?: () => void
|
||||||
}) => {
|
}) => {
|
||||||
const renderItems = useMemo(() => {
|
const renderItems = useMemo(() => {
|
||||||
if (isSkeleton) {
|
if (isSkeleton) {
|
||||||
return new Array(10).fill({}) as Array<Album | Playlist | Artist>
|
return new Array(rows * 5).fill({}) as Array<Album | Playlist | Artist>
|
||||||
}
|
}
|
||||||
return albums ?? playlists ?? artists ?? []
|
return albums ?? playlists ?? artists ?? []
|
||||||
}, [albums, artists, isSkeleton, playlists])
|
}, [albums, artists, isSkeleton, playlists, rows])
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const goTo = (id: number) => {
|
const goTo = (id: number) => {
|
||||||
|
|
@ -106,6 +110,7 @@ const CoverRow = ({
|
||||||
if (albums) navigate(`/album/${id}`)
|
if (albums) navigate(`/album/${id}`)
|
||||||
if (playlists) navigate(`/playlist/${id}`)
|
if (playlists) navigate(`/playlist/${id}`)
|
||||||
if (artists) navigate(`/artist/${id}`)
|
if (artists) navigate(`/artist/${id}`)
|
||||||
|
if (navigateCallback) navigateCallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefetch = (id: number) => {
|
const prefetch = (id: number) => {
|
||||||
|
|
@ -129,12 +134,12 @@ const CoverRow = ({
|
||||||
<div
|
<div
|
||||||
key={item.id ?? index}
|
key={item.id ?? index}
|
||||||
onMouseOver={() => prefetch(item.id)}
|
onMouseOver={() => prefetch(item.id)}
|
||||||
className="grid gap-x-[24px] gap-y-7"
|
className='grid gap-x-[24px] gap-y-7'
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{/* Cover */}
|
{/* Cover */}
|
||||||
{isSkeleton ? (
|
{isSkeleton ? (
|
||||||
<Skeleton className="box-content aspect-square w-full rounded-xl border border-black border-opacity-0" />
|
<Skeleton className='box-content aspect-square w-full rounded-xl border border-black border-opacity-0' />
|
||||||
) : (
|
) : (
|
||||||
<Cover
|
<Cover
|
||||||
onClick={() => goTo(item.id)}
|
onClick={() => goTo(item.id)}
|
||||||
|
|
@ -143,30 +148,30 @@ const CoverRow = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Info */}
|
{/* Info */}
|
||||||
<div className="mt-2">
|
<div className='mt-2'>
|
||||||
<div className="font-semibold">
|
<div className='font-semibold'>
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
{isSkeleton ? (
|
{isSkeleton ? (
|
||||||
<div className="flex w-full -translate-y-px flex-col">
|
<div className='flex w-full -translate-y-px flex-col'>
|
||||||
<Skeleton className="w-full leading-tight">
|
<Skeleton className='w-full leading-tight'>
|
||||||
PLACEHOLDER
|
PLACEHOLDER
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Skeleton className="w-1/3 translate-y-px leading-tight">
|
<Skeleton className='w-1/3 translate-y-px leading-tight'>
|
||||||
PLACEHOLDER
|
PLACEHOLDER
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="line-clamp-2 leading-tight ">
|
<span className='line-clamp-2 leading-tight '>
|
||||||
{/* Playlist private icon */}
|
{/* Playlist private icon */}
|
||||||
{(item as Playlist).privacy && (
|
{(item as Playlist).privacy && (
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
name="lock"
|
name='lock'
|
||||||
className="mr-1 mb-1 inline-block h-3 w-3 text-gray-300"
|
className='mr-1 mb-1 inline-block h-3 w-3 text-gray-300'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
onClick={() => goTo(item.id)}
|
onClick={() => goTo(item.id)}
|
||||||
className="decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-200"
|
className='decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-200'
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -176,11 +181,11 @@ const CoverRow = ({
|
||||||
|
|
||||||
{/* Subtitle */}
|
{/* Subtitle */}
|
||||||
{isSkeleton ? (
|
{isSkeleton ? (
|
||||||
<Skeleton className="w-3/5 translate-y-px text-[12px]">
|
<Skeleton className='w-3/5 translate-y-px text-[12px]'>
|
||||||
PLACEHOLDER
|
PLACEHOLDER
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex text-[12px] text-gray-500 dark:text-gray-400">
|
<div className='flex text-[12px] text-gray-500 dark:text-gray-400'>
|
||||||
<span>{getSubtitleText(item, subtitle)}</span>
|
<span>{getSubtitleText(item, subtitle)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,19 @@ import style from './DailyTracksCard.module.scss'
|
||||||
|
|
||||||
const DailyTracksCard = () => {
|
const DailyTracksCard = () => {
|
||||||
return (
|
return (
|
||||||
<div className="relative h-[198px] cursor-pointer overflow-hidden rounded-2xl">
|
<div className='relative h-[198px] cursor-pointer overflow-hidden rounded-2xl'>
|
||||||
{/* Cover */}
|
{/* Cover */}
|
||||||
<img
|
<img
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute top-0 left-0 w-full will-change-transform',
|
'absolute top-0 left-0 w-full will-change-transform',
|
||||||
style.animation
|
style.animation
|
||||||
)}
|
)}
|
||||||
src="https://p2.music.126.net/QxJA2mr4hhb9DZyucIOIQw==/109951165422200291.jpg?param=1024y1024"
|
src='https://p2.music.126.net/QxJA2mr4hhb9DZyucIOIQw==/109951165422200291.jpg?param=1024y1024'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 每日推荐 */}
|
{/* 每日推荐 */}
|
||||||
<div className="absolute flex h-full w-1/2 items-center bg-gradient-to-r from-[#0000004d] to-transparent pl-8">
|
<div className='absolute flex h-full w-1/2 items-center bg-gradient-to-r from-[#0000004d] to-transparent pl-8'>
|
||||||
<div className="grid grid-cols-2 grid-rows-2 gap-2 text-[64px] font-semibold leading-[64px] text-white opacity-[96]">
|
<div className='grid grid-cols-2 grid-rows-2 gap-2 text-[64px] font-semibold leading-[64px] text-white opacity-[96]'>
|
||||||
{Array.from('每日推荐').map(word => (
|
{Array.from('每日推荐').map(word => (
|
||||||
<div key={word}>{word}</div>
|
<div key={word}>{word}</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -23,8 +23,8 @@ const DailyTracksCard = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Play button */}
|
{/* Play button */}
|
||||||
<button className="btn-pressed-animation absolute right-6 bottom-6 grid h-11 w-11 cursor-default place-content-center rounded-full border border-white border-opacity-[.08] bg-white bg-opacity-[.14] text-white backdrop-blur backdrop-filter transition-all hover:bg-opacity-[.44]">
|
<button className='btn-pressed-animation absolute right-6 bottom-6 grid h-11 w-11 cursor-default place-content-center rounded-full border border-white border-opacity-[.08] bg-white bg-opacity-[.14] text-white backdrop-blur backdrop-filter transition-all hover:bg-opacity-[.44]'>
|
||||||
<SvgIcon name="play" className="ml-1 h-4 w-4" />
|
<SvgIcon name='play' className='ml-1 h-4 w-4' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -25,36 +25,36 @@ const FMCard = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative flex h-[198px] overflow-hidden rounded-2xl p-4"
|
className='relative flex h-[198px] overflow-hidden rounded-2xl p-4'
|
||||||
style={{ background }}
|
style={{ background }}
|
||||||
>
|
>
|
||||||
<img className="rounded-lg shadow-2xl" src={coverUrl} />
|
<img className='rounded-lg shadow-2xl' src={coverUrl} />
|
||||||
|
|
||||||
<div className="ml-5 flex w-full flex-col justify-between text-white">
|
<div className='ml-5 flex w-full flex-col justify-between text-white'>
|
||||||
{/* Track info */}
|
{/* Track info */}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xl font-semibold">How Can I Make It OK?</div>
|
<div className='text-xl font-semibold'>How Can I Make It OK?</div>
|
||||||
<div className="opacity-75">Wolf Alice</div>
|
<div className='opacity-75'>Wolf Alice</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{Object.values(ACTION).map(action => (
|
{Object.values(ACTION).map(action => (
|
||||||
<button
|
<button
|
||||||
key={action}
|
key={action}
|
||||||
className="btn-pressed-animation btn-hover-animation mr-1 cursor-default rounded-lg p-2 transition duration-200 after:bg-white/10"
|
className='btn-pressed-animation btn-hover-animation mr-1 cursor-default rounded-lg p-2 transition duration-200 after:bg-white/10'
|
||||||
>
|
>
|
||||||
<SvgIcon name={action} className="h-5 w-5" />
|
<SvgIcon name={action} className='h-5 w-5' />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* FM logo */}
|
{/* FM logo */}
|
||||||
<div className="right-4 bottom-5 flex text-white opacity-20">
|
<div className='right-4 bottom-5 flex text-white opacity-20'>
|
||||||
<SvgIcon name="fm" className="mr-2 h-5 w-5" />
|
<SvgIcon name='fm' className='mr-2 h-5 w-5' />
|
||||||
<span className="font-semibold">私人FM</span>
|
<span className='font-semibold'>私人FM</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import Topbar from '@/components/Topbar'
|
||||||
const Main = () => {
|
const Main = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="mainContainer"
|
id='mainContainer'
|
||||||
className="relative flex h-screen max-h-screen flex-grow flex-col overflow-y-auto bg-white dark:bg-[#1d1d1d]"
|
className='relative flex h-screen max-h-screen flex-grow flex-col overflow-y-auto bg-white dark:bg-[#1d1d1d]'
|
||||||
>
|
>
|
||||||
<Topbar />
|
<Topbar />
|
||||||
<main id="main" className="mb-24 flex-grow px-8">
|
<main id='main' className='mb-24 flex-grow px-8'>
|
||||||
<Router />
|
<Router />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -29,39 +29,39 @@ const PlayingTrack = () => {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{track && (
|
{track && (
|
||||||
<div className="flex items-center gap-3">
|
<div className='flex items-center gap-3'>
|
||||||
{track?.al?.picUrl && (
|
{track?.al?.picUrl && (
|
||||||
<img
|
<img
|
||||||
onClick={toAlbum}
|
onClick={toAlbum}
|
||||||
className="aspect-square h-full rounded-md shadow-md"
|
className='aspect-square h-full rounded-md shadow-md'
|
||||||
src={resizeImage(track?.al?.picUrl ?? '', 'sm')}
|
src={resizeImage(track?.al?.picUrl ?? '', 'sm')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!track?.al?.picUrl && (
|
{!track?.al?.picUrl && (
|
||||||
<div
|
<div
|
||||||
onClick={toAlbum}
|
onClick={toAlbum}
|
||||||
className="flex aspect-square h-full items-center justify-center rounded-md bg-black/[.04] shadow-sm"
|
className='flex aspect-square h-full items-center justify-center rounded-md bg-black/[.04] shadow-sm'
|
||||||
>
|
>
|
||||||
<SvgIcon className="h-6 w-6 text-gray-300" name="music-note" />
|
<SvgIcon className='h-6 w-6 text-gray-300' name='music-note' />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col justify-center leading-tight">
|
<div className='flex flex-col justify-center leading-tight'>
|
||||||
<div
|
<div
|
||||||
onClick={toTrackListSource}
|
onClick={toTrackListSource}
|
||||||
className="line-clamp-1 font-semibold text-black decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-300"
|
className='line-clamp-1 font-semibold text-black decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-300'
|
||||||
>
|
>
|
||||||
{track?.name}
|
{track?.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
<div className='mt-0.5 text-xs text-gray-500 dark:text-gray-400'>
|
||||||
<ArtistInline artists={track?.ar ?? []} />
|
<ArtistInline artists={track?.ar ?? []} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<IconButton onClick={() => toast('Work in progress')}>
|
<IconButton onClick={() => toast('Work in progress')}>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
className="h-4 w-4 text-black dark:text-white"
|
className='h-4 w-4 text-black dark:text-white'
|
||||||
name="heart-outline"
|
name='heart-outline'
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -76,22 +76,22 @@ const MediaControls = () => {
|
||||||
const state = useMemo(() => playerSnapshot.state, [playerSnapshot.state])
|
const state = useMemo(() => playerSnapshot.state, [playerSnapshot.state])
|
||||||
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
|
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center gap-2 text-black dark:text-white">
|
<div className='flex items-center justify-center gap-2 text-black dark:text-white'>
|
||||||
<IconButton onClick={() => track && player.prevTrack()} disabled={!track}>
|
<IconButton onClick={() => track && player.prevTrack()} disabled={!track}>
|
||||||
<SvgIcon className="h-4 w-4" name="previous" />
|
<SvgIcon className='h-4 w-4' name='previous' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => track && player.playOrPause()}
|
onClick={() => track && player.playOrPause()}
|
||||||
disabled={!track}
|
disabled={!track}
|
||||||
className="rounded-2xl"
|
className='rounded-2xl'
|
||||||
>
|
>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
className="h-[1.5rem] w-[1.5rem] "
|
className='h-[1.5rem] w-[1.5rem] '
|
||||||
name={state === PlayerState.PLAYING ? 'pause' : 'play'}
|
name={state === PlayerState.PLAYING ? 'pause' : 'play'}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={() => track && player.nextTrack()} disabled={!track}>
|
<IconButton onClick={() => track && player.nextTrack()} disabled={!track}>
|
||||||
<SvgIcon className="h-4 w-4" name="next" />
|
<SvgIcon className='h-4 w-4' name='next' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -99,21 +99,21 @@ const MediaControls = () => {
|
||||||
|
|
||||||
const Others = () => {
|
const Others = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end gap-2 pr-2 text-black dark:text-white">
|
<div className='flex items-center justify-end gap-2 pr-2 text-black dark:text-white'>
|
||||||
<IconButton onClick={() => toast('Work in progress')}>
|
<IconButton onClick={() => toast('Work in progress')}>
|
||||||
<SvgIcon className="h-4 w-4" name="playlist" />
|
<SvgIcon className='h-4 w-4' name='playlist' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={() => toast('Work in progress')}>
|
<IconButton onClick={() => toast('Work in progress')}>
|
||||||
<SvgIcon className="h-4 w-4" name="repeat" />
|
<SvgIcon className='h-4 w-4' name='repeat' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={() => toast('Work in progress')}>
|
<IconButton onClick={() => toast('Work in progress')}>
|
||||||
<SvgIcon className="h-4 w-4" name="shuffle" />
|
<SvgIcon className='h-4 w-4' name='shuffle' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={() => toast('Work in progress')}>
|
<IconButton onClick={() => toast('Work in progress')}>
|
||||||
<SvgIcon className="h-4 w-4" name="volume" />
|
<SvgIcon className='h-4 w-4' name='volume' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={() => toast('Work in progress')}>
|
<IconButton onClick={() => toast('Work in progress')}>
|
||||||
<SvgIcon className="h-4 w-4" name="chevron-up" />
|
<SvgIcon className='h-4 w-4' name='chevron-up' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -128,7 +128,7 @@ const Progress = () => {
|
||||||
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
|
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute w-screen">
|
<div className='absolute w-screen'>
|
||||||
{track && (
|
{track && (
|
||||||
<Slider
|
<Slider
|
||||||
min={0}
|
min={0}
|
||||||
|
|
@ -141,7 +141,7 @@ const Progress = () => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!track && (
|
{!track && (
|
||||||
<div className="absolute h-[2px] w-full bg-gray-500 bg-opacity-10"></div>
|
<div className='absolute h-[2px] w-full bg-gray-500 bg-opacity-10'></div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -149,7 +149,7 @@ const Progress = () => {
|
||||||
|
|
||||||
const Player = () => {
|
const Player = () => {
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-0 left-0 right-0 grid h-16 grid-cols-3 grid-rows-1 bg-white bg-opacity-[.86] py-2.5 px-5 backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]">
|
<div className='fixed bottom-0 left-0 right-0 grid h-16 grid-cols-3 grid-rows-1 bg-white bg-opacity-[.86] py-2.5 px-5 backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]'>
|
||||||
<Progress />
|
<Progress />
|
||||||
|
|
||||||
<PlayingTrack />
|
<PlayingTrack />
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import SvgIcon from '@/components/SvgIcon'
|
||||||
import { prefetchPlaylist } from '@/hooks/usePlaylist'
|
import { prefetchPlaylist } from '@/hooks/usePlaylist'
|
||||||
import useUser from '@/hooks/useUser'
|
import useUser from '@/hooks/useUser'
|
||||||
import useUserPlaylists from '@/hooks/useUserPlaylists'
|
import useUserPlaylists from '@/hooks/useUserPlaylists'
|
||||||
|
import { scrollToTop } from '@/utils/common'
|
||||||
|
|
||||||
interface Tab {
|
interface Tab {
|
||||||
name: string
|
name: string
|
||||||
|
|
@ -34,9 +35,10 @@ const primaryTabs: PrimaryTab[] = [
|
||||||
const PrimaryTabs = () => {
|
const PrimaryTabs = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="app-region-drag h-14"></div>
|
<div className='app-region-drag h-14'></div>
|
||||||
{primaryTabs.map(tab => (
|
{primaryTabs.map(tab => (
|
||||||
<NavLink
|
<NavLink
|
||||||
|
onClick={() => scrollToTop()}
|
||||||
key={tab.route}
|
key={tab.route}
|
||||||
to={tab.route}
|
to={tab.route}
|
||||||
className={({ isActive }: { isActive: boolean }) =>
|
className={({ isActive }: { isActive: boolean }) =>
|
||||||
|
|
@ -47,12 +49,12 @@ const PrimaryTabs = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SvgIcon className="mr-3 h-6 w-6" name={tab.icon} />
|
<SvgIcon className='mr-3 h-6 w-6' name={tab.icon} />
|
||||||
<span className="font-semibold">{tab.name}</span>
|
<span className='font-semibold'>{tab.name}</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="mx-5 my-2 h-px bg-black opacity-5 dark:bg-white dark:opacity-10"></div>
|
<div className='mx-5 my-2 h-px bg-black opacity-5 dark:bg-white dark:opacity-10'></div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -65,10 +67,11 @@ const Playlists = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-16 overflow-auto pb-2">
|
<div className='mb-16 overflow-auto pb-2'>
|
||||||
{playlists?.playlist?.map(playlist => (
|
{playlists?.playlist?.map(playlist => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={playlist.id}
|
key={playlist.id}
|
||||||
|
onClick={() => scrollToTop()}
|
||||||
to={`/playlist/${playlist.id}`}
|
to={`/playlist/${playlist.id}`}
|
||||||
onMouseOver={() => prefetchPlaylist({ id: playlist.id })}
|
onMouseOver={() => prefetchPlaylist({ id: playlist.id })}
|
||||||
className={({ isActive }: { isActive: boolean }) =>
|
className={({ isActive }: { isActive: boolean }) =>
|
||||||
|
|
@ -78,7 +81,7 @@ const Playlists = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="line-clamp-1">{playlist.name}</span>
|
<span className='line-clamp-1'>{playlist.name}</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -88,8 +91,8 @@ const Playlists = () => {
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="sidebar"
|
id='sidebar'
|
||||||
className="grid h-screen max-w-sm grid-rows-[12rem_auto] border-r border-gray-300/10 bg-gray-50 bg-opacity-[.85] dark:border-gray-500/10 dark:bg-gray-900 dark:bg-opacity-80"
|
className='grid h-screen max-w-sm grid-rows-[12rem_auto] border-r border-gray-300/10 bg-gray-50 bg-opacity-[.85] dark:border-gray-500/10 dark:bg-gray-900 dark:bg-opacity-80'
|
||||||
>
|
>
|
||||||
<PrimaryTabs />
|
<PrimaryTabs />
|
||||||
<Playlists />
|
<Playlists />
|
||||||
|
|
|
||||||
|
|
@ -122,18 +122,18 @@ const Slider = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="group flex h-2 -translate-y-[3px] items-center"
|
className='group flex h-2 -translate-y-[3px] items-center'
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
{/* Track */}
|
{/* Track */}
|
||||||
<div className="absolute h-[2px] w-full bg-gray-500 bg-opacity-10"></div>
|
<div className='absolute h-[2px] w-full bg-gray-500 bg-opacity-10'></div>
|
||||||
|
|
||||||
{/* Passed track */}
|
{/* Passed track */}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute h-[2px] group-hover:bg-brand-500',
|
'absolute h-[2px] group-hover:bg-brand-500',
|
||||||
isDragging ? 'bg-brand-500' : 'bg-gray-500 dark:bg-gray-400'
|
isDragging ? 'bg-brand-500' : 'bg-gray-300 dark:bg-gray-400'
|
||||||
)}
|
)}
|
||||||
style={usedTrackStyle}
|
style={usedTrackStyle}
|
||||||
></div>
|
></div>
|
||||||
|
|
@ -148,7 +148,7 @@ const Slider = ({
|
||||||
onClick={e => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
>
|
>
|
||||||
<div className="absolute h-2 w-2 rounded-full bg-brand-500"></div>
|
<div className='absolute h-2 w-2 rounded-full bg-brand-500'></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ const Slider = () => {
|
||||||
onMouseEnter={() => setIsHover(true)}
|
onMouseEnter={() => setIsHover(true)}
|
||||||
onMouseLeave={() => setIsHover(false)}
|
onMouseLeave={() => setIsHover(false)}
|
||||||
>
|
>
|
||||||
<div className="absolute h-[2px] w-full bg-gray-500 bg-opacity-10"></div>
|
<div className='absolute h-[2px] w-full bg-gray-500 bg-opacity-10'></div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute h-[2px]',
|
'absolute h-[2px]',
|
||||||
|
|
@ -43,16 +43,16 @@ const Slider = () => {
|
||||||
)}
|
)}
|
||||||
style={thumbStyle}
|
style={thumbStyle}
|
||||||
>
|
>
|
||||||
<div className="absolute h-2 w-2 rounded-full bg-brand-500"></div>
|
<div className='absolute h-2 w-2 rounded-full bg-brand-500'></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="range"
|
type='range'
|
||||||
min="0"
|
min='0'
|
||||||
max="100"
|
max='100'
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => setValue(Number(e.target.value))}
|
onChange={e => setValue(Number(e.target.value))}
|
||||||
className="absolute h-[2px] w-full appearance-none opacity-0"
|
className='absolute h-[2px] w-full appearance-none opacity-0'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
const SvgIcon = ({ name, className }: { name: string; className?: string }) => {
|
const SvgIcon = ({ name, className }: { name: string; className?: string }) => {
|
||||||
const symbolId = `#icon-${name}`
|
const symbolId = `#icon-${name}`
|
||||||
return (
|
return (
|
||||||
<svg aria-hidden="true" className={className}>
|
<svg aria-hidden='true' className={className}>
|
||||||
<use href={symbolId} fill="currentColor" />
|
<use href={symbolId} fill='currentColor' />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@ const NavigationButtons = () => {
|
||||||
if (action === ACTION.FORWARD) navigate(1)
|
if (action === ACTION.FORWARD) navigate(1)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-1">
|
<div className='flex gap-1'>
|
||||||
{[ACTION.BACK, ACTION.FORWARD].map(action => (
|
{[ACTION.BACK, ACTION.FORWARD].map(action => (
|
||||||
<div
|
<div
|
||||||
onClick={() => handleNavigate(action)}
|
onClick={() => handleNavigate(action)}
|
||||||
key={action}
|
key={action}
|
||||||
className="app-region-no-drag btn-hover-animation rounded-lg p-3 text-gray-500 transition duration-300 after:rounded-full after:bg-black/[.06] hover:text-gray-900 dark:text-gray-300 dark:after:bg-white/10 dark:hover:text-gray-200"
|
className='app-region-no-drag btn-hover-animation rounded-lg p-3 text-gray-500 transition duration-300 after:rounded-full after:bg-black/[.06] hover:text-gray-900 dark:text-gray-300 dark:after:bg-white/10 dark:hover:text-gray-200'
|
||||||
>
|
>
|
||||||
<SvgIcon className="h-4 w-4" name={action} />
|
<SvgIcon className='h-4 w-4' name={action} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -30,15 +30,15 @@ const NavigationButtons = () => {
|
||||||
|
|
||||||
const SearchBox = () => {
|
const SearchBox = () => {
|
||||||
return (
|
return (
|
||||||
<div className="app-region-no-drag group flex w-[16rem] cursor-text items-center rounded-full bg-gray-500 bg-opacity-5 px-3 transition duration-300 hover:bg-opacity-10 dark:bg-gray-300 dark:bg-opacity-5">
|
<div className='app-region-no-drag group flex w-[16rem] cursor-text items-center rounded-full bg-gray-500 bg-opacity-5 px-3 transition duration-300 hover:bg-opacity-10 dark:bg-gray-300 dark:bg-opacity-5'>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
className="mr-2 h-4 w-4 text-gray-500 transition duration-300 group-hover:text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-200"
|
className='mr-2 h-4 w-4 text-gray-500 transition duration-300 group-hover:text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-200'
|
||||||
name="search"
|
name='search'
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type='text'
|
||||||
className="w-full bg-transparent placeholder:text-gray-500 dark:text-white dark:placeholder:text-gray-400"
|
className='w-full bg-transparent placeholder:text-gray-500 dark:text-white dark:placeholder:text-gray-400'
|
||||||
placeholder="Search"
|
placeholder='Search'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -46,8 +46,8 @@ const SearchBox = () => {
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
return (
|
return (
|
||||||
<div className="app-region-no-drag btn-hover-animation rounded-lg p-2.5 text-gray-500 transition duration-300 after:rounded-full after:bg-black/[.06] hover:text-gray-900 dark:text-gray-300 dark:after:bg-white/10 dark:hover:text-gray-200">
|
<div className='app-region-no-drag btn-hover-animation rounded-lg p-2.5 text-gray-500 transition duration-300 after:rounded-full after:bg-black/[.06] hover:text-gray-900 dark:text-gray-300 dark:after:bg-white/10 dark:hover:text-gray-200'>
|
||||||
<SvgIcon className="h-5 w-5" name="settings" />
|
<SvgIcon className='h-5 w-5' name='settings' />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +59,7 @@ const Avatar = () => {
|
||||||
<img
|
<img
|
||||||
src={user?.profile?.avatarUrl}
|
src={user?.profile?.avatarUrl}
|
||||||
onClick={() => navigate('/login')}
|
onClick={() => navigate('/login')}
|
||||||
className="app-region-no-drag h-9 w-9 rounded-full bg-gray-100 dark:bg-gray-700"
|
className='app-region-no-drag h-9 w-9 rounded-full bg-gray-100 dark:bg-gray-700'
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -83,12 +83,12 @@ const Topbar = () => {
|
||||||
'bg-white bg-opacity-[.86] backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]'
|
'bg-white bg-opacity-[.86] backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex gap-2">
|
<div className='flex gap-2'>
|
||||||
<NavigationButtons />
|
<NavigationButtons />
|
||||||
<SearchBox />
|
<SearchBox />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className='flex items-center gap-3'>
|
||||||
<Settings />
|
<Settings />
|
||||||
<Avatar />
|
<Avatar />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ const PlayOrPauseButtonInTrack = memo(
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
className="h-3.5 w-3.5 text-brand-500"
|
className='h-3.5 w-3.5 text-brand-500'
|
||||||
name={isPlaying && isHighlight ? 'pause' : 'play'}
|
name={isPlaying && isHighlight ? 'pause' : 'play'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -74,10 +74,10 @@ const Track = memo(
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Track name and number */}
|
{/* Track name and number */}
|
||||||
<div className="col-span-6 grid grid-cols-[2rem_auto] pr-8">
|
<div className='col-span-6 grid grid-cols-[2rem_auto] pr-8'>
|
||||||
{/* Track number */}
|
{/* Track number */}
|
||||||
{isSkeleton ? (
|
{isSkeleton ? (
|
||||||
<Skeleton className="h-6.5 w-6.5 -translate-x-1"></Skeleton>
|
<Skeleton className='h-6.5 w-6.5 -translate-x-1'></Skeleton>
|
||||||
) : (
|
) : (
|
||||||
!isHighlight && (
|
!isHighlight && (
|
||||||
<div
|
<div
|
||||||
|
|
@ -101,9 +101,9 @@ const Track = memo(
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Track name */}
|
{/* Track name */}
|
||||||
<div className="flex">
|
<div className='flex'>
|
||||||
{isSkeleton ? (
|
{isSkeleton ? (
|
||||||
<Skeleton className="text-lg">
|
<Skeleton className='text-lg'>
|
||||||
PLACEHOLDER123456789012345
|
PLACEHOLDER123456789012345
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -120,7 +120,7 @@ const Track = memo(
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Artists */}
|
{/* Artists */}
|
||||||
<div className="col-span-4 flex items-center">
|
<div className='col-span-4 flex items-center'>
|
||||||
{isSkeleton ? (
|
{isSkeleton ? (
|
||||||
<Skeleton>PLACEHOLDER1234</Skeleton>
|
<Skeleton>PLACEHOLDER1234</Skeleton>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -136,7 +136,7 @@ const Track = memo(
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions & Track duration */}
|
{/* Actions & Track duration */}
|
||||||
<div className="col-span-2 flex items-center justify-end">
|
<div className='col-span-2 flex items-center justify-end'>
|
||||||
{/* Like button */}
|
{/* Like button */}
|
||||||
{!isSkeleton && (
|
{!isSkeleton && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -150,7 +150,7 @@ const Track = memo(
|
||||||
>
|
>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
name={isLiked ? 'heart' : 'heart-outline'}
|
name={isLiked ? 'heart' : 'heart-outline'}
|
||||||
className="h-4 w-4 "
|
className='h-4 w-4 '
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -209,15 +209,15 @@ const TracksAlbum = ({
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid w-full">
|
<div className='grid w-full'>
|
||||||
{/* Tracks table header */}
|
{/* Tracks table header */}
|
||||||
<div className="mx-4 mt-10 mb-2 grid grid-cols-12 border-b border-gray-100 py-2.5 text-sm text-gray-400 dark:border-gray-800 dark:text-gray-500">
|
<div className='mx-4 mt-10 mb-2 grid grid-cols-12 border-b border-gray-100 py-2.5 text-sm text-gray-400 dark:border-gray-800 dark:text-gray-500'>
|
||||||
<div className="col-span-6 grid grid-cols-[2rem_auto]">
|
<div className='col-span-6 grid grid-cols-[2rem_auto]'>
|
||||||
<div>#</div>
|
<div>#</div>
|
||||||
<div>TITLE</div>
|
<div>TITLE</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-4">ARTIST</div>
|
<div className='col-span-4'>ARTIST</div>
|
||||||
<div className="col-span-2 justify-self-end">TIME</div>
|
<div className='col-span-2 justify-self-end'>TIME</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tracks */}
|
{/* Tracks */}
|
||||||
|
|
|
||||||
|
|
@ -16,38 +16,38 @@ const TrackListGrid = ({
|
||||||
'grid-cols-1 py-1.5 px-2'
|
'grid-cols-1 py-1.5 px-2'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-[3rem_auto] items-center">
|
<div className='grid grid-cols-[3rem_auto] items-center'>
|
||||||
{/* Cover */}
|
{/* Cover */}
|
||||||
<div>
|
<div>
|
||||||
{!isSkeleton && (
|
{!isSkeleton && (
|
||||||
<img
|
<img
|
||||||
src={resizeImage(track.al.picUrl, 'xs')}
|
src={resizeImage(track.al.picUrl, 'xs')}
|
||||||
className="box-content h-9 w-9 rounded-md border border-black border-opacity-[.03]"
|
className='box-content h-9 w-9 rounded-md border border-black border-opacity-[.03]'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isSkeleton && (
|
{isSkeleton && (
|
||||||
<Skeleton className="mr-4 h-9 w-9 rounded-md border border-gray-100" />
|
<Skeleton className='mr-4 h-9 w-9 rounded-md border border-gray-100' />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Track name & Artists */}
|
{/* Track name & Artists */}
|
||||||
<div className="flex flex-col justify-center">
|
<div className='flex flex-col justify-center'>
|
||||||
{!isSkeleton && (
|
{!isSkeleton && (
|
||||||
<div
|
<div
|
||||||
v-if="!isSkeleton"
|
v-if='!isSkeleton'
|
||||||
className="line-clamp-1 break-all text-base font-semibold"
|
className='line-clamp-1 break-all text-base font-semibold'
|
||||||
>
|
>
|
||||||
{track.name}
|
{track.name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isSkeleton && (
|
{isSkeleton && (
|
||||||
<Skeleton className="text-base">PLACEHOLDER12345</Skeleton>
|
<Skeleton className='text-base'>PLACEHOLDER12345</Skeleton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-xs">
|
<div className='text-xs'>
|
||||||
{!isSkeleton && <ArtistInline artists={track.ar} />}
|
{!isSkeleton && <ArtistInline artists={track.ar} />}
|
||||||
{isSkeleton && (
|
{isSkeleton && (
|
||||||
<Skeleton className="w-2/3 translate-y-px">PLACE</Skeleton>
|
<Skeleton className='w-2/3 translate-y-px'>PLACE</Skeleton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -33,32 +33,32 @@ const Track = memo(
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Track info */}
|
{/* Track info */}
|
||||||
<div className="col-span-6 grid grid-cols-[4.2rem_auto] pr-8">
|
<div className='col-span-6 grid grid-cols-[4.2rem_auto] pr-8'>
|
||||||
{/* Cover */}
|
{/* Cover */}
|
||||||
<div>
|
<div>
|
||||||
{isSkeleton ? (
|
{isSkeleton ? (
|
||||||
<Skeleton className="mr-4 h-12 w-12 rounded-md border border-gray-100 dark:border-gray-800" />
|
<Skeleton className='mr-4 h-12 w-12 rounded-md border border-gray-100 dark:border-gray-800' />
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
src={resizeImage(track.al.picUrl, 'xs')}
|
src={resizeImage(track.al.picUrl, 'xs')}
|
||||||
className="box-content h-12 w-12 rounded-md border border-black border-opacity-[.03]"
|
className='box-content h-12 w-12 rounded-md border border-black border-opacity-[.03]'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Track name & Artists */}
|
{/* Track name & Artists */}
|
||||||
<div className="flex flex-col justify-center">
|
<div className='flex flex-col justify-center'>
|
||||||
{isSkeleton ? (
|
{isSkeleton ? (
|
||||||
<Skeleton className="text-lg">PLACEHOLDER12345</Skeleton>
|
<Skeleton className='text-lg'>PLACEHOLDER12345</Skeleton>
|
||||||
) : (
|
) : (
|
||||||
<div className="line-clamp-1 break-all text-lg font-semibold dark:text-white">
|
<div className='line-clamp-1 break-all text-lg font-semibold dark:text-white'>
|
||||||
{track.name}
|
{track.name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
<div className='text-sm text-gray-600 dark:text-gray-400'>
|
||||||
{isSkeleton ? (
|
{isSkeleton ? (
|
||||||
<Skeleton className="w-2/3 translate-y-px">PLACE</Skeleton>
|
<Skeleton className='w-2/3 translate-y-px'>PLACE</Skeleton>
|
||||||
) : (
|
) : (
|
||||||
<ArtistInline artists={track.ar} />
|
<ArtistInline artists={track.ar} />
|
||||||
)}
|
)}
|
||||||
|
|
@ -67,7 +67,7 @@ const Track = memo(
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Album name */}
|
{/* Album name */}
|
||||||
<div className="col-span-4 flex items-center text-gray-600 dark:text-gray-400">
|
<div className='col-span-4 flex items-center text-gray-600 dark:text-gray-400'>
|
||||||
{isSkeleton ? (
|
{isSkeleton ? (
|
||||||
<Skeleton>PLACEHOLDER1234567890</Skeleton>
|
<Skeleton>PLACEHOLDER1234567890</Skeleton>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -75,17 +75,17 @@ const Track = memo(
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/album/${track.al.id}`}
|
to={`/album/${track.al.id}`}
|
||||||
onMouseOver={() => prefetchAlbum({ id: track.al.id })}
|
onMouseOver={() => prefetchAlbum({ id: track.al.id })}
|
||||||
className="hover:underline"
|
className='hover:underline'
|
||||||
>
|
>
|
||||||
{track.al.name}
|
{track.al.name}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<span className="flex-grow"></span>
|
<span className='flex-grow'></span>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions & Track duration */}
|
{/* Actions & Track duration */}
|
||||||
<div className="col-span-2 flex items-center justify-end">
|
<div className='col-span-2 flex items-center justify-end'>
|
||||||
{/* Like button */}
|
{/* Like button */}
|
||||||
{!isSkeleton && (
|
{!isSkeleton && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -98,7 +98,7 @@ const Track = memo(
|
||||||
>
|
>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
name={isLiked ? 'heart' : 'heart-outline'}
|
name={isLiked ? 'heart' : 'heart-outline'}
|
||||||
className="h-4 w-4 "
|
className='h-4 w-4 '
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -107,7 +107,7 @@ const Track = memo(
|
||||||
{isSkeleton ? (
|
{isSkeleton ? (
|
||||||
<Skeleton>0:00</Skeleton>
|
<Skeleton>0:00</Skeleton>
|
||||||
) : (
|
) : (
|
||||||
<div className="min-w-[2.5rem] text-right text-gray-600 dark:text-gray-400">
|
<div className='min-w-[2.5rem] text-right text-gray-600 dark:text-gray-400'>
|
||||||
{formatDuration(track.dt, 'en', 'hh:mm:ss')}
|
{formatDuration(track.dt, 'en', 'hh:mm:ss')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -146,16 +146,16 @@ const TracksList = memo(
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{/* Tracks table header */}
|
{/* Tracks table header */}
|
||||||
<div className="ml-2 mr-4 mt-10 mb-2 grid grid-cols-12 border-b border-gray-100 py-2.5 text-sm text-gray-400 dark:border-gray-800 dark:text-gray-500">
|
<div className='ml-2 mr-4 mt-10 mb-2 grid grid-cols-12 border-b border-gray-100 py-2.5 text-sm text-gray-400 dark:border-gray-800 dark:text-gray-500'>
|
||||||
<div className="col-span-6 grid grid-cols-[4.2rem_auto]">
|
<div className='col-span-6 grid grid-cols-[4.2rem_auto]'>
|
||||||
<div></div>
|
<div></div>
|
||||||
<div>TITLE</div>
|
<div>TITLE</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-4">ALBUM</div>
|
<div className='col-span-4'>ALBUM</div>
|
||||||
<div className="col-span-2 justify-self-end">TIME</div>
|
<div className='col-span-2 justify-self-end'>TIME</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid w-full gap-1">
|
<div className='grid w-full gap-1'>
|
||||||
{/* Tracks */}
|
{/* Tracks */}
|
||||||
{!isSkeleton &&
|
{!isSkeleton &&
|
||||||
tracks.map(track => (
|
tracks.map(track => (
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,12 @@ import useAlbum from '@/hooks/useAlbum'
|
||||||
import useArtistAlbums from '@/hooks/useArtistAlbums'
|
import useArtistAlbums from '@/hooks/useArtistAlbums'
|
||||||
import { player } from '@/store'
|
import { player } from '@/store'
|
||||||
import { State as PlayerState } from '@/utils/player'
|
import { State as PlayerState } from '@/utils/player'
|
||||||
import { formatDate, formatDuration, resizeImage } from '@/utils/common'
|
import {
|
||||||
|
formatDate,
|
||||||
|
formatDuration,
|
||||||
|
resizeImage,
|
||||||
|
scrollToTop,
|
||||||
|
} from '@/utils/common'
|
||||||
|
|
||||||
const PlayButton = ({
|
const PlayButton = ({
|
||||||
album,
|
album,
|
||||||
|
|
@ -49,7 +54,7 @@ const PlayButton = ({
|
||||||
<Button onClick={wrappedHandlePlay} isSkelton={isLoading}>
|
<Button onClick={wrappedHandlePlay} isSkelton={isLoading}>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
name={isPlaying && isThisAlbumPlaying ? 'pause' : 'play'}
|
name={isPlaying && isThisAlbumPlaying ? 'pause' : 'play'}
|
||||||
className="mr-2 h-4 w-4"
|
className='mr-2 h-4 w-4'
|
||||||
/>
|
/>
|
||||||
{isPlaying && isThisAlbumPlaying ? '暂停' : '播放'}
|
{isPlaying && isThisAlbumPlaying ? '暂停' : '播放'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -77,29 +82,29 @@ const Header = ({
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{/* Header background */}
|
{/* Header background */}
|
||||||
<div className="absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden">
|
<div className='absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden'>
|
||||||
{coverUrl && !isCoverError && (
|
{coverUrl && !isCoverError && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<img
|
<img
|
||||||
src={coverUrl}
|
src={coverUrl}
|
||||||
className="absolute -top-full w-full blur-[100px]"
|
className='absolute -top-full w-full blur-[100px]'
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={coverUrl}
|
src={coverUrl}
|
||||||
className="absolute -top-full w-full blur-[100px]"
|
className='absolute -top-full w-full blur-[100px]'
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
<div className="absolute top-0 h-full w-full bg-gradient-to-b from-white/[.84] to-white dark:from-black/[.5] dark:to-[#1d1d1d]"></div>
|
<div className='absolute top-0 h-full w-full bg-gradient-to-b from-white/[.84] to-white dark:from-black/[.5] dark:to-[#1d1d1d]'></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-[17rem_auto] items-center gap-9">
|
<div className='grid grid-cols-[17rem_auto] items-center gap-9'>
|
||||||
{/* Cover */}
|
{/* Cover */}
|
||||||
<div className="relative z-0 aspect-square self-start">
|
<div className='relative z-0 aspect-square self-start'>
|
||||||
{/* Neon shadow */}
|
{/* Neon shadow */}
|
||||||
{!isLoading && coverUrl && !isCoverError && (
|
{!isLoading && coverUrl && !isCoverError && (
|
||||||
<div
|
<div
|
||||||
className="absolute top-3.5 z-[-1] h-full w-full scale-x-[.92] scale-y-[.96] rounded-2xl bg-cover opacity-40 blur-lg filter"
|
className='absolute top-3.5 z-[-1] h-full w-full scale-x-[.92] scale-y-[.96] rounded-2xl bg-cover opacity-40 blur-lg filter'
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url("${coverUrl}")`,
|
backgroundImage: `url("${coverUrl}")`,
|
||||||
}}
|
}}
|
||||||
|
|
@ -108,41 +113,41 @@ const Header = ({
|
||||||
|
|
||||||
{!isLoading && isCoverError ? (
|
{!isLoading && isCoverError ? (
|
||||||
// Fallback cover
|
// Fallback cover
|
||||||
<div className="flex h-full w-full items-center justify-center rounded-2xl border border-black border-opacity-5 bg-gray-100 text-gray-300">
|
<div className='flex h-full w-full items-center justify-center rounded-2xl border border-black border-opacity-5 bg-gray-100 text-gray-300'>
|
||||||
<SvgIcon name="music-note" className="h-1/2 w-1/2" />
|
<SvgIcon name='music-note' className='h-1/2 w-1/2' />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
coverUrl && (
|
coverUrl && (
|
||||||
<img
|
<img
|
||||||
src={coverUrl}
|
src={coverUrl}
|
||||||
className="rounded-2xl border border-b-0 border-black border-opacity-5 dark:border-white dark:border-opacity-5"
|
className='rounded-2xl border border-b-0 border-black border-opacity-5 dark:border-white dark:border-opacity-5'
|
||||||
onError={() => setCoverError(true)}
|
onError={() => setCoverError(true)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{isLoading && <Skeleton className="h-full w-full rounded-2xl" />}
|
{isLoading && <Skeleton className='h-full w-full rounded-2xl' />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info */}
|
{/* Info */}
|
||||||
<div className="z-10 flex h-full flex-col justify-between">
|
<div className='z-10 flex h-full flex-col justify-between'>
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Skeleton className="w-3/4 text-6xl">PLACEHOLDER</Skeleton>
|
<Skeleton className='w-3/4 text-6xl'>PLACEHOLDER</Skeleton>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-6xl font-bold dark:text-white">
|
<div className='text-6xl font-bold dark:text-white'>
|
||||||
{album?.name}
|
{album?.name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Artist */}
|
{/* Artist */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Skeleton className="mt-5 w-64 text-lg">PLACEHOLDER</Skeleton>
|
<Skeleton className='mt-5 w-64 text-lg'>PLACEHOLDER</Skeleton>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-5 text-lg font-medium text-gray-800 dark:text-gray-300">
|
<div className='mt-5 text-lg font-medium text-gray-800 dark:text-gray-300'>
|
||||||
Album by{' '}
|
Album by{' '}
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/artist/${album?.artist.name}`}
|
to={`/artist/${album?.artist.name}`}
|
||||||
className="cursor-default font-semibold hover:underline"
|
className='cursor-default font-semibold hover:underline'
|
||||||
>
|
>
|
||||||
{album?.artist.name}
|
{album?.artist.name}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
@ -151,11 +156,11 @@ const Header = ({
|
||||||
|
|
||||||
{/* Release date & track count & album duration */}
|
{/* Release date & track count & album duration */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Skeleton className="w-72 translate-y-px text-sm">
|
<Skeleton className='w-72 translate-y-px text-sm'>
|
||||||
PLACEHOLDER
|
PLACEHOLDER
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm font-thin text-gray-500 dark:text-gray-400">
|
<div className='text-sm font-thin text-gray-500 dark:text-gray-400'>
|
||||||
{dayjs(album?.publishTime || 0).year()} · {album?.size} Songs,{' '}
|
{dayjs(album?.publishTime || 0).year()} · {album?.size} Songs,{' '}
|
||||||
{albumDuration}
|
{albumDuration}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -163,17 +168,17 @@ const Header = ({
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Skeleton className="mt-5 min-h-[2.5rem] w-1/2 text-sm">
|
<Skeleton className='mt-5 min-h-[2.5rem] w-1/2 text-sm'>
|
||||||
PLACEHOLDER
|
PLACEHOLDER
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
) : (
|
) : (
|
||||||
<div className="line-clamp-2 mt-5 min-h-[2.5rem] text-sm text-gray-500 dark:text-gray-400">
|
<div className='line-clamp-2 mt-5 min-h-[2.5rem] text-sm text-gray-500 dark:text-gray-400'>
|
||||||
{album?.description}
|
{album?.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<div className="mt-5 flex gap-4">
|
<div className='mt-5 flex gap-4'>
|
||||||
<PlayButton {...{ album, handlePlay, isLoading }} />
|
<PlayButton {...{ album, handlePlay, isLoading }} />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -181,7 +186,7 @@ const Header = ({
|
||||||
isSkelton={isLoading}
|
isSkelton={isLoading}
|
||||||
onClick={() => toast('Work in progress')}
|
onClick={() => toast('Work in progress')}
|
||||||
>
|
>
|
||||||
<SvgIcon name="heart" className="h-4 w-4" />
|
<SvgIcon name='heart' className='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -190,7 +195,7 @@ const Header = ({
|
||||||
isSkelton={isLoading}
|
isSkelton={isLoading}
|
||||||
onClick={() => toast('Work in progress')}
|
onClick={() => toast('Work in progress')}
|
||||||
>
|
>
|
||||||
<SvgIcon name="more" className="h-4 w-4" />
|
<SvgIcon name='more' className='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -208,7 +213,6 @@ const MoreAlbum = ({ album }: { album: Album | undefined }) => {
|
||||||
|
|
||||||
const filteredAlbums = useMemo((): Album[] => {
|
const filteredAlbums = useMemo((): Album[] => {
|
||||||
if (!albums) return []
|
if (!albums) return []
|
||||||
const albumID = album?.id
|
|
||||||
const allReleases = albums?.hotAlbums || []
|
const allReleases = albums?.hotAlbums || []
|
||||||
const filteredAlbums = allReleases.filter(
|
const filteredAlbums = allReleases.filter(
|
||||||
album =>
|
album =>
|
||||||
|
|
@ -218,19 +222,18 @@ const MoreAlbum = ({ album }: { album: Album | undefined }) => {
|
||||||
|
|
||||||
const qualifiedAlbums = [...filteredAlbums, ...singles]
|
const qualifiedAlbums = [...filteredAlbums, ...singles]
|
||||||
|
|
||||||
|
const formatName = (name: string) =>
|
||||||
|
name.toLowerCase().replace(/(\s|deluxe|edition|\(|\))/g, '')
|
||||||
|
|
||||||
const uniqueAlbums: Album[] = []
|
const uniqueAlbums: Album[] = []
|
||||||
qualifiedAlbums.forEach(a => {
|
qualifiedAlbums.forEach(a => {
|
||||||
// 去除当前页面的专辑
|
// 去除当前页面的专辑
|
||||||
if (Number(a.id) === Number(albumID) || album?.name === a.name) return
|
if (formatName(a.name) === formatName(album?.name ?? '')) return
|
||||||
|
|
||||||
// 去除重复的专辑(包含 deluxe edition 的专辑会视为重复)
|
// 去除重复的专辑(包含 deluxe edition 的专辑会视为重复)
|
||||||
if (
|
if (
|
||||||
uniqueAlbums.findIndex(aa => {
|
uniqueAlbums.findIndex(aa => {
|
||||||
return (
|
return formatName(a.name) === formatName(aa.name)
|
||||||
a.name === aa.name ||
|
|
||||||
a.name.toLowerCase().replace(/(\s|deluxe|edition|\(|\))/g, '') ===
|
|
||||||
aa.name.toLowerCase().replace(/(\s|deluxe|edition|\(|\))/g, '')
|
|
||||||
)
|
|
||||||
}) !== -1
|
}) !== -1
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
|
|
@ -248,25 +251,27 @@ const MoreAlbum = ({ album }: { album: Album | undefined }) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
return uniqueAlbums.slice(0, 5)
|
return uniqueAlbums.slice(0, 5)
|
||||||
}, [album?.id, album?.name, albums])
|
}, [album?.name, albums])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="my-5 h-px w-full bg-gray-100 dark:bg-gray-800"></div>
|
<div className='my-5 h-px w-full bg-gray-100 dark:bg-gray-800'></div>
|
||||||
<div className="pl-px text-[1.375rem] font-semibold text-gray-800 dark:text-gray-100">
|
<div className='pl-px text-[1.375rem] font-semibold text-gray-800 dark:text-gray-100'>
|
||||||
More by{' '}
|
More by{' '}
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/artist/${album?.artist?.id}`}
|
to={`/artist/${album?.artist?.id}`}
|
||||||
className="cursor-default hover:underline"
|
className='cursor-default hover:underline'
|
||||||
>
|
>
|
||||||
{album?.artist.name}
|
{album?.artist.name}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3">
|
<div className='mt-3'>
|
||||||
<CoverRow
|
<CoverRow
|
||||||
albums={filteredAlbums}
|
albums={filteredAlbums}
|
||||||
subtitle={Subtitle.TYPE_RELEASE_YEAR}
|
subtitle={Subtitle.TYPE_RELEASE_YEAR}
|
||||||
isSkeleton={isLoading}
|
isSkeleton={isLoading}
|
||||||
|
rows={1}
|
||||||
|
navigateCallback={scrollToTop}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -289,7 +294,7 @@ const Album = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-10">
|
<div className='mt-10'>
|
||||||
<Header
|
<Header
|
||||||
album={album?.album}
|
album={album?.album}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
|
@ -301,10 +306,10 @@ const Album = () => {
|
||||||
isSkeleton={isLoading}
|
isSkeleton={isLoading}
|
||||||
/>
|
/>
|
||||||
{album?.album && (
|
{album?.album && (
|
||||||
<div className="mt-5 text-xs text-gray-400">
|
<div className='mt-5 text-xs text-gray-400'>
|
||||||
<div> Released {formatDate(album.album.publishTime || 0, 'en')} </div>
|
<div> Released {formatDate(album.album.publishTime || 0, 'en')} </div>
|
||||||
{album.album.company && (
|
{album.album.company && (
|
||||||
<div className="mt-[2px]">© {album.album.company} </div>
|
<div className='mt-[2px]'>© {album.album.company} </div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,15 @@ export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CoverRow
|
<CoverRow
|
||||||
title="Good Morning"
|
title='Good Morning'
|
||||||
playlists={recommendedPlaylists?.result.slice(0, 10) ?? []}
|
playlists={recommendedPlaylists?.result.slice(0, 10) ?? []}
|
||||||
isSkeleton={isLoadingRecommendedPlaylists}
|
isSkeleton={isLoadingRecommendedPlaylists}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-10 mb-4 text-[28px] font-bold text-black dark:text-white">
|
<div className='mt-10 mb-4 text-[28px] font-bold text-black dark:text-white'>
|
||||||
For You
|
For You
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-6">
|
<div className='grid grid-cols-2 gap-6'>
|
||||||
<DailyTracksCard />
|
<DailyTracksCard />
|
||||||
<FMCard />
|
<FMCard />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,13 @@ const EmailInput = ({
|
||||||
setEmail: (email: string) => void
|
setEmail: (email: string) => void
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className='w-full'>
|
||||||
<div className="mb-1 text-sm font-medium text-gray-700">Email</div>
|
<div className='mb-1 text-sm font-medium text-gray-700'>Email</div>
|
||||||
<input
|
<input
|
||||||
value={email}
|
value={email}
|
||||||
onChange={e => setEmail(e.target.value)}
|
onChange={e => setEmail(e.target.value)}
|
||||||
className="w-full rounded-md border border-gray-300 px-2 py-2"
|
className='w-full rounded-md border border-gray-300 px-2 py-2'
|
||||||
type="email"
|
type='email'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -45,9 +45,9 @@ const PhoneInput = ({
|
||||||
setPhone: (phone: string) => void
|
setPhone: (phone: string) => void
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className='w-full'>
|
||||||
<div className="mb-1 text-sm font-medium text-gray-700">Phone</div>
|
<div className='mb-1 text-sm font-medium text-gray-700'>Phone</div>
|
||||||
<div className="flex w-full">
|
<div className='flex w-full'>
|
||||||
<input
|
<input
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'rounded-md rounded-r-none border border-r-0 border-gray-300 px-3 py-2',
|
'rounded-md rounded-r-none border border-r-0 border-gray-300 px-3 py-2',
|
||||||
|
|
@ -55,14 +55,14 @@ const PhoneInput = ({
|
||||||
countryCode.length == 4 && 'w-16',
|
countryCode.length == 4 && 'w-16',
|
||||||
countryCode.length >= 5 && 'w-20'
|
countryCode.length >= 5 && 'w-20'
|
||||||
)}
|
)}
|
||||||
type="text"
|
type='text'
|
||||||
placeholder="+86"
|
placeholder='+86'
|
||||||
value={countryCode}
|
value={countryCode}
|
||||||
onChange={e => setCountryCode(e.target.value)}
|
onChange={e => setCountryCode(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className="flex-grow rounded-md rounded-l-none border border-gray-300 px-3 py-2"
|
className='flex-grow rounded-md rounded-l-none border border-gray-300 px-3 py-2'
|
||||||
type="text"
|
type='text'
|
||||||
value={phone}
|
value={phone}
|
||||||
onChange={e => setPhone(e.target.value)}
|
onChange={e => setPhone(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -80,22 +80,22 @@ const PasswordInput = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 flex w-full flex-col">
|
<div className='mt-3 flex w-full flex-col'>
|
||||||
<div className="mb-1 text-sm font-medium text-gray-700">Password</div>
|
<div className='mb-1 text-sm font-medium text-gray-700'>Password</div>
|
||||||
<div className="flex w-full">
|
<div className='flex w-full'>
|
||||||
<input
|
<input
|
||||||
value={password}
|
value={password}
|
||||||
onChange={e => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
className="w-full rounded-md rounded-r-none border border-r-0 border-gray-300 px-2 py-2"
|
className='w-full rounded-md rounded-r-none border border-r-0 border-gray-300 px-2 py-2'
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-center rounded-md rounded-l-none border border-l-0 border-gray-300 pr-1">
|
<div className='flex items-center justify-center rounded-md rounded-l-none border border-l-0 border-gray-300 pr-1'>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
className="cursor-default rounded p-1.5 text-gray-400 transition duration-300 hover:bg-gray-100 hover:text-gray-600"
|
className='cursor-default rounded p-1.5 text-gray-400 transition duration-300 hover:bg-gray-100 hover:text-gray-600'
|
||||||
>
|
>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
className="h-5 w-5"
|
className='h-5 w-5'
|
||||||
name={showPassword ? 'eye-off' : 'eye'}
|
name={showPassword ? 'eye-off' : 'eye'}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -153,21 +153,21 @@ const OtherLoginMethods = ({
|
||||||
]
|
]
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="mt-8 mb-4 flex w-full items-center">
|
<div className='mt-8 mb-4 flex w-full items-center'>
|
||||||
<span className="h-px flex-grow bg-gray-300"></span>
|
<span className='h-px flex-grow bg-gray-300'></span>
|
||||||
<span className="mx-2 text-sm text-gray-400">or</span>
|
<span className='mx-2 text-sm text-gray-400'>or</span>
|
||||||
<span className="h-px flex-grow bg-gray-300"></span>
|
<span className='h-px flex-grow bg-gray-300'></span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className='flex gap-3'>
|
||||||
{otherLoginMethods.map(
|
{otherLoginMethods.map(
|
||||||
({ id, name }) =>
|
({ id, name }) =>
|
||||||
method !== id && (
|
method !== id && (
|
||||||
<button
|
<button
|
||||||
key={id}
|
key={id}
|
||||||
onClick={() => setMethod(id)}
|
onClick={() => setMethod(id)}
|
||||||
className="flex w-full cursor-default items-center justify-center rounded-lg bg-gray-100 py-2 font-medium text-gray-600 transition duration-300 hover:bg-gray-200 hover:text-gray-800"
|
className='flex w-full cursor-default items-center justify-center rounded-lg bg-gray-100 py-2 font-medium text-gray-600 transition duration-300 hover:bg-gray-200 hover:text-gray-800'
|
||||||
>
|
>
|
||||||
<SvgIcon className="mr-2 h-5 w-5" name={id} />
|
<SvgIcon className='mr-2 h-5 w-5' name={id} />
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
@ -274,11 +274,11 @@ const LoginWithQRCode = () => {
|
||||||
}, [qrCodeUrl])
|
}, [qrCodeUrl])
|
||||||
const qrCodeMessage = 'test'
|
const qrCodeMessage = 'test'
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className='flex flex-col items-center justify-center'>
|
||||||
<div className="rounded-3xl border p-6">
|
<div className='rounded-3xl border p-6'>
|
||||||
<img src={qrCodeImage} alt="QR Code" className="no-drag" />
|
<img src={qrCodeImage} alt='QR Code' className='no-drag' />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 text-sm text-gray-500">{qrCodeMessage}</div>
|
<div className='mt-4 text-sm text-gray-500'>{qrCodeMessage}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -287,8 +287,8 @@ export default function Login() {
|
||||||
const [method, setMethod] = useState<Method>(Method.PHONE)
|
const [method, setMethod] = useState<Method>(Method.PHONE)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full place-content-center">
|
<div className='grid h-full place-content-center'>
|
||||||
<div className="w-80">
|
<div className='w-80'>
|
||||||
{method === Method.EMAIL && <LoginWithEmail />}
|
{method === Method.EMAIL && <LoginWithEmail />}
|
||||||
{method === Method.PHONE && <LoginWithPhone />}
|
{method === Method.PHONE && <LoginWithPhone />}
|
||||||
{method === Method.QRCODE && <LoginWithQRCode />}
|
{method === Method.QRCODE && <LoginWithQRCode />}
|
||||||
|
|
|
||||||
|
|
@ -27,18 +27,18 @@ const Header = memo(
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{/* Header background */}
|
{/* Header background */}
|
||||||
<div className="absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden">
|
<div className='absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden'>
|
||||||
<img src={coverUrl} className="absolute top-0 w-full blur-[100px]" />
|
<img src={coverUrl} className='absolute top-0 w-full blur-[100px]' />
|
||||||
<img src={coverUrl} className="absolute top-0 w-full blur-[100px]" />
|
<img src={coverUrl} className='absolute top-0 w-full blur-[100px]' />
|
||||||
<div className="absolute top-0 h-full w-full bg-gradient-to-b from-white/[.84] to-white dark:from-black/[.5] dark:to-[#1d1d1d]"></div>
|
<div className='absolute top-0 h-full w-full bg-gradient-to-b from-white/[.84] to-white dark:from-black/[.5] dark:to-[#1d1d1d]'></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-[16rem_auto] items-center gap-9">
|
<div className='grid grid-cols-[16rem_auto] items-center gap-9'>
|
||||||
{/* Cover */}
|
{/* Cover */}
|
||||||
<div className="relative z-0 aspect-square self-start">
|
<div className='relative z-0 aspect-square self-start'>
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<div
|
<div
|
||||||
className="absolute top-3.5 z-[-1] h-full w-full scale-x-[.92] scale-y-[.96] rounded-2xl bg-cover opacity-40 blur-lg filter"
|
className='absolute top-3.5 z-[-1] h-full w-full scale-x-[.92] scale-y-[.96] rounded-2xl bg-cover opacity-40 blur-lg filter'
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url("${coverUrl}")`,
|
backgroundImage: `url("${coverUrl}")`,
|
||||||
}}
|
}}
|
||||||
|
|
@ -48,70 +48,70 @@ const Header = memo(
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<img
|
<img
|
||||||
src={coverUrl}
|
src={coverUrl}
|
||||||
className="rounded-2xl border border-black border-opacity-5"
|
className='rounded-2xl border border-black border-opacity-5'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<Skeleton v-else className="h-full w-full rounded-2xl" />
|
<Skeleton v-else className='h-full w-full rounded-2xl' />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <!-- Playlist info --> */}
|
{/* <!-- Playlist info --> */}
|
||||||
<div className="z-10 flex h-full flex-col justify-between">
|
<div className='z-10 flex h-full flex-col justify-between'>
|
||||||
{/* <!-- Playlist name --> */}
|
{/* <!-- Playlist name --> */}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<div className="text-4xl font-bold dark:text-white">
|
<div className='text-4xl font-bold dark:text-white'>
|
||||||
{playlist?.name}
|
{playlist?.name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<Skeleton v-else className="w-3/4 text-4xl">
|
<Skeleton v-else className='w-3/4 text-4xl'>
|
||||||
PLACEHOLDER
|
PLACEHOLDER
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* <!-- Playlist creator --> */}
|
{/* <!-- Playlist creator --> */}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<div className="mt-5 text-lg font-medium text-gray-800 dark:text-gray-300">
|
<div className='mt-5 text-lg font-medium text-gray-800 dark:text-gray-300'>
|
||||||
Playlist by <span>{playlist?.creator?.nickname}</span>
|
Playlist by <span>{playlist?.creator?.nickname}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<Skeleton v-else className="mt-5 w-64 text-lg">
|
<Skeleton v-else className='mt-5 w-64 text-lg'>
|
||||||
PLACEHOLDER
|
PLACEHOLDER
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* <!-- Playlist last update time & track count --> */}
|
{/* <!-- Playlist last update time & track count --> */}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<div className="text-sm font-thin text-gray-500 dark:text-gray-400">
|
<div className='text-sm font-thin text-gray-500 dark:text-gray-400'>
|
||||||
Updated at
|
Updated at
|
||||||
{formatDate(playlist?.updateTime || 0, 'en')} ·
|
{formatDate(playlist?.updateTime || 0, 'en')} ·
|
||||||
{playlist?.trackCount} Songs
|
{playlist?.trackCount} Songs
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<Skeleton v-else className="w-72 translate-y-px text-sm">
|
<Skeleton v-else className='w-72 translate-y-px text-sm'>
|
||||||
PLACEHOLDER
|
PLACEHOLDER
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* <!-- Playlist description --> */}
|
{/* <!-- Playlist description --> */}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<div className="line-clamp-2 mt-5 min-h-[2.5rem] text-sm text-gray-500 dark:text-gray-400">
|
<div className='line-clamp-2 mt-5 min-h-[2.5rem] text-sm text-gray-500 dark:text-gray-400'>
|
||||||
{playlist?.description}
|
{playlist?.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<Skeleton v-else className="mt-5 min-h-[2.5rem] w-1/2 text-sm">
|
<Skeleton v-else className='mt-5 min-h-[2.5rem] w-1/2 text-sm'>
|
||||||
PLACEHOLDER
|
PLACEHOLDER
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* <!-- Buttons --> */}
|
{/* <!-- Buttons --> */}
|
||||||
<div className="mt-5 flex gap-4">
|
<div className='mt-5 flex gap-4'>
|
||||||
<Button onClick={() => handlePlay()} isSkelton={isLoading}>
|
<Button onClick={() => handlePlay()} isSkelton={isLoading}>
|
||||||
<SvgIcon name="play" className="mr-2 h-4 w-4" />
|
<SvgIcon name='play' className='mr-2 h-4 w-4' />
|
||||||
PLAY
|
PLAY
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
@ -120,7 +120,7 @@ const Header = memo(
|
||||||
isSkelton={isLoading}
|
isSkelton={isLoading}
|
||||||
onClick={() => toast('Work in progress')}
|
onClick={() => toast('Work in progress')}
|
||||||
>
|
>
|
||||||
<SvgIcon name="heart" className="h-4 w-4" />
|
<SvgIcon name='heart' className='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -129,7 +129,7 @@ const Header = memo(
|
||||||
isSkelton={isLoading}
|
isSkelton={isLoading}
|
||||||
onClick={() => toast('Work in progress')}
|
onClick={() => toast('Work in progress')}
|
||||||
>
|
>
|
||||||
<SvgIcon name="more" className="h-4 w-4" />
|
<SvgIcon name='more' className='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -225,7 +225,7 @@ const Playlist = () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-10">
|
<div className='mt-10'>
|
||||||
<Header
|
<Header
|
||||||
playlist={playlist?.playlist}
|
playlist={playlist?.playlist}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
|
|
||||||
|
|
@ -102,3 +102,9 @@ export function formatDuration(
|
||||||
export function sleep(time: number) {
|
export function sleep(time: number) {
|
||||||
return new Promise(resolve => setTimeout(resolve, time))
|
return new Promise(resolve => setTimeout(resolve, time))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function scrollToTop(smooth = false) {
|
||||||
|
const main = document.getElementById('mainContainer')
|
||||||
|
if (!main) return
|
||||||
|
main.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' })
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ module.exports = {
|
||||||
bracketSpacing: true,
|
bracketSpacing: true,
|
||||||
htmlWhitespaceSensitivity: 'strict',
|
htmlWhitespaceSensitivity: 'strict',
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
|
jsxSingleQuote: true,
|
||||||
|
|
||||||
// Tailwind CSS
|
// Tailwind CSS
|
||||||
plugins: [require('prettier-plugin-tailwindcss')],
|
plugins: [require('prettier-plugin-tailwindcss')],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue