feat: updates

This commit is contained in:
qier222 2023-03-26 02:16:01 +08:00
parent ce757215a3
commit c1cd31840e
No known key found for this signature in database
86 changed files with 1048 additions and 778 deletions

View file

@ -96,6 +96,10 @@ const MenuItem = ({
`
)}
></div>
{/* 增加三角形避免斜着移动到submenu时意外关闭菜单 */}
<div className='absolute -right-8 -bottom-6 h-12 w-12 rotate-45'></div>
<div className='absolute -right-8 -top-6 h-12 w-12 rotate-45'></div>
</>
)}
</div>

View file

@ -73,13 +73,21 @@ const Playlist = ({ playlist }: { playlist: Playlist }) => {
}, [playlist.id])
return (
<Image
onClick={goTo}
key={playlist.id}
src={resizeImage(playlist.coverImgUrl || playlist?.picUrl || '', 'md')}
className='aspect-square rounded-24'
onMouseOver={prefetch}
/>
<div className='group relative'>
<Image
onClick={goTo}
key={playlist.id}
src={resizeImage(playlist.coverImgUrl || playlist?.picUrl || '', 'md')}
className='aspect-square rounded-24'
onMouseOver={prefetch}
/>
{/* Hover mask layer */}
<div className='pointer-events-none absolute inset-0 w-full bg-gradient-to-b from-transparent to-black opacity-0 transition-all duration-400 group-hover:opacity-100 '></div>
{/* Name */}
<div className='pointer-events-none absolute bottom-0 p-3 text-sm font-medium text-neutral-300 opacity-0 transition-all duration-400 group-hover:opacity-100'>
{playlist.name}
</div>
</div>
)
}

View file

@ -46,11 +46,7 @@ const CoverRow = ({
return (
<div className={className}>
{/* Title */}
{title && (
<h4 className='mb-6 text-14 font-bold uppercase dark:text-neutral-300'>
{title}
</h4>
)}
{title && <h4 className='mb-6 text-14 font-bold uppercase dark:text-neutral-300'>{title}</h4>}
<Virtuoso
className='no-scrollbar'
@ -66,20 +62,14 @@ const CoverRow = ({
Footer: () => <div className='h-16'></div>,
}}
itemContent={(index, row) => (
<div
key={index}
className='grid w-full grid-cols-4 gap-4 lg:mb-6 lg:gap-6'
>
<div key={index} className='grid w-full grid-cols-4 gap-4 lg:mb-6 lg:gap-6'>
{row.map((item: Item) => (
<img
onClick={() => goTo(item.id)}
key={item.id}
alt={item.name}
src={resizeImage(
item?.picUrl ||
(item as Playlist)?.coverImgUrl ||
item?.picUrl ||
'',
item?.picUrl || (item as Playlist)?.coverImgUrl || item?.picUrl || '',
'md'
)}
className='aspect-square w-full rounded-24'

View file

@ -22,11 +22,7 @@ const sizes = {
},
} as const
const CoverWall = ({
albums,
}: {
albums: { id: number; coverUrl: string; large: boolean }[]
}) => {
const CoverWall = ({ albums }: { albums: { id: number; coverUrl: string; large: boolean }[] }) => {
const navigate = useNavigate()
const breakpoint = useBreakpoint()
@ -41,10 +37,7 @@ const CoverWall = ({
>
{albums.map(album => (
<Image
src={resizeImage(
album.coverUrl,
sizes[album.large ? 'large' : 'small'][breakpoint]
)}
src={resizeImage(album.coverUrl, sizes[album.large ? 'large' : 'small'][breakpoint])}
key={album.id}
className={cx(
'aspect-square h-full w-full rounded-20 lg:rounded-24',

View file

@ -1,27 +1,16 @@
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import useIsMobile from '@/web/hooks/useIsMobile'
const Devtool = () => {
const isMobile = useIsMobile()
return (
<ReactQueryDevtools
initialIsOpen={false}
toggleButtonProps={{
style: {
position: 'fixed',
...(isMobile
? {
top: 0,
right: 0,
bottom: 'auto',
left: 'atuo',
}
: {
top: 36,
right: 148,
bottom: 'atuo',
left: 'auto',
}),
top: 36,
right: 148,
bottom: 'atuo',
left: 'auto',
},
}}
/>

View file

@ -0,0 +1,45 @@
import { css, cx } from '@emotion/css'
import { motion } from 'framer-motion'
export interface DropdownItem {
label: string
onClick: () => void
}
function Dropdown({ items, onClose }: { items: DropdownItem[]; onClose: () => void }) {
return (
<motion.div
initial={{ opacity: 0, scale: 0.96 }}
animate={{
opacity: 1,
scale: 1,
transition: {
duration: 0.1,
},
}}
exit={{ opacity: 0, scale: 0.96 }}
transition={{ duration: 0.2 }}
className={cx(
'origin-top rounded-12 border border-white/[.06] bg-gray-900/95 p-px py-2.5 shadow-xl outline outline-1 outline-black backdrop-blur-3xl',
css`
min-width: 200px;
`
)}
>
{items.map((item, index) => (
<div
className='active:bg-gray/50 relative flex w-full items-center justify-between whitespace-nowrap rounded-[5px] p-3 text-16 font-medium text-neutral-200 transition-colors duration-400 hover:bg-white/[.06]'
key={index}
onClick={() => {
item.onClick()
onClose()
}}
>
{item.label}
</div>
))}
</motion.div>
)
}
export default Dropdown

View file

@ -11,9 +11,7 @@ const ErrorBoundary = ({ children }: { children: ReactNode }) => {
>
<div className='app-region-no-drag'>
<p>Something went wrong:</p>
<pre className='mb-2 text-18 dark:text-white'>
{error.toString()}
</pre>
<pre className='mb-2 text-18 dark:text-white'>{error.toString()}</pre>
<div className='max-h-72 max-w-2xl overflow-scroll whitespace-pre-line rounded-12 border border-white/10 px-3 py-2 dark:text-white/50'>
{componentStack?.trim()}
</div>

View file

@ -101,8 +101,7 @@ const ImageDesktop = ({
}
const ImageMobile = (props: Props) => {
const { src, className, srcSet, sizes, lazyLoad, onClick, onMouseOver } =
props
const { src, className, srcSet, sizes, lazyLoad, onClick, onMouseOver } = props
return (
<div
onClick={onClick}

View file

@ -21,11 +21,8 @@ const Layout = () => {
<div
id='layout'
className={cx(
'relative grid h-screen select-none overflow-hidden bg-white dark:bg-black',
window.env?.isElectron && !fullscreen && 'rounded-24',
css`
min-width: 720px;
`
'relative grid h-screen select-none overflow-hidden bg-black',
window.env?.isElectron && !fullscreen && 'rounded-24'
)}
>
<BlurBackground />
@ -47,7 +44,15 @@ const Layout = () => {
<ContextMenus />
{/* {window.env?.isElectron && <Airplay />} */}
{/* Border */}
<div
className={cx(
'pointer-events-none fixed inset-0 z-50 rounded-24',
css`
box-shadow: inset 0px 0px 0px 1px rgba(255, 255, 255, 0.06);
`
)}
></div>
</div>
)
}

View file

@ -94,10 +94,7 @@ const LoginWithQRCode = () => {
)
const text = useMemo(
() =>
key?.data?.unikey
? `https://music.163.com/login?codekey=${key.data.unikey}`
: '',
() => (key?.data?.unikey ? `https://music.163.com/login?codekey=${key.data.unikey}` : ''),
[key?.data?.unikey]
)

View file

@ -6,9 +6,7 @@ import { MotionConfig, motion } from 'framer-motion'
import { useSnapshot } from 'valtio'
import Icon from '../Icon'
import { State as PlayerState } from '@/web/utils/player'
import useUserLikedTracksIDs, {
useMutationLikeATrack,
} from '@/web/api/hooks/useUserLikedTracksIDs'
import useUserLikedTracksIDs, { useMutationLikeATrack } from '@/web/api/hooks/useUserLikedTracksIDs'
const LikeButton = () => {
const { track } = useSnapshot(player)
@ -38,9 +36,7 @@ const Controls = () => {
<motion.div
className={cx(
'fixed bottom-0 right-0 flex',
mini
? 'flex-col items-center justify-between'
: 'items-center justify-between',
mini ? 'flex-col items-center justify-between' : 'items-center justify-between',
mini
? css`
right: 24px;
@ -85,11 +81,7 @@ const Controls = () => {
className='rounded-full bg-black/10 p-2.5 transition-colors duration-400 dark:bg-white/10 hover:dark:bg-white/20'
>
<Icon
name={
[PlayerState.Playing, PlayerState.Loading].includes(state)
? 'pause'
: 'play'
}
name={[PlayerState.Playing, PlayerState.Loading].includes(state) ? 'pause' : 'play'}
className='h-6 w-6 '
/>
</motion.button>

View file

@ -7,6 +7,7 @@ import persistedUiStates from '@/web/states/persistedUiStates'
import Controls from './Controls'
import Cover from './Cover'
import Progress from './Progress'
import { ease } from '@/web/utils/const'
const NowPlaying = () => {
const { track } = useSnapshot(player)
@ -21,6 +22,7 @@ const NowPlaying = () => {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ ease, duration: 0.4 }}
className={cx(
'relative flex aspect-square h-full w-full flex-col justify-end overflow-hidden rounded-24 border',
css`

View file

@ -25,6 +25,7 @@ const Player = () => {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ ease, duration: 0.4 }}
>
<PlayingNext />
</motion.div>

View file

@ -8,9 +8,7 @@ import { resizeImage } from '@/web/utils/common'
import { motion, PanInfo } from 'framer-motion'
import { useLockBodyScroll } from 'react-use'
import { useState } from 'react'
import useUserLikedTracksIDs, {
useMutationLikeATrack,
} from '@/web/api/hooks/useUserLikedTracksIDs'
import useUserLikedTracksIDs, { useMutationLikeATrack } from '@/web/api/hooks/useUserLikedTracksIDs'
import uiStates from '@/web/states/uiStates'
import { ease } from '@/web/utils/const'
@ -27,10 +25,7 @@ const LikeButton = () => {
className='flex h-full items-center'
onClick={() => track?.id && likeATrack.mutateAsync(track.id)}
>
<Icon
name={isLiked ? 'heart' : 'heart-outline'}
className='h-7 w-7 text-white/10'
/>
<Icon name={isLiked ? 'heart' : 'heart-outline'} className='h-7 w-7 text-white/10' />
</button>
)
}
@ -42,10 +37,7 @@ const PlayerMobile = () => {
useLockBodyScroll(locked)
const { mobileShowPlayingNext } = useSnapshot(uiStates)
const onDragEnd = (
event: MouseEvent | TouchEvent | PointerEvent,
info: PanInfo
) => {
const onDragEnd = (event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => {
console.log(JSON.stringify(info))
const { x, y } = info.offset
const offset = 100
@ -107,9 +99,7 @@ const PlayerMobile = () => {
className='flex h-full flex-grow items-center '
>
<div className='flex-shrink-0'>
<div className='line-clamp-1 text-14 font-bold text-white'>
{track?.name}
</div>
<div className='line-clamp-1 text-14 font-bold text-white'>{track?.name}</div>
<div className='line-clamp-1 mt-1 text-12 font-bold text-white/60'>
{track?.ar?.map(a => a.name).join(', ')}
</div>
@ -143,10 +133,7 @@ const PlayerMobile = () => {
onClick={() => player.playOrPause()}
className='ml-2.5 flex items-center justify-center rounded-full bg-white/20 p-2.5'
>
<Icon
name={state === 'playing' ? 'pause' : 'play'}
className='h-6 w-6 text-white/80'
/>
<Icon name={state === 'playing' ? 'pause' : 'play'} className='h-6 w-6 text-white/80' />
</button>
</div>
)

View file

@ -67,10 +67,7 @@ const PlayingNextMobile = () => {
`
)}
>
<Icon
name='player-handler'
className='mb-5 h-2.5 rotate-180 text-brand-700'
/>
<Icon name='player-handler' className='mb-5 h-2.5 rotate-180 text-brand-700' />
</motion.div>
{/* List */}

View file

@ -1,21 +1,17 @@
import { Route, Routes, useLocation } from 'react-router-dom'
import { AnimatePresence } from 'framer-motion'
import React, { ReactNode, Suspense } from 'react'
import React, { lazy, Suspense } from 'react'
import VideoPlayer from './VideoPlayer'
const My = React.lazy(() => import('@/web/pages/My'))
const Discover = React.lazy(() => import('@/web/pages/Discover'))
const Browse = React.lazy(() => import('@/web/pages/Browse'))
const Album = React.lazy(() => import('@/web/pages/Album'))
const Playlist = React.lazy(() => import('@/web/pages/Playlist'))
const Artist = React.lazy(() => import('@/web/pages/Artist'))
const Lyrics = React.lazy(() => import('@/web/pages/Lyrics'))
const Search = React.lazy(() => import('@/web/pages/Search'))
const Settings = React.lazy(() => import('@/web/pages/Settings'))
const lazy = (component: ReactNode) => {
return <Suspense>{component}</Suspense>
}
const My = lazy(() => import('@/web/pages/My'))
const Discover = lazy(() => import('@/web/pages/Discover'))
const Browse = lazy(() => import('@/web/pages/Browse'))
const Album = lazy(() => import('@/web/pages/Album'))
const Playlist = lazy(() => import('@/web/pages/Playlist'))
const Artist = lazy(() => import('@/web/pages/Artist'))
const Lyrics = lazy(() => import('@/web/pages/Lyrics'))
const Search = lazy(() => import('@/web/pages/Search'))
const Settings = lazy(() => import('@/web/pages/Settings'))
const Router = () => {
const location = useLocation()
@ -24,16 +20,16 @@ const Router = () => {
<AnimatePresence mode='wait'>
<VideoPlayer />
<Routes location={location} key={location.pathname}>
<Route path='/' element={lazy(<My />)} />
<Route path='/discover' element={lazy(<Discover />)} />
<Route path='/browse' element={lazy(<Browse />)} />
<Route path='/album/:id' element={lazy(<Album />)} />
<Route path='/playlist/:id' element={lazy(<Playlist />)} />
<Route path='/artist/:id' element={lazy(<Artist />)} />
<Route path='/settings' element={lazy(<Settings />)} />
<Route path='/lyrics' element={lazy(<Lyrics />)} />
<Route path='/search/:keywords' element={lazy(<Search />)}>
<Route path=':type' element={lazy(<Search />)} />
<Route path='/' element={<My />} />
<Route path='/discover' element={<Discover />} />
<Route path='/browse' element={<Browse />} />
<Route path='/album/:id' element={<Album />} />
<Route path='/playlist/:id' element={<Playlist />} />
<Route path='/artist/:id' element={<Artist />} />
<Route path='/settings' element={<Settings />} />
<Route path='/lyrics' element={<Lyrics />} />
<Route path='/search/:keywords' element={<Search />}>
<Route path=':type' element={<Search />} />
</Route>
</Routes>
</AnimatePresence>

View file

@ -23,8 +23,7 @@ const Slider = ({
const [isDragging, setIsDragging] = useState(false)
const [draggingValue, setDraggingValue] = useState(value)
const memoedValue = useMemo(
() =>
isDragging && onlyCallOnChangeAfterDragEnded ? draggingValue : value,
() => (isDragging && onlyCallOnChangeAfterDragEnded ? draggingValue : value),
[isDragging, draggingValue, value, onlyCallOnChangeAfterDragEnded]
)
@ -50,8 +49,7 @@ const Slider = ({
* Handle slider click event
*/
const handleClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) =>
onChange(getNewValue({ x: e.clientX, y: e.clientY })),
(e: React.MouseEvent<HTMLDivElement>) => onChange(getNewValue({ x: e.clientX, y: e.clientY })),
[getNewValue, onChange]
)
@ -69,22 +67,14 @@ const Slider = ({
const handlePointerMove = (e: { clientX: number; clientY: number }) => {
if (!isDragging) return
const newValue = getNewValue({ x: e.clientX, y: e.clientY })
onlyCallOnChangeAfterDragEnded
? setDraggingValue(newValue)
: onChange(newValue)
onlyCallOnChangeAfterDragEnded ? setDraggingValue(newValue) : onChange(newValue)
}
document.addEventListener('pointermove', handlePointerMove)
return () => {
document.removeEventListener('pointermove', handlePointerMove)
}
}, [
isDragging,
onChange,
setDraggingValue,
onlyCallOnChangeAfterDragEnded,
getNewValue,
])
}, [isDragging, onChange, setDraggingValue, onlyCallOnChangeAfterDragEnded, getNewValue])
/**
* Handle pointer up events
@ -102,28 +92,18 @@ const Slider = ({
return () => {
document.removeEventListener('pointerup', handlePointerUp)
}
}, [
isDragging,
setIsDragging,
onlyCallOnChangeAfterDragEnded,
draggingValue,
onChange,
])
}, [isDragging, setIsDragging, onlyCallOnChangeAfterDragEnded, draggingValue, onChange])
/**
* Track and thumb styles
*/
const usedTrackStyle = useMemo(() => {
const percentage = `${(memoedValue / max) * 100}%`
return orientation === 'horizontal'
? { width: percentage }
: { height: percentage }
return orientation === 'horizontal' ? { width: percentage } : { height: percentage }
}, [max, memoedValue, orientation])
const thumbStyle = useMemo(() => {
const percentage = `${(memoedValue / max) * 100}%`
return orientation === 'horizontal'
? { left: percentage }
: { bottom: percentage }
return orientation === 'horizontal' ? { left: percentage } : { bottom: percentage }
}, [max, memoedValue, orientation])
return (
@ -159,9 +139,7 @@ const Slider = ({
<div
className={cx(
'absolute flex h-2 w-2 items-center justify-center rounded-full bg-black bg-opacity-20 transition-opacity dark:bg-white',
isDragging || alwaysShowThumb
? 'opacity-100'
: 'opacity-0 group-hover:opacity-100',
isDragging || alwaysShowThumb ? 'opacity-100' : 'opacity-0 group-hover:opacity-100',
orientation === 'horizontal' && '-translate-x-1',
orientation === 'vertical' && 'translate-y-1'
)}

View file

@ -3,12 +3,14 @@ import { IpcChannels } from '@/shared/IpcChannels'
import useIpcRenderer from '@/web/hooks/useIpcRenderer'
import { useState } from 'react'
import { css, cx } from '@emotion/css'
import uiStates from '../states/uiStates'
const Controls = () => {
const [isMaximized, setIsMaximized] = useState(false)
useIpcRenderer(IpcChannels.IsMaximized, (e, value) => {
setIsMaximized(value)
uiStates.fullscreen = value
})
const minimize = () => {
@ -38,10 +40,7 @@ const Controls = () => {
<Icon className='h-3 w-3' name='windows-minimize' />
</button>
<button onClick={maxRestore} className={classNames}>
<Icon
className='h-3 w-3'
name={isMaximized ? 'windows-un-maximize' : 'windows-maximize'}
/>
<Icon className='h-3 w-3' name={isMaximized ? 'windows-un-maximize' : 'windows-maximize'} />
</button>
<button
onClick={close}

View file

@ -1,7 +1,11 @@
import { css, cx } from '@emotion/css'
import Icon from '../Icon'
import { resizeImage } from '@/web/utils/common'
import useUser, { useMutationLogout } from '@/web/api/hooks/useUser'
import useUser, {
useDailyCheckIn,
useMutationLogout,
useRefreshCookie,
} from '@/web/api/hooks/useUser'
import uiStates from '@/web/states/uiStates'
import { useRef, useState } from 'react'
import BasicContextMenu from '../ContextMenus/BasicContextMenu'
@ -15,6 +19,9 @@ const Avatar = ({ className }: { className?: string }) => {
const { t } = useTranslation()
const navigate = useNavigate()
useRefreshCookie()
useDailyCheckIn()
const avatarUrl = user?.profile?.avatarUrl
? resizeImage(user?.profile?.avatarUrl ?? '', 'sm')
: ''

View file

@ -8,8 +8,7 @@ const TrafficLight = () => {
return <></>
}
const className =
'mr-2 h-3 w-3 rounded-full last-of-type:mr-0 dark:bg-white/20'
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>

View file

@ -13,7 +13,7 @@ const delay = ['-100ms', '-500ms', '-1200ms', '-1000ms', '-700ms']
const Wave = ({ playing }: { playing: boolean }) => {
return (
<div className='grid h-3 grid-cols-5 items-end gap-0.5'>
<div className='grid h-3 flex-shrink-0 grid-cols-5 items-end gap-0.5'>
{[...new Array(5).keys()].map(i => (
<div
key={i}