feat: updates

This commit is contained in:
qier222 2022-07-11 11:06:41 +08:00
parent 0b4baa3eff
commit 222fb02355
No known key found for this signature in database
GPG key ID: 9C85007ED905F14D
77 changed files with 654 additions and 551 deletions

View 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

View file

@ -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'

View file

@ -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>
)
}

View file

@ -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 />

View file

@ -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' />

View file

@ -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}

View file

@ -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
}
},

View file

@ -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>
)
}

View file

@ -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>

View file

@ -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>

View file

@ -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 = ({

View file

@ -12,7 +12,7 @@ const Player = () => {
`
)}
>
<PlayingNext className='mb-3 h-full' />
<PlayingNext className='h-full' />
<div className='pb-6'>
<NowPlaying />
</div>

View file

@ -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>

View file

@ -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>
</>
)
}

View file

@ -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>
)
}

View file

@ -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 = () => {

View file

@ -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'

View file

@ -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>

View file

@ -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'

View file

@ -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>
)}