mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-16 13:17:46 +00:00
feat: updates
This commit is contained in:
parent
d09c5fd171
commit
133881d287
28 changed files with 389 additions and 115 deletions
|
|
@ -107,10 +107,8 @@ class Main {
|
|||
height: this.store.get('window.height'),
|
||||
minWidth: 1240,
|
||||
minHeight: 848,
|
||||
// vibrancy: 'fullscreen-ui',
|
||||
titleBarStyle: 'customButtonsOnHover',
|
||||
titleBarStyle: isMac ? 'customButtonsOnHover' : 'hidden',
|
||||
trafficLightPosition: { x: 24, y: 24 },
|
||||
// frame: !(isWindows || isLinux), // TODO: 适用于linux下独立的启用开关
|
||||
frame: false,
|
||||
transparent: true,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,17 @@ import IpcRendererReact from '@/web/IpcRendererReact'
|
|||
import Layout from '@/web/components/New/Layout'
|
||||
import Devtool from '@/web/components/New/Devtool'
|
||||
import ErrorBoundary from '@/web/components/New/ErrorBoundary'
|
||||
import useIsMobile from '@/web/hooks/useIsMobile'
|
||||
import LayoutMobile from '@/web/components/New/LayoutMobile'
|
||||
|
||||
const App = () => {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div className='dark'>
|
||||
{window.env?.isEnableTitlebar && <TitleBar />}
|
||||
<Layout />
|
||||
{isMobile ? <LayoutMobile /> : <Layout />}
|
||||
<Toaster position='bottom-center' containerStyle={{ bottom: '5rem' }} />
|
||||
<IpcRendererReact />
|
||||
<Devtool />
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export interface FetchPersonalFMResponse {
|
|||
}
|
||||
export function fetchPersonalFM(): Promise<FetchPersonalFMResponse> {
|
||||
return request({
|
||||
url: window.ipcRenderer ? '/personal/fm' : '/personal_fm',
|
||||
url: window.env?.isElectron ? '/personal/fm' : '/personal_fm',
|
||||
method: 'get',
|
||||
params: {
|
||||
timestamp: Date.now(),
|
||||
|
|
|
|||
3
packages/web/assets/icons/hide-list.svg
Normal file
3
packages/web/assets/icons/hide-list.svg
Normal 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 |
|
|
@ -2,40 +2,78 @@ 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'>
|
||||
<div className='text-center'>
|
||||
<Image
|
||||
alt={artist.name}
|
||||
src={resizeImage(artist.img1v1Url, 'md')}
|
||||
className='aspect-square rounded-full'
|
||||
className={cx(
|
||||
'aspect-square rounded-full',
|
||||
css`
|
||||
min-width: 96px;
|
||||
min-height: 96px;
|
||||
`
|
||||
)}
|
||||
/>
|
||||
<div className='line-clamp-1 mt-2.5 text-14 font-bold text-neutral-700 dark:text-neutral-600'>
|
||||
<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>
|
||||
))}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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}`)}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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 />}
|
||||
|
|
|
|||
37
packages/web/components/New/LayoutMobile.tsx
Normal file
37
packages/web/components/New/LayoutMobile.tsx
Normal 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
|
||||
|
|
@ -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;
|
||||
}
|
||||
`
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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={cx(
|
||||
'app-region-drag relative flex h-full w-full flex-col justify-center',
|
||||
css`
|
||||
grid-area: sidebar;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<div className='grid grid-cols-1 justify-items-center gap-12 text-black/10 dark:text-white/20'>
|
||||
<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 => (
|
||||
<NavLink key={tab.name} to={tab.path}>
|
||||
<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 duration-500 active:scale-75',
|
||||
'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'
|
||||
)}
|
||||
/>
|
||||
</NavLink>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
<TabName />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sidebar
|
||||
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: menubar;
|
||||
`
|
||||
)}
|
||||
>
|
||||
{window.env?.isMac && (
|
||||
<div className='fixed top-6 left-6 translate-y-0.5'>
|
||||
<TrafficLight />
|
||||
</div>
|
||||
)}
|
||||
<Tabs />
|
||||
{!isMobile && <TabName />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuBar
|
||||
|
|
@ -122,7 +122,7 @@ const NowPlaying = () => {
|
|||
<div className='mt-4 flex w-full items-center justify-between'>
|
||||
<button>
|
||||
<Icon
|
||||
name='shuffle'
|
||||
name='hide-list'
|
||||
className='h-7 w-7 text-black/90 dark:text-white/40'
|
||||
/>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const Player = () => {
|
|||
)}
|
||||
>
|
||||
<PlayingNext className='mb-3 h-full' />
|
||||
<div className='pb-20'>
|
||||
<div className='pb-6'>
|
||||
<NowPlaying />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
95
packages/web/components/New/PlayerMobile.tsx
Normal file
95
packages/web/components/New/PlayerMobile.tsx
Normal 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
|
||||
|
|
@ -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'
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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,22 +36,32 @@ 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='text-right'>
|
||||
{formatDuration(track.dt, 'en', 'hh:mm:ss')}
|
||||
</div>
|
||||
|
||||
{/* The wave icon */}
|
||||
<div className='flex flex-grow items-center'>
|
||||
{track.name}
|
||||
{playingTrack?.id === track.id && playing && (
|
||||
<div className='absolute -left-7'>
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
13
packages/web/components/New/TrafficLight.tsx
Normal file
13
packages/web/components/New/TrafficLight.tsx
Normal 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
|
||||
8
packages/web/hooks/useIsMobile.ts
Normal file
8
packages/web/hooks/useIsMobile.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import useBreakpoint from './useBreakpoint'
|
||||
|
||||
const useIsMobile = () => {
|
||||
const breakpoint = useBreakpoint()
|
||||
return ['sm', 'md'].includes(breakpoint)
|
||||
}
|
||||
|
||||
export default useIsMobile
|
||||
|
|
@ -66,12 +66,12 @@ const My = () => {
|
|||
<PageTransition>
|
||||
<div className='grid grid-cols-1 gap-10'>
|
||||
<PlayLikedSongsCard />
|
||||
<div>
|
||||
|
||||
<ArtistRow
|
||||
artists={recentListenedArtists?.map(a => a.artist)}
|
||||
placeholderRow={1}
|
||||
title='RECENTLY LISTENED'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Tabs
|
||||
|
|
@ -79,7 +79,13 @@ const My = () => {
|
|||
value={selectedTab}
|
||||
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>
|
||||
</PageTransition>
|
||||
|
|
|
|||
|
|
@ -130,3 +130,7 @@ a {
|
|||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ module.exports = {
|
|||
600: '#0E0E0E',
|
||||
700: '#060606',
|
||||
800: '#020202',
|
||||
900: '#313131',
|
||||
},
|
||||
neutral: {
|
||||
100: '#E3E3E3',
|
||||
|
|
|
|||
|
|
@ -250,7 +250,9 @@ export class Player {
|
|||
}
|
||||
if (this.trackID !== id) return
|
||||
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({
|
||||
src: [url],
|
||||
format: ['mp3', 'flac', 'webm'],
|
||||
|
|
@ -285,7 +287,7 @@ export class Player {
|
|||
|
||||
private _cacheAudio(audio: string) {
|
||||
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
|
||||
cacheAudio(id, audio)
|
||||
}
|
||||
|
|
@ -490,7 +492,7 @@ export class Player {
|
|||
async playTrack(trackID: TrackID) {
|
||||
this._setStateToLoading()
|
||||
const index = this.trackList.findIndex(t => t === trackID)
|
||||
if (!index) toast('播放失败,歌曲不在列表内')
|
||||
if (index === -1) toast('播放失败,歌曲不在列表内')
|
||||
this._trackIndex = index
|
||||
this._playTrack()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue