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

@ -2,41 +2,79 @@ import { resizeImage } from '@/web/utils/common'
import { css, cx } from '@emotion/css'
import Image from './Image'
const ArtistRow = ({
artists,
title,
className,
}: {
artists: Artist[] | undefined
title?: string
className?: string
}) => {
const Artist = ({ artist }: { artist: Artist }) => {
return (
<div className={className}>
{/* Title */}
{title && (
<h4 className='mb-6 text-14 font-bold uppercase dark:text-neutral-300'>
{title}
</h4>
)}
{/* Artists */}
<div className='grid grid-cols-5 gap-10'>
{artists?.map(artist => (
<div key={artist.id} className='text-center'>
<Image
alt={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 className='text-center'>
<Image
alt={artist.name}
src={resizeImage(artist.img1v1Url, 'md')}
className={cx(
'aspect-square rounded-full',
css`
min-width: 96px;
min-height: 96px;
`
)}
/>
<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'>
{artist.name}
</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

View file

@ -38,7 +38,7 @@ const CoverRow = ({
)}
{/* 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 => (
<Image
onClick={() => goTo(album.id)}

View file

@ -14,14 +14,14 @@ const CoverWall = ({
const breakpoint = useBreakpoint()
const sizes = {
small: {
sm: 'xs',
md: 'xs',
sm: 'sm',
md: 'sm',
lg: 'sm',
xl: 'sm',
'2xl': 'md',
},
large: {
sm: 'xs',
sm: 'sm',
md: 'sm',
lg: 'md',
xl: 'md',
@ -32,7 +32,7 @@ const CoverWall = ({
return (
<div
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`
gap: 13px;
`
@ -48,7 +48,7 @@ const CoverWall = ({
alt='Album Cover'
placeholder={null}
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'
)}
onClick={() => navigate(`/album/${album.id}`)}

View file

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

View file

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

View file

@ -1,9 +1,11 @@
import React, { useEffect, useState } from 'react'
import { css, cx } from '@emotion/css'
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 { ease } from '@/web/utils/const'
import TrafficLight from './TrafficLight'
import useIsMobile from '@/web/hooks/useIsMobile'
const tabs = [
{
@ -75,36 +77,68 @@ const TabName = () => {
)
}
const Sidebar = () => {
const Tabs = () => {
const navigate = useNavigate()
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 (
<div
className={cx(
'app-region-drag relative flex h-full w-full flex-col justify-center',
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'>
{tabs.map(tab => (
<NavLink key={tab.name} to={tab.path}>
<Icon
name={tab.icon}
className={cx(
'app-region-no-drag h-10 w-10 transition duration-500 active:scale-75',
location.pathname === tab.path
? 'text-brand-600 dark:text-brand-700'
: 'hover:text-black dark:hover:text-white'
)}
/>
</NavLink>
))}
</div>
<TabName />
{window.env?.isMac && (
<div className='fixed top-6 left-6 translate-y-0.5'>
<TrafficLight />
</div>
)}
<Tabs />
{!isMobile && <TabName />}
</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'>
<button>
<Icon
name='shuffle'
className='h-7 w-7 text-black/90 dark:text-white/40'
name='hide-list'
className='h-7 w-7 text-black/90 dark:text-white/40'
/>
</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) => (
<div key={`${index}-${line}`}>{line}</div>
))}

View file

@ -13,7 +13,7 @@ const Player = () => {
)}
>
<PlayingNext className='mb-3 h-full' />
<div className='pb-20'>
<div className='pb-6'>
<NowPlaying />
</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 Image from './Image'
import Wave from './Wave'
import Icon from '@/web/components/Icon'
const PlayingNext = ({ className }: { className?: string }) => {
const playerSnapshot = useSnapshot(player)
@ -15,22 +16,30 @@ const PlayingNext = ({ className }: { className?: string }) => {
<>
<div
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
className={cx(
'relative z-10 overflow-scroll',
'no-scrollbar relative z-10 overflow-scroll',
className,
css``,
css`
padding-top: 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 */}
<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}
</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(', ')}
</div>
</div>
@ -72,7 +88,7 @@ const PlayingNext = ({ className }: { className?: string }) => {
{playerSnapshot.trackIndex === index ? (
<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')}
</div>
)}

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import { useMemo } from 'react'
import { player } from '@/web/store'
import { useSnapshot } from 'valtio'
import Wave from './Wave'
import Icon from '@/web/components/Icon'
const TrackList = ({
tracks,
@ -35,21 +36,31 @@ const TrackList = ({
<div
key={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='flex-grow'>{track.name}</div>
<div className='h-10 w-10'></div>
<div className='flex flex-grow items-center'>
{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'>
{formatDuration(track.dt, 'en', 'hh:mm:ss')}
</div>
{/* The wave icon */}
{playingTrack?.id === track.id && playing && (
<div className='absolute -left-7'>
<Wave playing={playing} />
</div>
)}
</div>
))}
</div>

View file

@ -86,8 +86,8 @@ const TrackListHeader = ({
onClick={() => onPlay()}
className='h-[72px] w-[170px] rounded-full dark:bg-brand-700'
></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-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-white/10'></button>
</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