feat: updates

This commit is contained in:
qier222 2022-06-08 00:07:04 +08:00
parent d09c5fd171
commit 133881d287
No known key found for this signature in database
GPG key ID: 9C85007ED905F14D
28 changed files with 389 additions and 115 deletions

View file

@ -107,10 +107,8 @@ class Main {
height: this.store.get('window.height'), height: this.store.get('window.height'),
minWidth: 1240, minWidth: 1240,
minHeight: 848, minHeight: 848,
// vibrancy: 'fullscreen-ui', titleBarStyle: isMac ? 'customButtonsOnHover' : 'hidden',
titleBarStyle: 'customButtonsOnHover',
trafficLightPosition: { x: 24, y: 24 }, trafficLightPosition: { x: 24, y: 24 },
// frame: !(isWindows || isLinux), // TODO: 适用于linux下独立的启用开关
frame: false, frame: false,
transparent: true, transparent: true,
} }

View file

@ -4,13 +4,17 @@ import IpcRendererReact from '@/web/IpcRendererReact'
import Layout from '@/web/components/New/Layout' import Layout from '@/web/components/New/Layout'
import Devtool from '@/web/components/New/Devtool' import Devtool from '@/web/components/New/Devtool'
import ErrorBoundary from '@/web/components/New/ErrorBoundary' import ErrorBoundary from '@/web/components/New/ErrorBoundary'
import useIsMobile from '@/web/hooks/useIsMobile'
import LayoutMobile from '@/web/components/New/LayoutMobile'
const App = () => { const App = () => {
const isMobile = useIsMobile()
return ( return (
<ErrorBoundary> <ErrorBoundary>
<div className='dark'> <div className='dark'>
{window.env?.isEnableTitlebar && <TitleBar />} {window.env?.isEnableTitlebar && <TitleBar />}
<Layout /> {isMobile ? <LayoutMobile /> : <Layout />}
<Toaster position='bottom-center' containerStyle={{ bottom: '5rem' }} /> <Toaster position='bottom-center' containerStyle={{ bottom: '5rem' }} />
<IpcRendererReact /> <IpcRendererReact />
<Devtool /> <Devtool />

View file

@ -93,7 +93,7 @@ export interface FetchPersonalFMResponse {
} }
export function fetchPersonalFM(): Promise<FetchPersonalFMResponse> { export function fetchPersonalFM(): Promise<FetchPersonalFMResponse> {
return request({ return request({
url: window.ipcRenderer ? '/personal/fm' : '/personal_fm', url: window.env?.isElectron ? '/personal/fm' : '/personal_fm',
method: 'get', method: 'get',
params: { params: {
timestamp: Date.now(), timestamp: Date.now(),

View file

@ -0,0 +1,3 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.3916 7.48438H23.7295C24.1777 7.48438 24.5381 7.11523 24.5381 6.67578C24.5381 6.21875 24.1777 5.86719 23.7295 5.86719H12.3916C11.9433 5.86719 11.5918 6.22754 11.5918 6.67578C11.5918 7.11523 11.9433 7.48438 12.3916 7.48438ZM12.3916 12.0898H23.7295C24.1777 12.0898 24.5381 11.7207 24.5381 11.2812C24.5381 10.8242 24.1777 10.4727 23.7295 10.4727H12.3916C11.9433 10.4727 11.5918 10.833 11.5918 11.2812C11.5918 11.7207 11.9433 12.0898 12.3916 12.0898ZM4.96483 16.4668L8.02342 14.4189C8.57713 14.0498 8.55077 13.2764 8.02342 12.9072L4.96483 10.8242C4.2617 10.3408 3.51463 10.6045 3.52342 11.457V15.8428C3.51463 16.6689 4.28807 16.915 4.96483 16.4668ZM12.3916 16.6953H23.7295C24.1777 16.6953 24.5381 16.3262 24.5381 15.8867C24.5381 15.4385 24.1777 15.0781 23.7295 15.0781H12.3916C11.9433 15.0781 11.5918 15.4385 11.5918 15.8867C11.5918 16.3262 11.9433 16.6953 12.3916 16.6953ZM12.3916 21.3008H19.0361C19.4844 21.3008 19.8447 20.9316 19.8447 20.4922C19.8447 20.0439 19.4844 19.6836 19.0361 19.6836H12.3916C11.9433 19.6836 11.5918 20.0439 11.5918 20.4922C11.5918 20.9316 11.9433 21.3008 12.3916 21.3008Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -2,41 +2,79 @@ import { resizeImage } from '@/web/utils/common'
import { css, cx } from '@emotion/css' import { css, cx } from '@emotion/css'
import Image from './Image' import Image from './Image'
const ArtistRow = ({ const Artist = ({ artist }: { artist: Artist }) => {
artists,
title,
className,
}: {
artists: Artist[] | undefined
title?: string
className?: string
}) => {
return ( return (
<div className={className}> <div className='text-center'>
{/* Title */} <Image
{title && ( alt={artist.name}
<h4 className='mb-6 text-14 font-bold uppercase dark:text-neutral-300'> src={resizeImage(artist.img1v1Url, 'md')}
{title} className={cx(
</h4> 'aspect-square rounded-full',
)} css`
min-width: 96px;
{/* Artists */} min-height: 96px;
<div className='grid grid-cols-5 gap-10'> `
{artists?.map(artist => ( )}
<div key={artist.id} className='text-center'> />
<Image <div className='line-clamp-1 mt-2.5 text-12 font-medium text-neutral-700 dark:text-neutral-600 lg:text-14 lg:font-bold'>
alt={artist.name} {artist.name}
src={resizeImage(artist.img1v1Url, 'md')}
className='aspect-square rounded-full'
/>
<div className='line-clamp-1 mt-2.5 text-14 font-bold text-neutral-700 dark:text-neutral-600'>
{artist.name}
</div>
</div>
))}
</div> </div>
</div> </div>
) )
} }
const ArtistRow = ({
artists,
title,
className,
placeholderRow,
}: {
artists: Artist[] | undefined
title?: string
className?: string
placeholderRow?: number
}) => {
return (
<div className={className}>
{/* Title */}
{title && (
<h4 className='mb-6 text-12 font-medium uppercase dark:text-neutral-300 lg:text-14 lg:font-bold'>
{title}
</h4>
)}
{/* Artists */}
{artists && (
<div className='no-scrollbar -ml-2.5 flex w-screen overflow-x-scroll lg:ml-auto lg:grid lg:w-auto lg:grid-cols-5 lg:gap-10'>
{artists.map(artist => (
<div
className='mr-5 first-of-type:ml-2.5 last-of-type:mr-2.5 lg:mr-0'
key={artist.id}
>
<Artist artist={artist} key={artist.id} />
</div>
))}
</div>
)}
{/* Placeholder */}
{placeholderRow && !artists && (
<div className='no-scrollbar -ml-2.5 flex w-screen overflow-x-scroll lg:ml-auto lg:grid lg:w-auto lg:grid-cols-5 lg:gap-10'>
{[...new Array(placeholderRow * 5).keys()].map(i => (
<div
className='mr-5 first-of-type:ml-2.5 last-of-type:mr-2.5 lg:mr-0'
key={i}
>
<div className='aspect-square w-full rounded-full bg-white dark:bg-neutral-800' />
<div className='mt-2.5 text-14 font-bold text-transparent'>
PLACE
</div>
</div>
))}
</div>
)}
</div>
)
}
export default ArtistRow export default ArtistRow

View file

@ -38,7 +38,7 @@ const CoverRow = ({
)} )}
{/* Items */} {/* Items */}
<div className='grid grid-cols-3 gap-10 xl:grid-cols-4 2xl:grid-cols-5'> <div className='grid grid-cols-3 gap-4 lg:gap-10 xl:grid-cols-4 2xl:grid-cols-5'>
{albums?.map(album => ( {albums?.map(album => (
<Image <Image
onClick={() => goTo(album.id)} onClick={() => goTo(album.id)}

View file

@ -14,14 +14,14 @@ const CoverWall = ({
const breakpoint = useBreakpoint() const breakpoint = useBreakpoint()
const sizes = { const sizes = {
small: { small: {
sm: 'xs', sm: 'sm',
md: 'xs', md: 'sm',
lg: 'sm', lg: 'sm',
xl: 'sm', xl: 'sm',
'2xl': 'md', '2xl': 'md',
}, },
large: { large: {
sm: 'xs', sm: 'sm',
md: 'sm', md: 'sm',
lg: 'md', lg: 'md',
xl: 'md', xl: 'md',
@ -32,7 +32,7 @@ const CoverWall = ({
return ( return (
<div <div
className={cx( className={cx(
'grid w-full grid-flow-row-dense grid-cols-8', 'grid w-full grid-flow-row-dense grid-cols-4 lg:grid-cols-8',
css` css`
gap: 13px; gap: 13px;
` `
@ -48,7 +48,7 @@ const CoverWall = ({
alt='Album Cover' alt='Album Cover'
placeholder={null} placeholder={null}
className={cx( className={cx(
'aspect-square h-full w-full rounded-24', 'aspect-square h-full w-full rounded-20 lg:rounded-24',
album.large && 'col-span-2 row-span-2' album.large && 'col-span-2 row-span-2'
)} )}
onClick={() => navigate(`/album/${album.id}`)} onClick={() => navigate(`/album/${album.id}`)}

View file

@ -1,14 +1,18 @@
import useBreakpoint from '@/web/hooks/useBreakpoint'
import { ReactQueryDevtools } from 'react-query/devtools' import { ReactQueryDevtools } from 'react-query/devtools'
const Devtool = () => { const Devtool = () => {
const breakpoint = useBreakpoint()
const isMobile = ['sm', 'md'].includes(breakpoint)
return ( return (
<ReactQueryDevtools <ReactQueryDevtools
initialIsOpen={false} initialIsOpen={false}
toggleButtonProps={{ toggleButtonProps={{
style: { style: {
position: 'fixed', position: 'fixed',
bottom: 0, bottom: isMobile ? 'auto' : 0,
right: 0, right: 0,
top: isMobile ? 0 : 1,
left: 'auto', left: 'auto',
}, },
}} }}

View file

@ -1,6 +1,6 @@
import Main from '@/web/components/New/Main' import Main from '@/web/components/New/Main'
import Player from '@/web/components/New/Player' import Player from '@/web/components/New/Player'
import Sidebar from '@/web/components/New/Sidebar' import MenuBar from '@/web/components/New/MenuBar'
import Topbar from '@/web/components/New/Topbar' import Topbar from '@/web/components/New/Topbar'
import { css, cx } from '@emotion/css' import { css, cx } from '@emotion/css'
import { useMemo } from 'react' import { useMemo } from 'react'
@ -16,7 +16,7 @@ const Layout = () => {
id='layout' id='layout'
className={cx( className={cx(
'relative grid h-screen select-none overflow-hidden bg-white dark:bg-black', 'relative grid h-screen select-none overflow-hidden bg-white dark:bg-black',
window.ipcRenderer && 'rounded-24', window.env?.isElectron && 'rounded-24',
css` css`
grid-template-columns: 6.5rem auto 358px; grid-template-columns: 6.5rem auto 358px;
grid-template-rows: 132px auto; grid-template-rows: 132px auto;
@ -24,17 +24,17 @@ const Layout = () => {
showPlayer showPlayer
? css` ? css`
grid-template-areas: grid-template-areas:
'sidebar main -' 'menubar main -'
'sidebar main player'; 'menubar main player';
` `
: css` : css`
grid-template-areas: grid-template-areas:
'sidebar main main' 'menubar main main'
'sidebar main main'; 'menubar main main';
` `
)} )}
> >
<Sidebar /> <MenuBar />
<Topbar /> <Topbar />
<Main /> <Main />
{showPlayer && <Player />} {showPlayer && <Player />}

View file

@ -0,0 +1,37 @@
import Player from '@/web/components/New/PlayerMobile'
import { css, cx } from '@emotion/css'
import { useMemo } from 'react'
import { player } from '@/web/store'
import { useSnapshot } from 'valtio'
import Router from '@/web/components/New/Router'
import MenuBar from './MenuBar'
const LayoutMobile = () => {
const playerSnapshot = useSnapshot(player)
const showPlayer = useMemo(() => !!playerSnapshot.track, [playerSnapshot])
return (
<div id='layout' className='select-none bg-white pb-32 pt-3 dark:bg-black'>
<main className='min-h-screen overflow-y-auto overflow-x-hidden px-2.5 pb-16'>
<Router />
</main>
{showPlayer && (
<div
className={cx(
'fixed left-7 right-7',
css`
bottom: 72px;
`
)}
>
<Player />
</div>
)}
<div className='fixed bottom-0 left-0 right-0 py-3 dark:bg-black'>
<MenuBar />
</div>
</div>
)
}
export default LayoutMobile

View file

@ -5,13 +5,10 @@ const Main = () => {
return ( return (
<main <main
className={cx( className={cx(
'overflow-y-auto pb-16 pr-6 pl-10', 'no-scrollbar overflow-y-auto pb-16 pr-6 pl-10',
css` css`
padding-top: 132px; padding-top: 132px;
grid-area: main; grid-area: main;
&::-webkit-scrollbar {
display: none;
}
` `
)} )}
> >

View file

@ -1,9 +1,11 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { css, cx } from '@emotion/css' import { css, cx } from '@emotion/css'
import Icon from '../Icon' import Icon from '../Icon'
import { NavLink, useLocation } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { useAnimation, motion } from 'framer-motion' import { useAnimation, motion } from 'framer-motion'
import { ease } from '@/web/utils/const' import { ease } from '@/web/utils/const'
import TrafficLight from './TrafficLight'
import useIsMobile from '@/web/hooks/useIsMobile'
const tabs = [ const tabs = [
{ {
@ -75,36 +77,68 @@ const TabName = () => {
) )
} }
const Sidebar = () => { const Tabs = () => {
const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const controls = useAnimation()
const animate = async (path: string) => {
await controls.start((p: string) =>
p === path && location.pathname !== path ? 'scale' : 'reset'
)
await controls.start('reset')
}
return (
<div className='grid grid-cols-4 justify-items-center text-black/10 dark:text-white/20 lg:grid-cols-1 lg:gap-12'>
{tabs.map(tab => (
<motion.div
key={tab.name}
animate={controls}
transition={{ ease, duration: 0.18 }}
onMouseDown={() => animate(tab.path)}
onClick={() => navigate(tab.path)}
custom={tab.path}
variants={{
scale: { scale: 0.8 },
reset: { scale: 1 },
}}
>
<Icon
name={tab.icon}
className={cx(
'app-region-no-drag h-10 w-10 transition-colors duration-500',
location.pathname === tab.path
? 'text-brand-600 dark:text-brand-700'
: 'hover:text-black dark:hover:text-white'
)}
/>
</motion.div>
))}
</div>
)
}
const MenuBar = () => {
const isMobile = useIsMobile()
return ( return (
<div <div
className={cx( className={cx(
'app-region-drag relative flex h-full w-full flex-col justify-center', 'app-region-drag relative flex h-full w-full flex-col justify-center',
css` css`
grid-area: sidebar; grid-area: menubar;
` `
)} )}
> >
<div className='grid grid-cols-1 justify-items-center gap-12 text-black/10 dark:text-white/20'> {window.env?.isMac && (
{tabs.map(tab => ( <div className='fixed top-6 left-6 translate-y-0.5'>
<NavLink key={tab.name} to={tab.path}> <TrafficLight />
<Icon </div>
name={tab.icon} )}
className={cx( <Tabs />
'app-region-no-drag h-10 w-10 transition duration-500 active:scale-75', {!isMobile && <TabName />}
location.pathname === tab.path
? 'text-brand-600 dark:text-brand-700'
: 'hover:text-black dark:hover:text-white'
)}
/>
</NavLink>
))}
</div>
<TabName />
</div> </div>
) )
} }
export default Sidebar export default MenuBar

View file

@ -122,8 +122,8 @@ const NowPlaying = () => {
<div className='mt-4 flex w-full items-center justify-between'> <div className='mt-4 flex w-full items-center justify-between'>
<button> <button>
<Icon <Icon
name='shuffle' name='hide-list'
className='h-7 w-7 text-black/90 dark:text-white/40' className='h-7 w-7 text-black/90 dark:text-white/40'
/> />
</button> </button>

View file

@ -73,7 +73,7 @@ const PlayLikedSongsCard = () => {
` `
)} )}
> >
<div className='text-21 font-medium text-white/20'> <div className='text-18 font-medium text-white/20 lg:text-21'>
{lyricLines.map((line, index) => ( {lyricLines.map((line, index) => (
<div key={`${index}-${line}`}>{line}</div> <div key={`${index}-${line}`}>{line}</div>
))} ))}

View file

@ -13,7 +13,7 @@ const Player = () => {
)} )}
> >
<PlayingNext className='mb-3 h-full' /> <PlayingNext className='mb-3 h-full' />
<div className='pb-20'> <div className='pb-6'>
<NowPlaying /> <NowPlaying />
</div> </div>
</div> </div>

View file

@ -0,0 +1,95 @@
import { player } from '@/web/store'
import { css, cx } from '@emotion/css'
import { useSnapshot } from 'valtio'
import Image from '@/web/components/New/Image'
import Icon from '@/web/components/Icon'
import useCoverColor from '@/web/hooks/useCoverColor'
import { resizeImage } from '@/web/utils/common'
import { motion, PanInfo, useMotionValue } from 'framer-motion'
const PlayerMobile = () => {
const playerSnapshot = useSnapshot(player)
const bgColor = useCoverColor(playerSnapshot.track?.al?.picUrl ?? '')
const x = useMotionValue(0)
const onDragEnd = (
event: MouseEvent | TouchEvent | PointerEvent,
info: PanInfo
) => {
const x = info.offset.x
const offset = 100
if (x > offset) player.nextTrack()
if (x < -offset) player.prevTrack()
}
return (
<div
className={cx(
'relative flex h-16 w-full items-center rounded-20 py-2.5 px-3',
css`
background-color: ${bgColor.to};
`
)}
>
<div
className={cx(
'absolute -top-2.5 h-1.5 w-10 rounded-full bg-brand-700',
css`
left: calc((100% - 40px) / 2);
`
)}
></div>
<Image
src={resizeImage(playerSnapshot.track?.al.picUrl || '', 'sm')}
alt='Cover'
className='z-10 aspect-square h-full rounded-lg'
/>
<div className='relative flex-grow overflow-hidden px-3'>
<motion.div
drag='x'
style={{ x }}
dragConstraints={{ left: 0, right: 0 }}
onDragEnd={onDragEnd}
className='line-clamp-1 text-14 font-bold text-white'
>
{playerSnapshot.track?.name}
</motion.div>
<div
className={cx(
'absolute left-0 top-0 bottom-0 w-3 ',
css`
background: linear-gradient(to right, ${bgColor.to}, transparent);
`
)}
></div>
<div
className={cx(
'absolute right-0 top-0 bottom-0 w-3 bg-red-200',
css`
background: linear-gradient(to left, ${bgColor.to}, transparent);
`
)}
></div>
</div>
<button>
<Icon name='heart' className='h-7 w-7 text-white/10' />
</button>
<button
onClick={() => player.playOrPause()}
className='ml-2.5 flex items-center justify-center rounded-full bg-white/20 p-2.5'
>
<Icon
name={playerSnapshot.state === 'playing' ? 'pause' : 'play'}
className='h-6 w-6 text-white/80'
/>
</button>
</div>
)
}
export default PlayerMobile

View file

@ -6,6 +6,7 @@ import { css, cx } from '@emotion/css'
import { AnimatePresence, motion } from 'framer-motion' import { AnimatePresence, motion } from 'framer-motion'
import Image from './Image' import Image from './Image'
import Wave from './Wave' import Wave from './Wave'
import Icon from '@/web/components/Icon'
const PlayingNext = ({ className }: { className?: string }) => { const PlayingNext = ({ className }: { className?: string }) => {
const playerSnapshot = useSnapshot(player) const playerSnapshot = useSnapshot(player)
@ -15,22 +16,30 @@ const PlayingNext = ({ className }: { className?: string }) => {
<> <>
<div <div
className={cx( className={cx(
'absolute top-0 z-10 pb-6 text-14 font-bold text-neutral-700 dark:text-neutral-300' 'absolute top-0 left-0 z-10 flex w-full items-center justify-between px-4 pb-6 text-14 font-bold text-neutral-700 dark:text-neutral-300'
)} )}
> >
PLAYING NEXT <div className='flex'>
<div className='mr-2 h-4 w-1 bg-brand-700'></div>
PLAYING NEXT
</div>
<div className='flex'>
<div className='mr-2'>
<Icon name='repeat-1' className='h-7 w-7 opacity-40' />
</div>
<div className='mr-1'>
<Icon name='shuffle' className='h-7 w-7 opacity-40' />
</div>
</div>
</div> </div>
<div <div
className={cx( className={cx(
'relative z-10 overflow-scroll', 'no-scrollbar relative z-10 overflow-scroll',
className, className,
css``,
css` css`
padding-top: 42px; padding-top: 42px;
mask-image: linear-gradient(to bottom, transparent 0, black 42px); mask-image: linear-gradient(to bottom, transparent 0, black 42px);
&::-webkit-scrollbar {
display: none;
}
` `
)} )}
> >
@ -60,10 +69,17 @@ const PlayingNext = ({ className }: { className?: string }) => {
{/* Track info */} {/* Track info */}
<div className='mr-3 flex-grow'> <div className='mr-3 flex-grow'>
<div className='line-clamp-1 text-18 font-medium text-neutral-700 dark:text-neutral-200'> <div
className={cx(
'line-clamp-1 text-16 font-medium ',
playerSnapshot.trackIndex === index
? 'text-brand-700'
: 'text-neutral-700 dark:text-neutral-200'
)}
>
{track.name} {track.name}
</div> </div>
<div className='line-clamp-1 mt-1 text-16 font-bold text-neutral-200 dark:text-neutral-700'> <div className='line-clamp-1 mt-1 text-14 font-bold text-neutral-200 dark:text-neutral-700'>
{track.ar.map(a => a.name).join(', ')} {track.ar.map(a => a.name).join(', ')}
</div> </div>
</div> </div>
@ -72,7 +88,7 @@ const PlayingNext = ({ className }: { className?: string }) => {
{playerSnapshot.trackIndex === index ? ( {playerSnapshot.trackIndex === index ? (
<Wave playing={playerSnapshot.state === 'playing'} /> <Wave playing={playerSnapshot.state === 'playing'} />
) : ( ) : (
<div className='text-18 font-medium text-neutral-700 dark:text-neutral-200'> <div className='text-16 font-medium text-neutral-700 dark:text-neutral-200'>
{String(index + 1).padStart(2, '0')} {String(index + 1).padStart(2, '0')}
</div> </div>
)} )}

View file

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react' import { ComponentStory, ComponentMeta } from '@storybook/react'
import Sidebar from './Sidebar' import Sidebar from './MenuBar'
export default { export default {
title: 'Components/Sidebar', title: 'Components/Sidebar',

View file

@ -13,7 +13,7 @@ const Tabs = ({
onChange: (id: string) => void onChange: (id: string) => void
}) => { }) => {
return ( return (
<div className='flex'> <div className='no-scrollbar flex overflow-y-auto'>
{tabs.map(tab => ( {tabs.map(tab => (
<div <div
key={tab.id} key={tab.id}
@ -23,6 +23,7 @@ const Tabs = ({
? 'bg-brand-700 text-white' ? 'bg-brand-700 text-white'
: 'dark:bg-white/10 dark:text-white/20' : 'dark:bg-white/10 dark:text-white/20'
)} )}
onClick={() => onChange(tab.id)}
> >
{tab.name} {tab.name}
</div> </div>

View file

@ -10,13 +10,13 @@ const NavigationButtons = () => {
const navigate = useNavigate() const navigate = useNavigate()
const controlsBack = useAnimation() const controlsBack = useAnimation()
const controlsForward = useAnimation() const controlsForward = useAnimation()
const transition = { duration: 0.2, ease } const transition = { duration: 0.18, ease }
return ( return (
<> <>
<button <button
onClick={async () => { onClick={() => navigate(-1)}
navigate(-1) onMouseDown={async () => {
await controlsBack.start({ x: -5 }) await controlsBack.start({ x: -5 })
await controlsBack.start({ x: 0 }) await controlsBack.start({ x: 0 })
}} }}
@ -29,6 +29,8 @@ const NavigationButtons = () => {
<button <button
onClick={async () => { onClick={async () => {
navigate(1) navigate(1)
}}
onMouseDown={async () => {
await controlsForward.start({ x: 5 }) await controlsForward.start({ x: 5 })
await controlsForward.start({ x: 0 }) await controlsForward.start({ x: 0 })
}} }}

View file

@ -4,6 +4,7 @@ import { useMemo } from 'react'
import { player } from '@/web/store' import { player } from '@/web/store'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import Wave from './Wave' import Wave from './Wave'
import Icon from '@/web/components/Icon'
const TrackList = ({ const TrackList = ({
tracks, tracks,
@ -35,21 +36,31 @@ const TrackList = ({
<div <div
key={track.id} key={track.id}
onClick={e => handleClick(e, track.id)} onClick={e => handleClick(e, track.id)}
className='relative flex items-center py-2 text-18 font-medium text-night-50 transition duration-300 ease-in-out dark:hover:text-neutral-200' className='group relative flex items-center py-2 text-16 font-medium text-neutral-200 transition duration-300 ease-in-out'
> >
<div className='mr-6'>{String(track.no).padStart(2, '0')}</div> <div className='mr-6'>{String(track.no).padStart(2, '0')}</div>
<div className='flex-grow'>{track.name}</div> <div className='flex flex-grow items-center'>
<div className='h-10 w-10'></div> {track.name}
{playingTrack?.id === track.id && playing && (
<div className='ml-4'>
<Wave playing={playing} />
</div>
)}
</div>
<div className='mr-12 flex opacity-0 transition-opacity group-hover:opacity-100'>
<div className='mr-3 flex h-10 w-10 items-center justify-center rounded-full bg-brand-600 text-white/80'>
{/* <Icon name='play' className='h-7 w-7' /> */}
</div>
<div className='mr-3 flex h-10 w-10 items-center justify-center rounded-full bg-night-900 text-white/80'>
{/* <Icon name='play' className='h-7 w-7' /> */}
</div>
<div className='flex h-10 w-10 items-center justify-center rounded-full bg-night-900 text-white/80'>
{/* <Icon name='play' className='h-7 w-7' /> */}
</div>
</div>
<div className='text-right'> <div className='text-right'>
{formatDuration(track.dt, 'en', 'hh:mm:ss')} {formatDuration(track.dt, 'en', 'hh:mm:ss')}
</div> </div>
{/* The wave icon */}
{playingTrack?.id === track.id && playing && (
<div className='absolute -left-7'>
<Wave playing={playing} />
</div>
)}
</div> </div>
))} ))}
</div> </div>

View file

@ -86,8 +86,8 @@ const TrackListHeader = ({
onClick={() => onPlay()} onClick={() => onPlay()}
className='h-[72px] w-[170px] rounded-full dark:bg-brand-700' className='h-[72px] w-[170px] rounded-full dark:bg-brand-700'
></button> ></button>
<button className='ml-6 h-[72px] w-[72px] rounded-full dark:bg-night-50'></button> <button className='ml-6 h-[72px] w-[72px] rounded-full dark:bg-white/10'></button>
<button className='ml-6 h-[72px] w-[72px] rounded-full dark:bg-night-50'></button> <button className='ml-6 h-[72px] w-[72px] rounded-full dark:bg-white/10'></button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,13 @@
const TrafficLight = () => {
const className =
'mr-2 h-3 w-3 rounded-full last-of-type:mr-0 dark:bg-white/20'
return (
<div className='flex'>
<div className={className}></div>
<div className={className}></div>
<div className={className}></div>
</div>
)
}
export default TrafficLight

View file

@ -0,0 +1,8 @@
import useBreakpoint from './useBreakpoint'
const useIsMobile = () => {
const breakpoint = useBreakpoint()
return ['sm', 'md'].includes(breakpoint)
}
export default useIsMobile

View file

@ -66,12 +66,12 @@ const My = () => {
<PageTransition> <PageTransition>
<div className='grid grid-cols-1 gap-10'> <div className='grid grid-cols-1 gap-10'>
<PlayLikedSongsCard /> <PlayLikedSongsCard />
<div>
<ArtistRow <ArtistRow
artists={recentListenedArtists?.map(a => a.artist)} artists={recentListenedArtists?.map(a => a.artist)}
title='RECENTLY LISTENED' placeholderRow={1}
/> title='RECENTLY LISTENED'
</div> />
<div> <div>
<Tabs <Tabs
@ -79,7 +79,13 @@ const My = () => {
value={selectedTab} value={selectedTab}
onChange={(id: string) => setSelectedTab(id)} onChange={(id: string) => setSelectedTab(id)}
/> />
<CoverRow playlists={playlists?.playlist} className='mt-6' /> <CoverRow
playlists={
selectedTab === 'playlists' ? playlists?.playlist : undefined
}
albums={selectedTab === 'albums' ? albums?.data : undefined}
className='mt-6'
/>
</div> </div>
</div> </div>
</PageTransition> </PageTransition>

View file

@ -130,3 +130,7 @@ a {
* { * {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
.no-scrollbar::-webkit-scrollbar {
display: none;
}

View file

@ -48,6 +48,7 @@ module.exports = {
600: '#0E0E0E', 600: '#0E0E0E',
700: '#060606', 700: '#060606',
800: '#020202', 800: '#020202',
900: '#313131',
}, },
neutral: { neutral: {
100: '#E3E3E3', 100: '#E3E3E3',

View file

@ -250,7 +250,9 @@ export class Player {
} }
if (this.trackID !== id) return if (this.trackID !== id) return
Howler.unload() Howler.unload()
const url = audio.includes('?') ? `${audio}&id=${id}` : `${audio}?id=${id}` const url = audio.includes('?')
? `${audio}&dash-id=${id}`
: `${audio}?dash-id=${id}`
const howler = new Howl({ const howler = new Howl({
src: [url], src: [url],
format: ['mp3', 'flac', 'webm'], format: ['mp3', 'flac', 'webm'],
@ -285,7 +287,7 @@ export class Player {
private _cacheAudio(audio: string) { private _cacheAudio(audio: string) {
if (audio.includes('yesplaymusic') || !window.ipcRenderer) return if (audio.includes('yesplaymusic') || !window.ipcRenderer) return
const id = Number(new URL(audio).searchParams.get('id')) const id = Number(new URL(audio).searchParams.get('dash-id'))
if (isNaN(id) || !id) return if (isNaN(id) || !id) return
cacheAudio(id, audio) cacheAudio(id, audio)
} }
@ -490,7 +492,7 @@ export class Player {
async playTrack(trackID: TrackID) { async playTrack(trackID: TrackID) {
this._setStateToLoading() this._setStateToLoading()
const index = this.trackList.findIndex(t => t === trackID) const index = this.trackList.findIndex(t => t === trackID)
if (!index) toast('播放失败,歌曲不在列表内') if (index === -1) toast('播放失败,歌曲不在列表内')
this._trackIndex = index this._trackIndex = index
this._playTrack() this._playTrack()
} }