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
d09c5fd171
commit
133881d287
28 changed files with 389 additions and 115 deletions
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
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 { 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 */}
|
|
||||||
{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
|
<Image
|
||||||
alt={artist.name}
|
alt={artist.name}
|
||||||
src={resizeImage(artist.img1v1Url, 'md')}
|
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}
|
{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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
|
|
|
||||||
|
|
@ -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}`)}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -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 />}
|
||||||
|
|
|
||||||
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 (
|
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;
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
return (
|
||||||
<div
|
<div className='grid grid-cols-4 justify-items-center text-black/10 dark:text-white/20 lg:grid-cols-1 lg:gap-12'>
|
||||||
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'>
|
|
||||||
{tabs.map(tab => (
|
{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
|
<Icon
|
||||||
name={tab.icon}
|
name={tab.icon}
|
||||||
className={cx(
|
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
|
location.pathname === tab.path
|
||||||
? 'text-brand-600 dark:text-brand-700'
|
? 'text-brand-600 dark:text-brand-700'
|
||||||
: 'hover:text-black dark:hover:text-white'
|
: 'hover:text-black dark:hover:text-white'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</NavLink>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</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'>
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
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 { 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'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<div className='flex'>
|
||||||
|
<div className='mr-2 h-4 w-1 bg-brand-700'></div>
|
||||||
PLAYING NEXT
|
PLAYING NEXT
|
||||||
</div>
|
</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
|
||||||
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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -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,22 +36,32 @@ 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}
|
||||||
<div className='text-right'>
|
|
||||||
{formatDuration(track.dt, 'en', 'hh:mm:ss')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* The wave icon */}
|
|
||||||
{playingTrack?.id === track.id && playing && (
|
{playingTrack?.id === track.id && playing && (
|
||||||
<div className='absolute -left-7'>
|
<div className='ml-4'>
|
||||||
<Wave playing={playing} />
|
<Wave playing={playing} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
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>
|
<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)}
|
||||||
|
placeholderRow={1}
|
||||||
title='RECENTLY LISTENED'
|
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>
|
||||||
|
|
|
||||||
|
|
@ -130,3 +130,7 @@ a {
|
||||||
* {
|
* {
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue