mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-16 21:28:06 +00:00
feat: 增加一些新组件
This commit is contained in:
parent
0520af8466
commit
e9d82dd792
17 changed files with 124 additions and 256 deletions
|
|
@ -1,13 +1,10 @@
|
|||
import React from 'react'
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react'
|
||||
import { NowPlaying } from './NowPlaying'
|
||||
import NowPlaying from './NowPlaying'
|
||||
|
||||
export default {
|
||||
title: 'NowPlaying',
|
||||
component: NowPlaying,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
parameters: {
|
||||
viewport: {
|
||||
defaultViewport: 'iphone8p',
|
||||
|
|
@ -18,7 +15,3 @@ export default {
|
|||
const Template: ComponentStory<typeof NowPlaying> = args => <NowPlaying />
|
||||
|
||||
export const Primary = Template.bind({})
|
||||
Primary.args = {
|
||||
primary: true,
|
||||
label: 'NowPlaying',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import cx from 'classnames'
|
||||
import SvgIcon from './SvgIcon'
|
||||
|
||||
export const NowPlaying = () => {
|
||||
const NowPlaying = () => {
|
||||
return (
|
||||
<div className='relative flex aspect-square w-full flex-col justify-end overflow-hidden rounded-3xl'>
|
||||
{/* Cover */}
|
||||
|
|
@ -38,10 +38,12 @@ export const NowPlaying = () => {
|
|||
|
||||
{/* Controls */}
|
||||
<div className='mt-4 flex w-full items-center justify-between'>
|
||||
<SvgIcon
|
||||
name='shuffle'
|
||||
className='h-7 w-7 text-black/90 dark:text-white/40'
|
||||
/>
|
||||
<button>
|
||||
<SvgIcon
|
||||
name='shuffle'
|
||||
className='h-7 w-7 text-black/90 dark:text-white/40'
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div className='text-black/95 dark:text-white/80'>
|
||||
<button className='rounded-full bg-black/10 p-[10px] dark:bg-white/10'>
|
||||
|
|
@ -55,12 +57,16 @@ export const NowPlaying = () => {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<SvgIcon
|
||||
name='repeat-1'
|
||||
className='h-7 w-7 text-black/90 dark:text-white/40'
|
||||
/>
|
||||
<button>
|
||||
<SvgIcon
|
||||
name='repeat-1'
|
||||
className='h-7 w-7 text-black/90 dark:text-white/40'
|
||||
/>{' '}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NowPlaying
|
||||
|
|
|
|||
16
packages/web/components/Sidebar.stories.tsx
Normal file
16
packages/web/components/Sidebar.stories.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react'
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react'
|
||||
import Sidebar from './Sidebar'
|
||||
|
||||
export default {
|
||||
title: 'Sidebar',
|
||||
component: Sidebar,
|
||||
} as ComponentMeta<typeof Sidebar>
|
||||
|
||||
const Template: ComponentStory<typeof Sidebar> = args => (
|
||||
<div className='h-[calc(100vh_-_32px)] w-min rounded-l-3xl bg-[#F8F8F8] dark:bg-black'>
|
||||
<Sidebar />
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Primary = Template.bind({})
|
||||
|
|
@ -1,107 +1,29 @@
|
|||
import { NavLink } from 'react-router-dom'
|
||||
import SvgIcon from './SvgIcon'
|
||||
import useUserPlaylists from '@/web/hooks/useUserPlaylists'
|
||||
import { scrollToTop } from '@/web/utils/common'
|
||||
import { prefetchPlaylist } from '@/web/hooks/usePlaylist'
|
||||
import { player } from '@/web/store'
|
||||
import { Mode, TrackListSourceType } from '@/web/utils/player'
|
||||
import React from 'react'
|
||||
import cx from 'classnames'
|
||||
import { useMemo } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
const primaryTabs = [
|
||||
{
|
||||
name: '主页',
|
||||
icon: 'home',
|
||||
route: '/',
|
||||
},
|
||||
{
|
||||
name: '播客',
|
||||
icon: 'podcast',
|
||||
route: '/podcast',
|
||||
},
|
||||
{
|
||||
name: '音乐库',
|
||||
icon: 'music-library',
|
||||
route: '/library',
|
||||
},
|
||||
] as const
|
||||
|
||||
const PrimaryTabs = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className={cx(window.env?.isMac && 'app-region-drag', 'h-14')}></div>
|
||||
{primaryTabs.map(tab => (
|
||||
<NavLink
|
||||
onClick={() => scrollToTop()}
|
||||
key={tab.route}
|
||||
to={tab.route}
|
||||
className={({ isActive }) =>
|
||||
cx(
|
||||
'btn-hover-animation mx-3 flex cursor-default items-center rounded-lg px-3 py-2 transition-colors duration-200 after:scale-[0.97] after:bg-black/[.06] dark:after:bg-white/20',
|
||||
!isActive && 'text-gray-700 dark:text-white',
|
||||
isActive && 'text-brand-500 '
|
||||
)
|
||||
}
|
||||
>
|
||||
<SvgIcon className='mr-3 h-6 w-6' name={tab.icon} />
|
||||
<span className='font-semibold'>{tab.name}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
|
||||
<div className='mx-5 my-2 h-px bg-black opacity-5 dark:bg-white dark:opacity-10'></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Playlists = () => {
|
||||
const { data: playlists } = useUserPlaylists()
|
||||
const playerSnapshot = useSnapshot(player)
|
||||
const currentPlaylistID = useMemo(
|
||||
() => playerSnapshot.trackListSource?.id,
|
||||
[playerSnapshot.trackListSource]
|
||||
)
|
||||
const playlistMode = useMemo(
|
||||
() => playerSnapshot.trackListSource?.type,
|
||||
[playerSnapshot.trackListSource]
|
||||
)
|
||||
const mode = useMemo(() => playerSnapshot.mode, [playerSnapshot.mode])
|
||||
|
||||
return (
|
||||
<div className='mb-16 overflow-auto pb-2'>
|
||||
{playlists?.playlist?.map(playlist => (
|
||||
<NavLink
|
||||
onMouseOver={() => prefetchPlaylist({ id: playlist.id })}
|
||||
key={playlist.id}
|
||||
onClick={() => scrollToTop()}
|
||||
to={`/playlist/${playlist.id}`}
|
||||
className={({ isActive }: { isActive: boolean }) =>
|
||||
cx(
|
||||
'btn-hover-animation line-clamp-1 my-px mx-3 flex cursor-default items-center justify-between rounded-lg px-3 py-[0.38rem] text-sm text-black opacity-70 transition-colors duration-200 after:scale-[0.97] after:bg-black/[.06] dark:text-white dark:after:bg-white/20',
|
||||
isActive && 'after:scale-100 after:opacity-100'
|
||||
)
|
||||
}
|
||||
>
|
||||
<span className='line-clamp-1'>{playlist.name}</span>
|
||||
{playlistMode === TrackListSourceType.Playlist &&
|
||||
mode === Mode.TrackList &&
|
||||
currentPlaylistID === playlist.id && (
|
||||
<SvgIcon className='h-5 w-5' name='volume-half' />
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import SvgIcon from './SvgIcon'
|
||||
|
||||
const Sidebar = () => {
|
||||
return (
|
||||
<div
|
||||
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'
|
||||
>
|
||||
<PrimaryTabs />
|
||||
<Playlists />
|
||||
<div className='relative flex h-full w-[104px] flex-col justify-center'>
|
||||
<div className='grid grid-cols-1 justify-items-center gap-12 text-black/10 dark:text-white/20'>
|
||||
<SvgIcon
|
||||
name='my'
|
||||
className='h-10 w-10 text-brand-600 dark:text-brand-700'
|
||||
/>
|
||||
<SvgIcon name='explore' className='h-10 w-10' />
|
||||
<SvgIcon name='discovery' className='h-10 w-10' />
|
||||
<SvgIcon name='lyrics' className='h-10 w-10' />
|
||||
</div>
|
||||
<div
|
||||
className='absolute bottom-8 right-0 left-0 flex rotate-180 items-center font-medium text-brand-600 dark:text-brand-700'
|
||||
style={{
|
||||
writingMode: 'vertical-rl',
|
||||
textOrientation: 'mixed',
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
<span>USER PAGE</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
16
packages/web/components/Topbar.stories.tsx
Normal file
16
packages/web/components/Topbar.stories.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react'
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react'
|
||||
import Topbar from './Topbar'
|
||||
|
||||
export default {
|
||||
title: 'Topbar',
|
||||
component: Topbar,
|
||||
} as ComponentMeta<typeof Topbar>
|
||||
|
||||
const Template: ComponentStory<typeof Topbar> = args => (
|
||||
<div className='w-[calc(100vw_-_32px)] rounded-3xl bg-[#F8F8F8] px-11 dark:bg-black'>
|
||||
<Topbar />
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Primary = Template.bind({})
|
||||
|
|
@ -1,114 +1,44 @@
|
|||
import SvgIcon from '@/web/components/SvgIcon'
|
||||
import useScroll from '@/web/hooks/useScroll'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import Avatar from './Avatar'
|
||||
import cx from 'classnames'
|
||||
|
||||
const NavigationButtons = () => {
|
||||
const navigate = useNavigate()
|
||||
enum ACTION {
|
||||
Back = 'back',
|
||||
Forward = 'forward',
|
||||
}
|
||||
const handleNavigate = (action: ACTION) => {
|
||||
if (action === ACTION.Back) navigate(-1)
|
||||
if (action === ACTION.Forward) navigate(1)
|
||||
}
|
||||
return (
|
||||
<div className='flex gap-1'>
|
||||
{[ACTION.Back, ACTION.Forward].map(action => (
|
||||
<div
|
||||
onClick={() => handleNavigate(action)}
|
||||
key={action}
|
||||
className='app-region-no-drag btn-hover-animation rounded-lg p-2 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={action} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SearchBox = () => {
|
||||
const { type } = useParams()
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const navigate = useNavigate()
|
||||
const toSearch = (e: React.KeyboardEvent) => {
|
||||
if (!keywords) return
|
||||
if (e.key === 'Enter') {
|
||||
navigate(`/search/${keywords}${type ? `/${type}` : ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='app-region-no-drag group flex w-[16rem] cursor-text items-center rounded-full bg-gray-500 bg-opacity-5 pl-2.5 pr-2 transition duration-300 hover:bg-opacity-10 dark:bg-gray-300 dark:bg-opacity-5'>
|
||||
<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'
|
||||
name='search'
|
||||
/>
|
||||
<input
|
||||
value={keywords}
|
||||
onChange={e => setKeywords(e.target.value)}
|
||||
onKeyDown={toSearch}
|
||||
type='text'
|
||||
className='flex-grow bg-transparent placeholder:text-gray-500 dark:text-white dark:placeholder:text-gray-400'
|
||||
placeholder='搜索'
|
||||
/>
|
||||
<div
|
||||
onClick={() => setKeywords('')}
|
||||
className={cx(
|
||||
'cursor-default rounded-full p-1 text-gray-600 transition hover:bg-gray-400/20 dark:text-white/50 dark:hover:bg-white/20',
|
||||
!keywords && 'hidden'
|
||||
)}
|
||||
>
|
||||
<SvgIcon className='h-4 w-4' name='x' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Settings = () => {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<div
|
||||
onClick={() => navigate('/settings')}
|
||||
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-[1.125rem] w-[1.125rem]' name='settings' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import SvgIcon from './SvgIcon'
|
||||
|
||||
const Topbar = () => {
|
||||
/**
|
||||
* Show topbar background when scroll down
|
||||
*/
|
||||
const [mainContainer, setMainContainer] = useState<HTMLElement | null>(null)
|
||||
const scroll = useScroll(mainContainer, { throttle: 100 })
|
||||
|
||||
useEffect(() => {
|
||||
setMainContainer(document.getElementById('mainContainer'))
|
||||
}, [setMainContainer])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'sticky z-30 flex h-16 min-h-[4rem] w-full cursor-default items-center justify-between px-8 transition duration-300',
|
||||
window.env?.isMac && 'app-region-drag',
|
||||
window.env?.isEnableTitlebar ? 'top-8' : 'top-0',
|
||||
!scroll.arrivedState.top &&
|
||||
'bg-white bg-opacity-[.86] backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]'
|
||||
)}
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<NavigationButtons />
|
||||
<SearchBox />
|
||||
<div className='flex w-full items-center justify-between pt-11 pb-10'>
|
||||
{/* Left Part */}
|
||||
<div className='flex items-center'>
|
||||
{/* Navigation Buttons */}
|
||||
<button className='rounded-full bg-[#E9E9E9] p-[10px] dark:bg-[#0E0E0E]'>
|
||||
<SvgIcon name='back' className='h-7 w-7 text-[#717171]' />
|
||||
</button>
|
||||
<button className='ml-[10px] rounded-full bg-[#E9E9E9] p-[10px] dark:bg-[#0E0E0E]'>
|
||||
<SvgIcon name='forward' className='h-7 w-7 text-[#717171]' />
|
||||
</button>
|
||||
|
||||
{/* Dividing line */}
|
||||
<div className='mx-6 h-4 w-px bg-black/20 dark:bg-white/20'></div>
|
||||
|
||||
{/* Search Box */}
|
||||
<div className='flex min-w-[284px] items-center rounded-full bg-[#E9E9E9] p-[10px] text-[#717171] dark:bg-[#0E0E0E]'>
|
||||
<SvgIcon name='search' className='mr-[10px] h-7 w-7' />
|
||||
<input
|
||||
placeholder='Search Song Name'
|
||||
className='bg-transparent placeholder:text-[#717171]'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-3'>
|
||||
<Settings />
|
||||
<Avatar />
|
||||
{/* Right Part */}
|
||||
<div className='flex'>
|
||||
<button className='rounded-full bg-[#E9E9E9] p-[10px] dark:bg-[#0E0E0E]'>
|
||||
<SvgIcon name='placeholder' className='h-7 w-7 text-[#717171]' />
|
||||
</button>
|
||||
|
||||
{/* Avatar */}
|
||||
<div>
|
||||
<img
|
||||
className='ml-3 h-12 w-12 rounded-full'
|
||||
src='http://p1.music.126.net/AetIV1GOZiLKk1yy8PMPfw==/109951165378042240.jpg'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue