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
0b4baa3eff
commit
222fb02355
77 changed files with 654 additions and 551 deletions
38
packages/web/components/New/BlurBackground.tsx
Normal file
38
packages/web/components/New/BlurBackground.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { resizeImage } from '@/web/utils/common'
|
||||
import { cx, css } from '@emotion/css'
|
||||
import useIsMobile from '@/web/hooks/useIsMobile'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import uiStates from '@/web/states/uiStates'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ease } from '@/web/utils/const'
|
||||
|
||||
const BlurBackground = ({ cover }: { cover?: string }) => {
|
||||
const isMobile = useIsMobile()
|
||||
const { hideTopbarBackground } = useSnapshot(uiStates)
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{!isMobile && cover && hideTopbarBackground && (
|
||||
<motion.img
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ ease }}
|
||||
className={cx(
|
||||
'absolute z-0 object-cover opacity-70',
|
||||
css`
|
||||
top: -400px;
|
||||
left: -370px;
|
||||
width: 1572px;
|
||||
height: 528px;
|
||||
filter: blur(256px) saturate(1.2);
|
||||
`
|
||||
)}
|
||||
src={resizeImage(cover, 'sm')}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlurBackground
|
||||
|
|
@ -46,8 +46,6 @@ const CoverWall = ({
|
|||
sizes[album.large ? 'large' : 'small'][breakpoint]
|
||||
)}
|
||||
key={album.id}
|
||||
alt='Album Cover'
|
||||
placeholder={null}
|
||||
className={cx(
|
||||
'aspect-square h-full w-full rounded-20 lg:rounded-24',
|
||||
album.large && 'col-span-2 row-span-2'
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ import Player from '@/web/components/New/Player'
|
|||
import MenuBar from '@/web/components/New/MenuBar'
|
||||
import Topbar from '@/web/components/New/Topbar/TopbarDesktop'
|
||||
import { css, cx } from '@emotion/css'
|
||||
import { player } from '@/web/store'
|
||||
import player from '@/web/states/player'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import Login from './Login'
|
||||
import TrafficLight from './TrafficLight'
|
||||
|
||||
const Layout = () => {
|
||||
const playerSnapshot = useSnapshot(player)
|
||||
|
|
@ -39,6 +40,12 @@ const Layout = () => {
|
|||
<Main />
|
||||
<Login />
|
||||
{showPlayer && <Player />}
|
||||
|
||||
{window.env?.isMac && (
|
||||
<div className='fixed top-6 left-6 z-30 translate-y-0.5'>
|
||||
<TrafficLight />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Player from '@/web/components/New/PlayerMobile'
|
||||
import { css, cx } from '@emotion/css'
|
||||
import { useMemo } from 'react'
|
||||
import { player } from '@/web/store'
|
||||
import player from '@/web/states/player'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import Router from '@/web/components/New/Router'
|
||||
import MenuBar from './MenuBar'
|
||||
|
|
@ -17,8 +17,8 @@ const LayoutMobile = () => {
|
|||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<div id='layout' className='bg-white select-none pb-28 dark:bg-black'>
|
||||
<main className='min-h-screen pb-16 overflow-x-hidden overflow-y-auto'>
|
||||
<div id='layout' className='select-none bg-white pb-28 dark:bg-black'>
|
||||
<main className='min-h-screen overflow-y-auto overflow-x-hidden pb-16 '>
|
||||
{location.pathname === '/' && <Topbar />}
|
||||
<Router />
|
||||
</main>
|
||||
|
|
@ -48,7 +48,7 @@ const LayoutMobile = () => {
|
|||
)}
|
||||
|
||||
<MenuBar />
|
||||
{/* <PlayingNext /> */}
|
||||
<PlayingNext />
|
||||
</div>
|
||||
|
||||
<Login />
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { cx, css } from '@emotion/css'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { state } from '@/web/store'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
import uiStates from '@/web/states/uiStates'
|
||||
import { AnimatePresence, motion, useAnimation } from 'framer-motion'
|
||||
import { ease } from '@/web/utils/const'
|
||||
import Icon from '@/web/components/Icon'
|
||||
import LoginWithPhoneOrEmail from './LoginWithPhoneOrEmail'
|
||||
import LoginWithQRCode from './LoginWithQRCode'
|
||||
import persistedUiStates from '@/web/states/persistedUiStates'
|
||||
|
||||
const OR = ({
|
||||
children,
|
||||
|
|
@ -37,9 +37,10 @@ const OR = ({
|
|||
}
|
||||
|
||||
const Login = () => {
|
||||
const { uiStates, persistedUiStates } = useSnapshot(state)
|
||||
const { loginType } = useSnapshot(persistedUiStates)
|
||||
const { showLoginPanel } = useSnapshot(uiStates)
|
||||
const [cardType, setCardType] = useState<'qrCode' | 'phone/email'>(
|
||||
persistedUiStates.loginType === 'qrCode' ? 'qrCode' : 'phone/email'
|
||||
loginType === 'qrCode' ? 'qrCode' : 'phone/email'
|
||||
)
|
||||
|
||||
const animateCard = useAnimation()
|
||||
|
|
@ -52,8 +53,7 @@ const Login = () => {
|
|||
})
|
||||
|
||||
setCardType(cardType === 'qrCode' ? 'phone/email' : 'qrCode')
|
||||
state.persistedUiStates.loginType =
|
||||
cardType === 'qrCode' ? 'phone' : 'qrCode'
|
||||
persistedUiStates.loginType = cardType === 'qrCode' ? 'phone' : 'qrCode'
|
||||
|
||||
await animateCard.start({
|
||||
rotateY: 0,
|
||||
|
|
@ -66,7 +66,7 @@ const Login = () => {
|
|||
<>
|
||||
{/* Blur bg */}
|
||||
<AnimatePresence>
|
||||
{uiStates.showLoginPanel && (
|
||||
{showLoginPanel && (
|
||||
<motion.div
|
||||
className='fixed inset-0 z-30 bg-black/80 backdrop-blur-3xl lg:rounded-24'
|
||||
initial={{ opacity: 0 }}
|
||||
|
|
@ -79,7 +79,7 @@ const Login = () => {
|
|||
|
||||
{/* Content */}
|
||||
<AnimatePresence>
|
||||
{uiStates.showLoginPanel && (
|
||||
{showLoginPanel && (
|
||||
<div className='fixed inset-0 z-30 flex justify-center rounded-24 pt-56'>
|
||||
<motion.div
|
||||
className='flex flex-col items-center'
|
||||
|
|
@ -134,7 +134,7 @@ const Login = () => {
|
|||
<motion.div
|
||||
layout='position'
|
||||
transition={{ ease }}
|
||||
onClick={() => (state.uiStates.showLoginPanel = false)}
|
||||
onClick={() => (uiStates.showLoginPanel = false)}
|
||||
className='mt-10 flex h-14 w-14 items-center justify-center rounded-full bg-white/10'
|
||||
>
|
||||
<Icon name='x' className='h-7 w-7 text-white/50' />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { cx, css } from '@emotion/css'
|
||||
import { useState } from 'react'
|
||||
import { state } from '@/web/store'
|
||||
import { useMutation } from 'react-query'
|
||||
import { loginWithEmail, loginWithPhone } from '@/web/api/auth'
|
||||
import md5 from 'md5'
|
||||
|
|
@ -9,17 +8,20 @@ import { setCookies } from '@/web/utils/cookie'
|
|||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ease } from '@/web/utils/const'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import uiStates from '@/web/states/uiStates'
|
||||
import persistedUiStates from '@/web/states/persistedUiStates'
|
||||
|
||||
const LoginWithPhoneOrEmail = () => {
|
||||
const { persistedUiStates } = useSnapshot(state)
|
||||
const { loginPhoneCountryCode, loginType: persistedLoginType } =
|
||||
useSnapshot(persistedUiStates)
|
||||
const [email, setEmail] = useState<string>('')
|
||||
const [countryCode, setCountryCode] = useState<string>(
|
||||
persistedUiStates.loginPhoneCountryCode || '+86'
|
||||
loginPhoneCountryCode || '+86'
|
||||
)
|
||||
const [phone, setPhone] = useState<string>('')
|
||||
const [password, setPassword] = useState<string>('')
|
||||
const [loginType, setLoginType] = useState<'phone' | 'email'>(
|
||||
persistedUiStates.loginType === 'email' ? 'email' : 'phone'
|
||||
persistedLoginType === 'email' ? 'email' : 'phone'
|
||||
)
|
||||
|
||||
const doEmailLogin = useMutation(
|
||||
|
|
@ -36,7 +38,7 @@ const LoginWithPhoneOrEmail = () => {
|
|||
}
|
||||
setCookies(result.cookie)
|
||||
|
||||
state.uiStates.showLoginPanel = false
|
||||
uiStates.showLoginPanel = false
|
||||
},
|
||||
onError: error => {
|
||||
toast(`Login failed: ${error}`)
|
||||
|
|
@ -80,7 +82,7 @@ const LoginWithPhoneOrEmail = () => {
|
|||
return
|
||||
}
|
||||
setCookies(result.cookie)
|
||||
state.uiStates.showLoginPanel = false
|
||||
uiStates.showLoginPanel = false
|
||||
},
|
||||
onError: error => {
|
||||
toast(`Login failed: ${error}`)
|
||||
|
|
@ -132,7 +134,7 @@ const LoginWithPhoneOrEmail = () => {
|
|||
onClick={() => {
|
||||
const type = loginType === 'phone' ? 'email' : 'phone'
|
||||
setLoginType(type)
|
||||
state.persistedUiStates.loginType = type
|
||||
persistedUiStates.loginType = type
|
||||
}}
|
||||
>
|
||||
Phone
|
||||
|
|
@ -165,7 +167,7 @@ const LoginWithPhoneOrEmail = () => {
|
|||
<input
|
||||
onChange={e => {
|
||||
setCountryCode(e.target.value)
|
||||
state.persistedUiStates.loginPhoneCountryCode = e.target.value
|
||||
persistedUiStates.loginPhoneCountryCode = e.target.value
|
||||
}}
|
||||
className={cx(
|
||||
'my-3.5 flex-shrink-0 bg-transparent',
|
||||
|
|
@ -181,7 +183,7 @@ const LoginWithPhoneOrEmail = () => {
|
|||
onChange={e => setPhone(e.target.value)}
|
||||
className='my-3.5 flex-grow appearance-none bg-transparent'
|
||||
placeholder='Phone'
|
||||
type='number'
|
||||
type='tel'
|
||||
value={phone}
|
||||
/>
|
||||
</motion.div>
|
||||
|
|
@ -198,10 +200,11 @@ const LoginWithPhoneOrEmail = () => {
|
|||
initial='hidden'
|
||||
animate='show'
|
||||
exit='hidden'
|
||||
className='w-full'
|
||||
>
|
||||
<input
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
className='flex-grow appearance-none bg-transparent'
|
||||
className='w-full flex-grow appearance-none bg-transparent'
|
||||
placeholder='Email'
|
||||
type='email'
|
||||
value={email}
|
||||
|
|
@ -215,7 +218,7 @@ const LoginWithPhoneOrEmail = () => {
|
|||
<div className='mt-4 flex items-center rounded-12 bg-black/50 p-3 text-16 font-medium text-night-50'>
|
||||
<input
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
className='bg-transparent'
|
||||
className='w-full bg-transparent'
|
||||
placeholder='Password'
|
||||
type='password'
|
||||
value={password}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,11 @@
|
|||
import { cx, css } from '@emotion/css'
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import qrCode from 'qrcode'
|
||||
import { state } from '@/web/store'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useMutation, useQuery } from 'react-query'
|
||||
import {
|
||||
checkLoginQrCodeStatus,
|
||||
fetchLoginQrCodeKey,
|
||||
loginWithEmail,
|
||||
loginWithPhone,
|
||||
} from '@/web/api/auth'
|
||||
import md5 from 'md5'
|
||||
import { useQuery } from 'react-query'
|
||||
import { checkLoginQrCodeStatus, fetchLoginQrCodeKey } from '@/web/api/auth'
|
||||
import toast from 'react-hot-toast'
|
||||
import { setCookies } from '@/web/utils/cookie'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ease } from '@/web/utils/const'
|
||||
import Icon from '@/web/components/Icon'
|
||||
import uiStates from '@/web/states/uiStates'
|
||||
|
||||
const QRCode = ({ className, text }: { className?: string; text: string }) => {
|
||||
const [image, setImage] = useState<string>('')
|
||||
|
|
@ -90,7 +80,7 @@ const LoginWithQRCode = () => {
|
|||
break
|
||||
}
|
||||
setCookies(status.cookie)
|
||||
state.uiStates.showLoginPanel = false
|
||||
uiStates.showLoginPanel = false
|
||||
break
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,18 +1,37 @@
|
|||
import { css, cx } from '@emotion/css'
|
||||
import Router from './Router'
|
||||
import useIntersectionObserver from '@/web/hooks/useIntersectionObserver'
|
||||
import uiStates from '@/web/states/uiStates'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
const Main = () => {
|
||||
// Show/hide topbar background
|
||||
const observePoint = useRef<HTMLDivElement | null>(null)
|
||||
const { onScreen } = useIntersectionObserver(observePoint)
|
||||
useEffect(() => {
|
||||
uiStates.hideTopbarBackground = onScreen
|
||||
return () => {
|
||||
uiStates.hideTopbarBackground = false
|
||||
}
|
||||
}, [onScreen])
|
||||
|
||||
return (
|
||||
<main
|
||||
className={cx(
|
||||
'no-scrollbar overflow-y-auto pb-16 pr-6 pl-10',
|
||||
css`
|
||||
padding-top: 132px;
|
||||
grid-area: main;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<Router />
|
||||
<div ref={observePoint}></div>
|
||||
<div
|
||||
className={css`
|
||||
margin-top: 132px;
|
||||
`}
|
||||
>
|
||||
<Router />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,11 +144,6 @@ const MenuBar = ({ className }: { className?: string }) => {
|
|||
`
|
||||
)}
|
||||
>
|
||||
{window.env?.isMac && (
|
||||
<div className='fixed top-6 left-6 translate-y-0.5'>
|
||||
<TrafficLight />
|
||||
</div>
|
||||
)}
|
||||
<Tabs />
|
||||
{!isMobile && <TabName />}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|||
import { css, cx } from '@emotion/css'
|
||||
import Icon from '../Icon'
|
||||
import { formatDuration, resizeImage } from '@/web/utils/common'
|
||||
import { player } from '@/web/store'
|
||||
import player from '@/web/states/player'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { State as PlayerState, Mode as PlayerMode } from '@/web/utils/player'
|
||||
import Slider from './Slider'
|
||||
|
|
@ -17,7 +17,7 @@ const Progress = () => {
|
|||
const { track, progress } = useSnapshot(player)
|
||||
|
||||
return (
|
||||
<div className='flex flex-col w-full mt-10'>
|
||||
<div className='mt-10 flex w-full flex-col'>
|
||||
<Slider
|
||||
min={0}
|
||||
max={(track?.dt ?? 100000) / 1000}
|
||||
|
|
@ -28,7 +28,7 @@ const Progress = () => {
|
|||
onlyCallOnChangeAfterDragEnded={true}
|
||||
/>
|
||||
|
||||
<div className='flex justify-between mt-1 font-bold text-14 text-black/20 dark:text-white/20'>
|
||||
<div className='mt-1 flex justify-between text-14 font-bold text-black/20 dark:text-white/20'>
|
||||
<span>{formatDuration(progress * 1000, 'en', 'hh:mm:ss')}</span>
|
||||
<span>{formatDuration(track?.dt || 0, 'en', 'hh:mm:ss')}</span>
|
||||
</div>
|
||||
|
|
@ -117,9 +117,9 @@ const NowPlaying = () => {
|
|||
<Cover />
|
||||
|
||||
{/* Info & Controls */}
|
||||
<div className='flex flex-col items-center p-5 m-3 font-medium rounded-20 bg-white/60 backdrop-blur-3xl dark:bg-black/70'>
|
||||
<div className='m-3 flex flex-col items-center rounded-20 bg-white/60 p-5 font-medium backdrop-blur-3xl dark:bg-black/70'>
|
||||
{/* Track Info */}
|
||||
<div className='text-lg text-black line-clamp-1 dark:text-white'>
|
||||
<div className='line-clamp-1 text-lg text-black dark:text-white'>
|
||||
{track?.name}
|
||||
</div>
|
||||
<ArtistInline
|
||||
|
|
@ -129,13 +129,13 @@ const NowPlaying = () => {
|
|||
/>
|
||||
|
||||
{/* Dividing line */}
|
||||
<div className='w-2/3 h-px mt-2 bg-black/10 dark:bg-white/10'></div>
|
||||
<div className='mt-2 h-px w-2/3 bg-black/10 dark:bg-white/10'></div>
|
||||
|
||||
{/* Progress */}
|
||||
<Progress />
|
||||
|
||||
{/* Controls */}
|
||||
<div className='flex items-center justify-between w-full mt-4'>
|
||||
<div className='mt-4 flex w-full items-center justify-between'>
|
||||
<button>
|
||||
<Icon
|
||||
name='hide-list'
|
||||
|
|
@ -149,7 +149,7 @@ const NowPlaying = () => {
|
|||
disabled={!track}
|
||||
className='rounded-full bg-black/10 p-2.5 dark:bg-white/10'
|
||||
>
|
||||
<Icon name='previous' className='w-6 h-6 ' />
|
||||
<Icon name='previous' className='h-6 w-6 ' />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => track && player.playOrPause()}
|
||||
|
|
@ -161,7 +161,7 @@ const NowPlaying = () => {
|
|||
? 'pause'
|
||||
: 'play'
|
||||
}
|
||||
className='w-6 h-6 '
|
||||
className='h-6 w-6 '
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -169,7 +169,7 @@ const NowPlaying = () => {
|
|||
disabled={!track}
|
||||
className='rounded-full bg-black/10 p-2.5 dark:bg-white/10'
|
||||
>
|
||||
<Icon name='next' className='w-6 h-6 ' />
|
||||
<Icon name='next' className='h-6 w-6 ' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { ease } from '@/web/utils/const'
|
||||
import useIsMobile from '@/web/hooks/useIsMobile'
|
||||
import scrollPositions from '@/web/store/scrollPositions'
|
||||
import scrollPositions from '@/web/states/scrollPositions'
|
||||
import { useLayoutEffect } from 'react'
|
||||
|
||||
const PageTransition = ({
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const Player = () => {
|
|||
`
|
||||
)}
|
||||
>
|
||||
<PlayingNext className='mb-3 h-full' />
|
||||
<PlayingNext className='h-full' />
|
||||
<div className='pb-6'>
|
||||
<NowPlaying />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import { player } from '@/web/store'
|
||||
import player from '@/web/states/player'
|
||||
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'
|
||||
import { motion, PanInfo } from 'framer-motion'
|
||||
import { useLockBodyScroll } from 'react-use'
|
||||
import { useState } from 'react'
|
||||
import useUserLikedTracksIDs, {
|
||||
useMutationLikeATrack,
|
||||
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
||||
import PlayingNextMobile from './PlayingNextMobile'
|
||||
import uiStates from '@/web/states/uiStates'
|
||||
import { ease } from '@/web/utils/const'
|
||||
|
||||
const LikeButton = () => {
|
||||
const { track } = useSnapshot(player)
|
||||
|
|
@ -23,7 +24,7 @@ const LikeButton = () => {
|
|||
|
||||
return (
|
||||
<button
|
||||
className='flex items-center h-full'
|
||||
className='flex h-full items-center'
|
||||
onClick={() => track?.id && likeATrack.mutateAsync(track.id)}
|
||||
>
|
||||
<Icon
|
||||
|
|
@ -39,6 +40,7 @@ const PlayerMobile = () => {
|
|||
const bgColor = useCoverColor(track?.al?.picUrl ?? '')
|
||||
const [locked, setLocked] = useState(false)
|
||||
useLockBodyScroll(locked)
|
||||
const { mobileShowPlayingNext } = useSnapshot(uiStates)
|
||||
|
||||
const onDragEnd = (
|
||||
event: MouseEvent | TouchEvent | PointerEvent,
|
||||
|
|
@ -63,33 +65,56 @@ const PlayerMobile = () => {
|
|||
`
|
||||
)}
|
||||
>
|
||||
{/* Cover */}
|
||||
{/* Handler */}
|
||||
{!mobileShowPlayingNext && (
|
||||
<motion.div
|
||||
onClick={() => {
|
||||
uiStates.mobileShowPlayingNext = true
|
||||
}}
|
||||
className={cx(
|
||||
'absolute right-0 left-0 flex justify-center',
|
||||
css`
|
||||
--height: 20px;
|
||||
height: var(--height);
|
||||
top: calc(var(--height) * -1);
|
||||
`
|
||||
)}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ ease, duration: 0.2 }}
|
||||
>
|
||||
<Icon name='player-handler' className='h-2.5 text-brand-700' />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Cover */}
|
||||
<div className='h-full py-2.5'>
|
||||
<Image
|
||||
src={resizeImage(track?.al.picUrl || '', 'sm')}
|
||||
className='z-10 h-full rounded-lg aspect-square'
|
||||
className='z-10 aspect-square h-full rounded-lg'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Track info */}
|
||||
<div className='relative flex items-center flex-grow h-full px-3 overflow-hidden'>
|
||||
<div className='relative flex h-full flex-grow items-center overflow-hidden px-3'>
|
||||
<motion.div
|
||||
drag='x'
|
||||
dragConstraints={{ left: 0, right: 0 }}
|
||||
onDragStart={() => setLocked(true)}
|
||||
onDragEnd={onDragEnd}
|
||||
className='flex items-center flex-grow h-full '
|
||||
dragDirectionLock={true}
|
||||
className='flex h-full flex-grow items-center '
|
||||
>
|
||||
<div className='flex-shrink-0'>
|
||||
<div className='font-bold text-white line-clamp-1 text-14'>
|
||||
<div className='line-clamp-1 text-14 font-bold text-white'>
|
||||
{track?.name}
|
||||
</div>
|
||||
<div className='mt-1 font-bold line-clamp-1 text-12 text-white/60'>
|
||||
<div className='line-clamp-1 mt-1 text-12 font-bold text-white/60'>
|
||||
{track?.ar?.map(a => a.name).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-grow h-full'></div>
|
||||
<div className='h-full flex-grow'></div>
|
||||
</motion.div>
|
||||
|
||||
<div
|
||||
|
|
@ -120,7 +145,7 @@ const PlayerMobile = () => {
|
|||
>
|
||||
<Icon
|
||||
name={state === 'playing' ? 'pause' : 'play'}
|
||||
className='w-6 h-6 text-white/80'
|
||||
className='h-6 w-6 text-white/80'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,25 @@
|
|||
import { resizeImage } from '@/web/utils/common'
|
||||
import { player } from '@/web/store'
|
||||
import { isIosPwa, resizeImage } from '@/web/utils/common'
|
||||
import player from '@/web/states/player'
|
||||
import { State as PlayerState } from '@/web/utils/player'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import useTracks from '@/web/api/hooks/useTracks'
|
||||
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'
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { useRef } from 'react'
|
||||
import { useWindowSize } from 'react-use'
|
||||
import { playerWidth, topbarHeight } from '@/web/utils/const'
|
||||
import useIsMobile from '@/web/hooks/useIsMobile'
|
||||
import { Virtuoso } from 'react-virtuoso'
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'absolute top-0 left-0 z-10 flex w-full items-center justify-between px-7 pb-6 text-14 font-bold text-neutral-700 dark:text-neutral-300 lg:px-4'
|
||||
'absolute top-0 left-0 z-20 flex w-full items-center justify-between bg-contain bg-repeat-x px-7 pb-6 text-14 font-bold text-neutral-700 dark:text-neutral-300 lg:px-4'
|
||||
)}
|
||||
>
|
||||
<div className='flex'>
|
||||
<div className='w-1 h-4 mr-2 bg-brand-700'></div>
|
||||
<div className='mr-2 h-4 w-1 bg-brand-700'></div>
|
||||
PLAYING NEXT
|
||||
</div>
|
||||
<div className='flex'>
|
||||
|
|
@ -48,30 +46,21 @@ const Track = ({
|
|||
state: PlayerState
|
||||
}) => {
|
||||
return (
|
||||
<motion.div
|
||||
className='flex items-center justify-between'
|
||||
// initial={{ opacity: 0 }}
|
||||
// animate={{ opacity: 1 }}
|
||||
// exit={{ x: '100%', opacity: 0 }}
|
||||
// transition={{
|
||||
// duration: 0.24,
|
||||
// }}
|
||||
// layout
|
||||
<div
|
||||
className='mb-5 flex items-center justify-between'
|
||||
onClick={e => {
|
||||
if (e.detail === 2 && track?.id) player.playTrack(track.id)
|
||||
}}
|
||||
>
|
||||
{/* Cover */}
|
||||
<Image
|
||||
<img
|
||||
alt='Cover'
|
||||
className='flex-shrink-0 mr-4 aspect-square h-14 w-14 rounded-12'
|
||||
className='mr-4 aspect-square h-14 w-14 flex-shrink-0 rounded-12'
|
||||
src={resizeImage(track?.al?.picUrl || '', 'sm')}
|
||||
animation={false}
|
||||
placeholder={false}
|
||||
/>
|
||||
|
||||
{/* Track info */}
|
||||
<div className='flex-grow mr-3'>
|
||||
<div className='mr-3 flex-grow'>
|
||||
<div
|
||||
className={cx(
|
||||
'line-clamp-1 text-16 font-medium ',
|
||||
|
|
@ -82,7 +71,7 @@ const Track = ({
|
|||
>
|
||||
{track?.name}
|
||||
</div>
|
||||
<div className='mt-1 font-bold line-clamp-1 text-14 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>
|
||||
|
|
@ -91,11 +80,11 @@ const Track = ({
|
|||
{playingTrackIndex === index ? (
|
||||
<Wave playing={state === 'playing'} />
|
||||
) : (
|
||||
<div className='font-medium text-16 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>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -103,69 +92,57 @@ const TrackList = ({ className }: { className?: string }) => {
|
|||
const { trackList, trackIndex, state } = useSnapshot(player)
|
||||
const { data: tracksRaw } = useTracks({ ids: trackList })
|
||||
const tracks = tracksRaw?.songs || []
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
const { height } = useWindowSize()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const listHeight = height - topbarHeight - playerWidth - 24 - 20 // 24是封面与底部间距,20是list与封面间距
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: tracks.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 76,
|
||||
overscan: 10,
|
||||
})
|
||||
const listHeight = height - topbarHeight - playerWidth - 24 // 24是封面与底部间距
|
||||
const listHeightMobile = height - 154 - 110 - (isIosPwa ? 34 : 0) // 154是列表距离底部的距离,110是顶部的距离
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={parentRef}
|
||||
style={{
|
||||
height: `${listHeight}px`,
|
||||
}}
|
||||
className={cx(
|
||||
'no-scrollbar relative z-10 w-full overflow-auto',
|
||||
className,
|
||||
css`
|
||||
padding-top: 42px;
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
black 42px
|
||||
); // 顶部渐变遮罩
|
||||
`
|
||||
)}
|
||||
className={css`
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 22px,
|
||||
black 42px
|
||||
); // 顶部渐变遮罩
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className='relative w-full'
|
||||
<Virtuoso
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
height: `${isMobile ? listHeightMobile : listHeight}px`,
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((row: any) => (
|
||||
<div
|
||||
key={row.index}
|
||||
className='absolute top-0 left-0 w-full'
|
||||
style={{
|
||||
height: `${row.size}px`,
|
||||
transform: `translateY(${row.start}px)`,
|
||||
}}
|
||||
>
|
||||
<Track
|
||||
track={tracks?.[row.index]}
|
||||
index={row.index}
|
||||
playingTrackIndex={trackIndex}
|
||||
state={state}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
totalCount={tracks.length}
|
||||
className={cx(
|
||||
'no-scrollbar relative z-10 w-full overflow-auto',
|
||||
className,
|
||||
css`
|
||||
mask-image: linear-gradient(
|
||||
to top,
|
||||
transparent 8px,
|
||||
black 42px
|
||||
); // 底部渐变遮罩
|
||||
`
|
||||
)}
|
||||
fixedItemHeight={76}
|
||||
data={tracks}
|
||||
overscan={10}
|
||||
components={{
|
||||
Header: () => <div className='h-8'></div>,
|
||||
Footer: () => <div className='h-1'></div>,
|
||||
}}
|
||||
itemContent={(index, track) => (
|
||||
<Track
|
||||
key={index}
|
||||
track={track}
|
||||
index={index}
|
||||
playingTrackIndex={trackIndex}
|
||||
state={state}
|
||||
/>
|
||||
)}
|
||||
></Virtuoso>
|
||||
</div>
|
||||
|
||||
{/* 底部渐变遮罩 */}
|
||||
<div
|
||||
className='absolute left-0 right-0 z-20 hidden pointer-events-none h-14 bg-gradient-to-t from-black to-transparent lg:block'
|
||||
style={{ top: `${listHeight - 56}px` }}
|
||||
></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,72 +10,81 @@ import { useLockBodyScroll } from 'react-use'
|
|||
import { isIosPwa } from '@/web/utils/common'
|
||||
import PlayingNext from './PlayingNext'
|
||||
import { ease } from '@/web/utils/const'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import uiStates from '@/web/states/uiStates'
|
||||
import Icon from '@/web/components/Icon'
|
||||
|
||||
const PlayingNextMobile = () => {
|
||||
const [display, setDisplay] = useState(false)
|
||||
const { mobileShowPlayingNext: display } = useSnapshot(uiStates)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
useLockBodyScroll(isDragging || display)
|
||||
useLockBodyScroll(isDragging)
|
||||
|
||||
const dragControls = useDragControls()
|
||||
const y = useMotionValue('82%')
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
className='fixed inset-0 px-3 bg-black/80 backdrop-blur-3xl'
|
||||
exit={{
|
||||
y: '100%',
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
ease: 'easeOut',
|
||||
},
|
||||
}}
|
||||
animate={{ opacity: 1 }}
|
||||
initial={{ opacity: 0 }}
|
||||
style={{
|
||||
borderRadius: isDragging ? '24px' : '0px',
|
||||
y,
|
||||
}}
|
||||
drag='y'
|
||||
dragControls={dragControls}
|
||||
dragListener={false}
|
||||
dragConstraints={{ top: 0, bottom: 0 }}
|
||||
dragDirectionLock={true}
|
||||
onDrag={(event, info) => console.log(info.point.y)}
|
||||
>
|
||||
{/* Indictor */}
|
||||
{display && (
|
||||
<motion.div
|
||||
onPointerDown={e => {
|
||||
setIsDragging(true)
|
||||
dragControls.start(e)
|
||||
className='fixed inset-0 bg-black/80 backdrop-blur-3xl'
|
||||
exit={{
|
||||
y: '100%',
|
||||
borderRadius: '24px',
|
||||
transition: {
|
||||
ease: 'easeOut',
|
||||
duration: 0.4,
|
||||
},
|
||||
}}
|
||||
animate={{ y: 0, borderRadius: 0 }}
|
||||
initial={{ y: '100%', borderRadius: '24px' }}
|
||||
transition={{ duration: 0.6, ease }}
|
||||
dragControls={dragControls}
|
||||
dragListener={false}
|
||||
whileDrag={{
|
||||
borderRadius: '24px',
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
ease: 'linear',
|
||||
},
|
||||
}}
|
||||
onDragEnd={() => setIsDragging(false)}
|
||||
dragConstraints={{ top: 0, bottom: 0 }}
|
||||
className={cx(
|
||||
'mx-7 flex justify-center',
|
||||
css`
|
||||
--height: 30px;
|
||||
bottom: calc(
|
||||
70px + 64px +
|
||||
${isIosPwa ? '24px' : 'env(safe-area-inset-bottom)'}
|
||||
); // 拖动条到导航栏的距离 + 导航栏高度 + safe-area-inset-bottom
|
||||
height: var(--height);
|
||||
`
|
||||
)}
|
||||
layout
|
||||
dragDirectionLock={true}
|
||||
onDragEnd={(event, info) => {
|
||||
setIsDragging(false)
|
||||
const offset = info.offset.y
|
||||
if (offset > 150) {
|
||||
uiStates.mobileShowPlayingNext = false
|
||||
}
|
||||
}}
|
||||
drag='y'
|
||||
>
|
||||
{/* Indictor */}
|
||||
<motion.div
|
||||
className='mt-3.5 h-1.5 w-10 rounded-full bg-brand-700'
|
||||
layout
|
||||
style={{ width: isDragging || display ? '80px' : '40px' }}
|
||||
></motion.div>
|
||||
</motion.div>
|
||||
onPointerDown={e => {
|
||||
setIsDragging(true)
|
||||
dragControls.start(e)
|
||||
}}
|
||||
onClick={() => {
|
||||
uiStates.mobileShowPlayingNext = false
|
||||
}}
|
||||
className={cx(
|
||||
'flex flex-col justify-end',
|
||||
css`
|
||||
height: 108px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name='player-handler'
|
||||
className='mb-5 h-2.5 rotate-180 text-brand-700'
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* List */}
|
||||
<div className='relative'>
|
||||
<PlayingNext />
|
||||
</div>
|
||||
</motion.div>
|
||||
{/* List */}
|
||||
<div className='relative h-full px-7'>
|
||||
<PlayingNext />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useLayoutEffect } from 'react'
|
||||
import scrollPositions from '@/web/store/scrollPositions'
|
||||
import scrollPositions from '@/web/states/scrollPositions'
|
||||
import { throttle } from 'lodash-es'
|
||||
|
||||
const ScrollRestoration = () => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css'
|
|||
import Icon from '../../Icon'
|
||||
import { resizeImage } from '@/web/utils/common'
|
||||
import useUser from '@/web/api/hooks/useUser'
|
||||
import { state } from '@/web/store'
|
||||
import uiStates from '@/web/states/uiStates'
|
||||
|
||||
const Avatar = ({ className }: { className?: string }) => {
|
||||
const { data: user } = useUser()
|
||||
|
|
@ -16,7 +16,7 @@ const Avatar = ({ className }: { className?: string }) => {
|
|||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
onClick={() => (state.uiStates.showLoginPanel = true)}
|
||||
onClick={() => (uiStates.showLoginPanel = true)}
|
||||
className={cx(
|
||||
'app-region-no-drag rounded-full',
|
||||
className || 'h-12 w-12'
|
||||
|
|
@ -24,7 +24,7 @@ const Avatar = ({ className }: { className?: string }) => {
|
|||
/>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => (state.uiStates.showLoginPanel = true)}
|
||||
onClick={() => (uiStates.showLoginPanel = true)}
|
||||
className={cx(
|
||||
'rounded-full bg-day-600 p-2.5 dark:bg-night-600',
|
||||
className || 'h-12 w-12'
|
||||
|
|
|
|||
|
|
@ -1,32 +1,55 @@
|
|||
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'
|
||||
import topbarBackground from '@/web/assets/images/topbar-background.png'
|
||||
import uiStates from '@/web/states/uiStates'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ease } from '@/web/utils/const'
|
||||
|
||||
const Background = () => {
|
||||
const { hideTopbarBackground } = useSnapshot(uiStates)
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{!hideTopbarBackground && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ ease }}
|
||||
className={cx(
|
||||
'absolute inset-0 z-0 bg-contain bg-repeat-x',
|
||||
css`
|
||||
background-image: url(${topbarBackground});
|
||||
`
|
||||
)}
|
||||
></motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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 bg-contain pt-11 pb-10 pr-6 pl-10',
|
||||
'app-region-drag fixed top-0 left-0 right-0 z-20 flex items-center justify-between overflow-hidden bg-contain pt-11 pb-10 pr-6',
|
||||
css`
|
||||
left: 104px;
|
||||
padding-left: 144px;
|
||||
`,
|
||||
!location.pathname.startsWith('/album/') &&
|
||||
!location.pathname.startsWith('/playlist/') &&
|
||||
!location.pathname.startsWith('/browse') &&
|
||||
!location.pathname.startsWith('/artist') &&
|
||||
css`
|
||||
background-image: url(${topbarBackground});
|
||||
`
|
||||
window.env?.isElectron && 'rounded-t-24'
|
||||
)}
|
||||
>
|
||||
{/* Background */}
|
||||
<Background />
|
||||
|
||||
{/* Left Part */}
|
||||
<div className='flex items-center'>
|
||||
<div className='z-10 flex items-center'>
|
||||
<NavigationButtons />
|
||||
|
||||
{/* Dividing line */}
|
||||
|
|
@ -36,7 +59,7 @@ const TopbarDesktop = () => {
|
|||
</div>
|
||||
|
||||
{/* Right Part */}
|
||||
<div className='flex'>
|
||||
<div className='z-10 flex'>
|
||||
<SettingsButton />
|
||||
<Avatar className='ml-3 h-12 w-12' />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { formatDuration } from '@/web/utils/common'
|
||||
import { css, cx } from '@emotion/css'
|
||||
import { useMemo } from 'react'
|
||||
import { player } from '@/web/store'
|
||||
import player from '@/web/states/player'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import Wave from './Wave'
|
||||
import Icon from '@/web/components/Icon'
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { motion } from 'framer-motion'
|
|||
import { ease } from '@/web/utils/const'
|
||||
import { injectGlobal } from '@emotion/css'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import BlurBackground from '@/web/components/New/BlurBackground'
|
||||
|
||||
injectGlobal`
|
||||
.plyr__video-wrapper,
|
||||
|
|
@ -86,21 +87,7 @@ const Cover = memo(
|
|||
</div>
|
||||
|
||||
{/* Blur bg */}
|
||||
{!isMobile && (
|
||||
<img
|
||||
className={cx(
|
||||
'absolute z-0 object-cover opacity-70',
|
||||
css`
|
||||
top: -400px;
|
||||
left: -370px;
|
||||
width: 1572px;
|
||||
height: 528px;
|
||||
filter: blur(256px) saturate(1.2);
|
||||
`
|
||||
)}
|
||||
src={resizeImage(cover, 'sm')}
|
||||
/>
|
||||
)}
|
||||
<BlurBackground cover={cover} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -111,10 +98,12 @@ const TrackListHeader = ({
|
|||
album,
|
||||
playlist,
|
||||
onPlay,
|
||||
className,
|
||||
}: {
|
||||
album?: Album
|
||||
playlist?: Playlist
|
||||
onPlay: () => void
|
||||
className?: string
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useIsMobile()
|
||||
|
|
@ -126,6 +115,7 @@ const TrackListHeader = ({
|
|||
return (
|
||||
<div
|
||||
className={cx(
|
||||
className,
|
||||
'z-10 mx-2.5 rounded-48 p-8 dark:bg-white/10',
|
||||
'lg:mx-0 lg:grid lg:grid-rows-1 lg:gap-10 lg:rounded-none lg:p-0 lg:dark:bg-transparent',
|
||||
!isMobile &&
|
||||
|
|
@ -182,7 +172,7 @@ const TrackListHeader = ({
|
|||
|
||||
{/* Description */}
|
||||
{!isMobile && (
|
||||
<div className='line-clamp-3 mt-6 whitespace-pre-wrap text-14 font-bold dark:text-night-400 '>
|
||||
<div className='line-clamp-3 mt-6 whitespace-pre-wrap text-14 font-bold dark:text-night-400 '>
|
||||
{album?.description || playlist?.description}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue