mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-17 13:48:02 +00:00
feat: updates
This commit is contained in:
parent
133881d287
commit
d4d8dd817b
22 changed files with 667 additions and 172 deletions
|
|
@ -45,10 +45,10 @@ const ArtistRow = ({
|
|||
|
||||
{/* 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'>
|
||||
<div className='no-scrollbar -ml-2.5 flex w-screen snap-x 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'
|
||||
className='mr-5 snap-start first-of-type:ml-2.5 last-of-type:mr-2.5 lg:mr-0'
|
||||
key={artist.id}
|
||||
>
|
||||
<Artist artist={artist} key={artist.id} />
|
||||
|
|
@ -65,8 +65,14 @@ const ArtistRow = ({
|
|||
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'>
|
||||
<div
|
||||
className='aspect-square w-full rounded-full bg-white dark:bg-neutral-800'
|
||||
style={{
|
||||
minHeight: '96px',
|
||||
minWidth: '96px',
|
||||
}}
|
||||
/>
|
||||
<div className='mt-2.5 text-12 font-medium text-transparent lg:text-16 lg:font-bold'>
|
||||
PLACE
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ const CoverWall = ({
|
|||
'2xl': 'md',
|
||||
},
|
||||
large: {
|
||||
sm: 'sm',
|
||||
md: 'sm',
|
||||
sm: 'md',
|
||||
md: 'md',
|
||||
lg: 'md',
|
||||
xl: 'md',
|
||||
'2xl': 'lg',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Main from '@/web/components/New/Main'
|
||||
import Player from '@/web/components/New/Player'
|
||||
import MenuBar from '@/web/components/New/MenuBar'
|
||||
import Topbar from '@/web/components/New/Topbar'
|
||||
import Topbar from '@/web/components/New/Topbar/TopbarDesktop'
|
||||
import { css, cx } from '@emotion/css'
|
||||
import { useMemo } from 'react'
|
||||
import { player } from '@/web/store'
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { player } from '@/web/store'
|
|||
import { useSnapshot } from 'valtio'
|
||||
import Router from '@/web/components/New/Router'
|
||||
import MenuBar from './MenuBar'
|
||||
import TopbarMobile from './Topbar/TopbarMobile'
|
||||
import { isIOS, isPWA, isSafari } from '@/web/utils/common'
|
||||
|
||||
const LayoutMobile = () => {
|
||||
const playerSnapshot = useSnapshot(player)
|
||||
|
|
@ -13,21 +15,36 @@ const LayoutMobile = () => {
|
|||
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'>
|
||||
<TopbarMobile />
|
||||
<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'>
|
||||
<div
|
||||
className={cx(
|
||||
'fixed bottom-0 left-0 right-0 pt-3 dark:bg-black',
|
||||
css`
|
||||
padding-bottom: calc(
|
||||
${isIOS && isSafari && isPWA
|
||||
? '24px'
|
||||
: 'env(safe-area-inset-bottom)'} + 0.75rem
|
||||
);
|
||||
`
|
||||
)}
|
||||
>
|
||||
{showPlayer && (
|
||||
<div
|
||||
className={cx(
|
||||
'absolute left-7 right-7',
|
||||
css`
|
||||
top: calc(
|
||||
-100% - 6px + ${isIOS && isSafari && isPWA ? '24px' : 'env(safe-area-inset-bottom)'}
|
||||
);
|
||||
`
|
||||
)}
|
||||
>
|
||||
<Player />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MenuBar />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -79,8 +79,8 @@ const TabName = () => {
|
|||
|
||||
const Tabs = () => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const controls = useAnimation()
|
||||
const [active, setActive] = useState<string>(tabs[0].path)
|
||||
|
||||
const animate = async (path: string) => {
|
||||
await controls.start((p: string) =>
|
||||
|
|
@ -97,7 +97,10 @@ const Tabs = () => {
|
|||
animate={controls}
|
||||
transition={{ ease, duration: 0.18 }}
|
||||
onMouseDown={() => animate(tab.path)}
|
||||
onClick={() => navigate(tab.path)}
|
||||
onClick={() => {
|
||||
setActive(tab.path)
|
||||
navigate(tab.path)
|
||||
}}
|
||||
custom={tab.path}
|
||||
variants={{
|
||||
scale: { scale: 0.8 },
|
||||
|
|
@ -108,9 +111,9 @@ const Tabs = () => {
|
|||
name={tab.icon}
|
||||
className={cx(
|
||||
'app-region-no-drag h-10 w-10 transition-colors duration-500',
|
||||
location.pathname === tab.path
|
||||
active === tab.path
|
||||
? 'text-brand-600 dark:text-brand-700'
|
||||
: 'hover:text-black dark:hover:text-white'
|
||||
: 'lg:hover:text-black lg:dark:hover:text-white'
|
||||
)}
|
||||
/>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { ease } from '@/web/utils/const'
|
||||
import useIsMobile from '@/web/hooks/useIsMobile'
|
||||
|
||||
const PageTransition = ({
|
||||
children,
|
||||
|
|
@ -8,6 +9,11 @@ const PageTransition = ({
|
|||
children: React.ReactNode
|
||||
disableEnterAnimation?: boolean
|
||||
}) => {
|
||||
const isMobile = useIsMobile()
|
||||
if (isMobile) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: disableEnterAnimation ? 1 : 0 }}
|
||||
|
|
|
|||
|
|
@ -6,26 +6,33 @@ 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'
|
||||
import { useLockBodyScroll } from 'react-use'
|
||||
import { useState } from 'react'
|
||||
|
||||
const PlayerMobile = () => {
|
||||
const playerSnapshot = useSnapshot(player)
|
||||
const bgColor = useCoverColor(playerSnapshot.track?.al?.picUrl ?? '')
|
||||
const [locked, setLocked] = useState(false)
|
||||
|
||||
useLockBodyScroll(locked)
|
||||
|
||||
const x = useMotionValue(0)
|
||||
const onDragEnd = (
|
||||
event: MouseEvent | TouchEvent | PointerEvent,
|
||||
info: PanInfo
|
||||
) => {
|
||||
console.log(JSON.stringify(info))
|
||||
const x = info.offset.x
|
||||
const offset = 100
|
||||
if (x > offset) player.nextTrack()
|
||||
if (x < -offset) player.prevTrack()
|
||||
if (x > offset) player.prevTrack()
|
||||
if (x < -offset) player.nextTrack()
|
||||
setLocked(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'relative flex h-16 w-full items-center rounded-20 py-2.5 px-3',
|
||||
'relative flex h-16 w-full items-center rounded-20 px-3',
|
||||
css`
|
||||
background-color: ${bgColor.to};
|
||||
`
|
||||
|
|
@ -40,21 +47,32 @@ const PlayerMobile = () => {
|
|||
)}
|
||||
></div>
|
||||
|
||||
<Image
|
||||
src={resizeImage(playerSnapshot.track?.al.picUrl || '', 'sm')}
|
||||
alt='Cover'
|
||||
className='z-10 aspect-square h-full rounded-lg'
|
||||
/>
|
||||
<div className='h-full py-2.5'>
|
||||
<Image
|
||||
src={resizeImage(playerSnapshot.track?.al.picUrl || '', 'sm')}
|
||||
alt='Cover'
|
||||
className='z-10 aspect-square h-full rounded-lg'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='relative flex-grow overflow-hidden px-3'>
|
||||
<div className='relative flex h-full flex-grow items-center overflow-hidden px-3'>
|
||||
<motion.div
|
||||
drag='x'
|
||||
style={{ x }}
|
||||
dragConstraints={{ left: 0, right: 0 }}
|
||||
onDragStart={() => setLocked(true)}
|
||||
onDragEnd={onDragEnd}
|
||||
className='line-clamp-1 text-14 font-bold text-white'
|
||||
className=' flex h-full flex-grow items-center '
|
||||
>
|
||||
{playerSnapshot.track?.name}
|
||||
<div className='flex-shrink-0'>
|
||||
<div className='line-clamp-1 text-14 font-bold text-white'>
|
||||
{playerSnapshot.track?.name}
|
||||
</div>
|
||||
<div className='line-clamp-1 mt-1 text-12 font-bold text-white/60'>
|
||||
{playerSnapshot.track?.ar?.map(a => a.name).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-full flex-grow'></div>
|
||||
</motion.div>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react'
|
||||
import Topbar from './Topbar'
|
||||
import Topbar from './Topbar/TopbarDesktop'
|
||||
|
||||
export default {
|
||||
title: 'Components/Topbar',
|
||||
|
|
|
|||
|
|
@ -1,125 +0,0 @@
|
|||
import { css, cx } from '@emotion/css'
|
||||
import { motion, useAnimation } from 'framer-motion'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { ease } from '@/web/utils/const'
|
||||
import Icon from '../Icon'
|
||||
import { resizeImage } from '@/web/utils/common'
|
||||
import useUser from '@/web/api/hooks/useUser'
|
||||
|
||||
const NavigationButtons = () => {
|
||||
const navigate = useNavigate()
|
||||
const controlsBack = useAnimation()
|
||||
const controlsForward = useAnimation()
|
||||
const transition = { duration: 0.18, ease }
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
onMouseDown={async () => {
|
||||
await controlsBack.start({ x: -5 })
|
||||
await controlsBack.start({ x: 0 })
|
||||
}}
|
||||
className='app-region-no-drag rounded-full bg-day-600 p-2.5 dark:bg-night-600'
|
||||
>
|
||||
<motion.div animate={controlsBack} transition={transition}>
|
||||
<Icon name='back' className='h-7 w-7 text-neutral-500' />
|
||||
</motion.div>
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
navigate(1)
|
||||
}}
|
||||
onMouseDown={async () => {
|
||||
await controlsForward.start({ x: 5 })
|
||||
await controlsForward.start({ x: 0 })
|
||||
}}
|
||||
className='app-region-no-drag ml-2.5 rounded-full bg-day-600 p-2.5 dark:bg-night-600'
|
||||
>
|
||||
<motion.div animate={controlsForward} transition={transition}>
|
||||
<Icon name='forward' className='h-7 w-7 text-neutral-500' />
|
||||
</motion.div>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Avatar = ({ className }: { className?: string }) => {
|
||||
const navigate = useNavigate()
|
||||
const { data: user } = useUser()
|
||||
|
||||
const avatarUrl = user?.profile?.avatarUrl
|
||||
? resizeImage(user?.profile?.avatarUrl ?? '', 'sm')
|
||||
: ''
|
||||
|
||||
return (
|
||||
<>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
onClick={() => navigate('/login')}
|
||||
className={cx(
|
||||
'app-region-no-drag rounded-full',
|
||||
className || 'h-12 w-12'
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => navigate('/login')}
|
||||
className={cx(
|
||||
'rounded-full bg-day-600 p-2.5 dark:bg-night-600',
|
||||
className || 'h-12 w-12'
|
||||
)}
|
||||
>
|
||||
<Icon name='user' className='h-7 w-7 text-neutral-500' />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Topbar = () => {
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'app-region-drag fixed top-0 right-0 z-20 flex items-center justify-between overflow-hidden rounded-tr-24 pt-11 pb-10 pr-6 pl-10 ',
|
||||
css`
|
||||
left: 104px;
|
||||
`,
|
||||
!location.pathname.startsWith('/album/') &&
|
||||
!location.pathname.startsWith('/playlist/') &&
|
||||
'bg-gradient-to-b from-white dark:from-black'
|
||||
)}
|
||||
>
|
||||
{/* Left Part */}
|
||||
<div className='flex items-center'>
|
||||
<NavigationButtons />
|
||||
|
||||
{/* Dividing line */}
|
||||
<div className='mx-6 h-4 w-px bg-black/20 dark:bg-white/20'></div>
|
||||
|
||||
{/* Search Box */}
|
||||
<div className='app-region-no-drag flex min-w-[284px] items-center rounded-full bg-day-600 p-2.5 text-neutral-500 dark:bg-night-600'>
|
||||
<Icon name='search' className='mr-2.5 h-7 w-7' />
|
||||
<input
|
||||
placeholder='Artist, songs and more'
|
||||
className='bg-transparent font-medium placeholder:text-neutral-500 dark:text-neutral-200'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Part */}
|
||||
<div className='flex'>
|
||||
<button className='app-region-no-drag rounded-full bg-day-600 p-2.5 dark:bg-night-600'>
|
||||
<Icon name='placeholder' className='h-7 w-7 text-neutral-500' />
|
||||
</button>
|
||||
|
||||
<Avatar className='ml-3 h-12 w-12' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Topbar
|
||||
41
packages/web/components/New/Topbar/Avatar.tsx
Normal file
41
packages/web/components/New/Topbar/Avatar.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { css, cx } from '@emotion/css'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import Icon from '../../Icon'
|
||||
import { resizeImage } from '@/web/utils/common'
|
||||
import useUser from '@/web/api/hooks/useUser'
|
||||
|
||||
const Avatar = ({ className }: { className?: string }) => {
|
||||
const navigate = useNavigate()
|
||||
const { data: user } = useUser()
|
||||
|
||||
const avatarUrl = user?.profile?.avatarUrl
|
||||
? resizeImage(user?.profile?.avatarUrl ?? '', 'sm')
|
||||
: ''
|
||||
|
||||
return (
|
||||
<>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
onClick={() => navigate('/login')}
|
||||
className={cx(
|
||||
'app-region-no-drag rounded-full',
|
||||
className || 'h-12 w-12'
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => navigate('/login')}
|
||||
className={cx(
|
||||
'rounded-full bg-day-600 p-2.5 dark:bg-night-600',
|
||||
className || 'h-12 w-12'
|
||||
)}
|
||||
>
|
||||
<Icon name='user' className='h-7 w-7 text-neutral-500' />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Avatar
|
||||
45
packages/web/components/New/Topbar/NavigationButtons.tsx
Normal file
45
packages/web/components/New/Topbar/NavigationButtons.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { css, cx } from '@emotion/css'
|
||||
import { motion, useAnimation } from 'framer-motion'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { ease } from '@/web/utils/const'
|
||||
import Icon from '../../Icon'
|
||||
|
||||
const NavigationButtons = () => {
|
||||
const navigate = useNavigate()
|
||||
const controlsBack = useAnimation()
|
||||
const controlsForward = useAnimation()
|
||||
const transition = { duration: 0.18, ease }
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
onMouseDown={async () => {
|
||||
await controlsBack.start({ x: -5 })
|
||||
await controlsBack.start({ x: 0 })
|
||||
}}
|
||||
className='app-region-no-drag rounded-full bg-day-600 p-2.5 dark:bg-night-600'
|
||||
>
|
||||
<motion.div animate={controlsBack} transition={transition}>
|
||||
<Icon name='back' className='h-7 w-7 text-neutral-500' />
|
||||
</motion.div>
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
navigate(1)
|
||||
}}
|
||||
onMouseDown={async () => {
|
||||
await controlsForward.start({ x: 5 })
|
||||
await controlsForward.start({ x: 0 })
|
||||
}}
|
||||
className='app-region-no-drag ml-2.5 rounded-full bg-day-600 p-2.5 dark:bg-night-600'
|
||||
>
|
||||
<motion.div animate={controlsForward} transition={transition}>
|
||||
<Icon name='forward' className='h-7 w-7 text-neutral-500' />
|
||||
</motion.div>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavigationButtons
|
||||
16
packages/web/components/New/Topbar/SearchBox.tsx
Normal file
16
packages/web/components/New/Topbar/SearchBox.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { css, cx } from '@emotion/css'
|
||||
import Icon from '../../Icon'
|
||||
|
||||
const SearchBox = () => {
|
||||
return (
|
||||
<div className='app-region-no-drag flex items-center rounded-full bg-day-600 p-2.5 text-neutral-500 dark:bg-night-600 lg:min-w-[284px]'>
|
||||
<Icon name='search' className='mr-2.5 h-7 w-7' />
|
||||
<input
|
||||
placeholder='Artist, songs and more'
|
||||
className='bg-transparent font-medium placeholder:text-neutral-500 dark:text-neutral-200'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchBox
|
||||
11
packages/web/components/New/Topbar/SettingsButton.tsx
Normal file
11
packages/web/components/New/Topbar/SettingsButton.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import Icon from '@/web/components/Icon'
|
||||
|
||||
const SettingsButton = () => {
|
||||
return (
|
||||
<button className='app-region-no-drag rounded-full bg-day-600 p-2.5 dark:bg-night-600'>
|
||||
<Icon name='placeholder' className='h-7 w-7 text-neutral-500' />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsButton
|
||||
42
packages/web/components/New/Topbar/TopbarDesktop.tsx
Normal file
42
packages/web/components/New/Topbar/TopbarDesktop.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { css, cx } from '@emotion/css'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import Avatar from './Avatar'
|
||||
import SearchBox from './SearchBox'
|
||||
import SettingsButton from './SettingsButton'
|
||||
import NavigationButtons from './NavigationButtons'
|
||||
|
||||
const TopbarDesktop = () => {
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'app-region-drag fixed top-0 right-0 z-20 flex items-center justify-between overflow-hidden rounded-tr-24 pt-11 pb-10 pr-6 pl-10 ',
|
||||
css`
|
||||
left: 104px;
|
||||
`,
|
||||
!location.pathname.startsWith('/album/') &&
|
||||
!location.pathname.startsWith('/playlist/') &&
|
||||
'bg-gradient-to-b from-white dark:from-black'
|
||||
)}
|
||||
>
|
||||
{/* Left Part */}
|
||||
<div className='flex items-center'>
|
||||
<NavigationButtons />
|
||||
|
||||
{/* Dividing line */}
|
||||
<div className='mx-6 h-4 w-px bg-black/20 dark:bg-white/20'></div>
|
||||
|
||||
<SearchBox />
|
||||
</div>
|
||||
|
||||
{/* Right Part */}
|
||||
<div className='flex'>
|
||||
<SettingsButton />
|
||||
<Avatar className='ml-3 h-12 w-12' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopbarDesktop
|
||||
22
packages/web/components/New/Topbar/TopbarMobile.tsx
Normal file
22
packages/web/components/New/Topbar/TopbarMobile.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import Avatar from './Avatar'
|
||||
import SearchBox from './SearchBox'
|
||||
import SettingsButton from './SettingsButton'
|
||||
|
||||
const TopbarMobile = () => {
|
||||
return (
|
||||
<div className='mb-5 mt-7 flex'>
|
||||
<div className='flex-grow'>
|
||||
<SearchBox />
|
||||
</div>
|
||||
|
||||
<div className='ml-6 flex'>
|
||||
<SettingsButton />
|
||||
<div className='ml-3'>
|
||||
<Avatar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopbarMobile
|
||||
|
|
@ -41,7 +41,7 @@ const TrackList = ({
|
|||
<div className='mr-6'>{String(track.no).padStart(2, '0')}</div>
|
||||
<div className='flex flex-grow items-center'>
|
||||
{track.name}
|
||||
{playingTrack?.id === track.id && playing && (
|
||||
{playingTrack?.id === track.id && (
|
||||
<div className='ml-4'>
|
||||
<Wave playing={playing} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"scripts": {
|
||||
"dev": "vite dev --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite build && vite preview --host",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
|
|
@ -82,6 +83,7 @@
|
|||
"tailwindcss": "^3.0.24",
|
||||
"typescript": "*",
|
||||
"vite": "^2.9.6",
|
||||
"vite-plugin-pwa": "^0.12.0",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vitest": "^0.12.10"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,22 +44,23 @@ const getAlbumsFromAPI = async () => {
|
|||
)
|
||||
)) as FetchPlaylistResponse[]
|
||||
|
||||
const ids: number[] = []
|
||||
let ids: number[] = []
|
||||
playlists.forEach(playlist =>
|
||||
playlist?.playlist?.trackIds?.forEach(t => ids.push(t.id))
|
||||
)
|
||||
if (!ids.length) {
|
||||
return []
|
||||
}
|
||||
ids = sampleSize(ids, 100)
|
||||
|
||||
const tracks = await fetchTracksWithReactQuery({ ids })
|
||||
if (!tracks.songs.length) {
|
||||
if (!tracks?.songs?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 从歌单中抽出歌曲
|
||||
const pickedIds: number[] = []
|
||||
let albums: DiscoverAlbum[] = []
|
||||
const albums: DiscoverAlbum[] = []
|
||||
tracks.songs.forEach(t => {
|
||||
if (pickedIds.includes(t.al.id)) return
|
||||
pickedIds.push(t.al.id)
|
||||
|
|
@ -71,7 +72,6 @@ const getAlbumsFromAPI = async () => {
|
|||
})
|
||||
|
||||
// 挑选出大图
|
||||
albums = sampleSize(albums, 100)
|
||||
const largeCover = sampleSize([...Array(100).keys()], ~~(100 / 3))
|
||||
albums.map((album, index) => (album.large = largeCover.includes(index)))
|
||||
|
||||
|
|
|
|||
|
|
@ -129,8 +129,13 @@ a {
|
|||
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: black;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,3 +158,9 @@ export async function calcCoverColor(coverUrl: string) {
|
|||
return c.toHex()
|
||||
})
|
||||
}
|
||||
|
||||
export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
|
||||
export const isSafari = /Safari/.test(navigator.userAgent)
|
||||
export const isPWA =
|
||||
(navigator as any).standalone ||
|
||||
window.matchMedia('(display-mode: standalone)').matches
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path, { join } from 'path'
|
|||
import { defineConfig } from 'vite'
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
dotenv.config({ path: path.resolve(process.cwd(), '../../.env') })
|
||||
const IS_ELECTRON = process.env.IS_ELECTRON
|
||||
|
|
@ -20,6 +21,36 @@ export default defineConfig({
|
|||
plugins: [
|
||||
react(),
|
||||
|
||||
/**
|
||||
* @see https://vite-plugin-pwa.netlify.app/guide/generate.html
|
||||
*/
|
||||
VitePWA({
|
||||
manifest: {
|
||||
name: 'YesPlayMusic',
|
||||
short_name: 'YPM',
|
||||
description: 'Description of your app',
|
||||
theme_color: '#000',
|
||||
icons: [
|
||||
{
|
||||
src: 'pwa-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'pwa-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'pwa-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* @see https://github.com/vbenjs/vite-plugin-svg-icons
|
||||
*/
|
||||
|
|
@ -54,7 +85,7 @@ export default defineConfig({
|
|||
strictPort: IS_ELECTRON ? true : false,
|
||||
proxy: {
|
||||
'/netease/': {
|
||||
target: `http://127.0.0.1:${
|
||||
target: `http://192.168.2.111:${
|
||||
process.env.ELECTRON_DEV_NETEASE_API_PORT || 3000
|
||||
}`,
|
||||
changeOrigin: true,
|
||||
|
|
@ -68,6 +99,9 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
port: Number(process.env['ELECTRON_WEB_SERVER_PORT'] || 42710),
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue