feat: updates

This commit is contained in:
qier222 2022-10-28 20:29:04 +08:00
parent a1b0bcf4d3
commit 884f3df41a
No known key found for this signature in database
198 changed files with 4572 additions and 5336 deletions

View file

@ -0,0 +1,110 @@
import useUserAlbums, {
useMutationLikeAAlbum,
} from '@/web/api/hooks/useUserAlbums'
import contextMenus, { closeContextMenu } from '@/web/states/contextMenus'
import player from '@/web/states/player'
import { AnimatePresence } from 'framer-motion'
import { useMemo, useState } from 'react'
import toast from 'react-hot-toast'
import { useTranslation } from 'react-i18next'
import { useCopyToClipboard } from 'react-use'
import { useSnapshot } from 'valtio'
import BasicContextMenu from './BasicContextMenu'
const AlbumContextMenu = () => {
const { t } = useTranslation()
const { cursorPosition, type, dataSourceID, target, options } =
useSnapshot(contextMenus)
const likeAAlbum = useMutationLikeAAlbum()
const [, copyToClipboard] = useCopyToClipboard()
const { data: likedAlbums } = useUserAlbums()
const addToLibraryLabel = useMemo(() => {
return likedAlbums?.data?.find(a => a.id === Number(dataSourceID))
? 'Remove from Library'
: 'Add to Library'
}, [dataSourceID, likedAlbums?.data])
return (
<AnimatePresence>
{cursorPosition && type === 'album' && dataSourceID && target && (
<BasicContextMenu
target={target}
cursorPosition={cursorPosition}
onClose={closeContextMenu}
options={options}
items={[
{
type: 'item',
label: t`context-menu.add-to-queue`,
onClick: () => {
toast('开发中')
// toast.success('Added to Queue', { duration: 100000000 })
// toast.error('Not implemented yet', { duration: 100000000 })
// toast.loading('Loading...')
// toast('ADADADAD', { duration: 100000000 })
},
},
{
type: 'divider',
},
{
type: 'item',
label: addToLibraryLabel,
onClick: () => {
if (type !== 'album' || !dataSourceID) {
return
}
likeAAlbum.mutateAsync(Number(dataSourceID)).then(res => {
if (res?.code === 200) {
toast.success('Added to Library')
}
})
},
},
{
type: 'item',
label: t`context-menu.add-to-playlist`,
onClick: () => {
toast('开发中')
},
},
{
type: 'divider',
},
{
type: 'submenu',
label: t`context-menu.share`,
items: [
{
type: 'item',
label: t`context-menu.copy-netease-link`,
onClick: () => {
copyToClipboard(
`https://music.163.com/#/album?id=${dataSourceID}`
)
toast.success(t`toasts.copied`)
},
},
{
type: 'item',
label: 'Copy YPM Link',
onClick: () => {
copyToClipboard(
`${window.location.origin}/album/${dataSourceID}`
)
toast.success(t`toasts.copied`)
},
},
],
},
]}
/>
)}
</AnimatePresence>
)
}
export default AlbumContextMenu

View file

@ -0,0 +1,91 @@
import useUserArtists, {
useMutationLikeAArtist,
} from '@/web/api/hooks/useUserArtists'
import contextMenus, { closeContextMenu } from '@/web/states/contextMenus'
import { AnimatePresence } from 'framer-motion'
import { useMemo, useState } from 'react'
import toast from 'react-hot-toast'
import { useTranslation } from 'react-i18next'
import { useCopyToClipboard } from 'react-use'
import { useSnapshot } from 'valtio'
import BasicContextMenu from './BasicContextMenu'
const ArtistContextMenu = () => {
const { t } = useTranslation()
const { cursorPosition, type, dataSourceID, target, options } =
useSnapshot(contextMenus)
const likeAArtist = useMutationLikeAArtist()
const [, copyToClipboard] = useCopyToClipboard()
const { data: likedArtists } = useUserArtists()
const followLabel = useMemo(() => {
return likedArtists?.data?.find(a => a.id === Number(dataSourceID))
? t`context-menu.unfollow`
: t`context-menu.follow`
}, [dataSourceID, likedArtists?.data, t])
return (
<AnimatePresence>
{cursorPosition && type === 'artist' && dataSourceID && target && (
<BasicContextMenu
target={target}
cursorPosition={cursorPosition}
onClose={closeContextMenu}
options={options}
items={[
{
type: 'item',
label: followLabel,
onClick: () => {
if (type !== 'artist' || !dataSourceID) {
return
}
likeAArtist.mutateAsync(Number(dataSourceID)).then(res => {
if (res?.code === 200) {
toast.success(
followLabel === t`context-menu.unfollow`
? t`context-menu.unfollowed`
: t`context-menu.followed`
)
}
})
},
},
{
type: 'divider',
},
{
type: 'submenu',
label: t`context-menu.share`,
items: [
{
type: 'item',
label: t`context-menu.copy-netease-link`,
onClick: () => {
copyToClipboard(
`https://music.163.com/#/artist?id=${dataSourceID}`
)
toast.success(t`toasts.copied`)
},
},
{
type: 'item',
label: 'Copy YPM Link',
onClick: () => {
copyToClipboard(
`${window.location.origin}/artist/${dataSourceID}`
)
toast.success(t`toasts.copied`)
},
},
],
},
]}
/>
)}
</AnimatePresence>
)
}
export default ArtistContextMenu

View file

@ -0,0 +1,85 @@
import { useLayoutEffect, useRef, useState } from 'react'
import { useClickAway } from 'react-use'
import useLockMainScroll from '@/web/hooks/useLockMainScroll'
import useMeasure from 'react-use-measure'
import { ContextMenuItem } from './MenuItem'
import MenuPanel from './MenuPanel'
const BasicContextMenu = ({
onClose,
items,
target,
cursorPosition,
options,
classNames,
}: {
onClose: (e: MouseEvent) => void
items: ContextMenuItem[]
target: HTMLElement
cursorPosition: { x: number; y: number }
options?: {
useCursorPosition?: boolean
} | null
classNames?: string
}) => {
const menuRef = useRef<HTMLDivElement>(null)
const [measureRef, menu] = useMeasure()
const [position, setPosition] = useState<{ x: number; y: number } | null>(
null
)
useClickAway(menuRef, onClose)
useLockMainScroll(!!position)
useLayoutEffect(() => {
if (options?.useCursorPosition) {
const leftX = cursorPosition.x
const rightX = cursorPosition.x - menu.width
const bottomY = cursorPosition.y
const topY = cursorPosition.y - menu.height
const position = {
x: leftX + menu.width < window.innerWidth ? leftX : rightX,
y: bottomY + menu.height < window.innerHeight ? bottomY : topY,
}
setPosition(position)
} else {
const button = target.getBoundingClientRect()
const leftX = button.x
const rightX = button.x - menu.width + button.width
const bottomY = button.y + button.height + 8
const topY = button.y - menu.height - 8
const position = {
x: leftX + menu.width < window.innerWidth ? leftX : rightX,
y: bottomY + menu.height < window.innerHeight ? bottomY : topY,
}
setPosition(position)
}
}, [target, menu, options?.useCursorPosition, cursorPosition])
return (
<>
<MenuPanel
position={{ x: 99999, y: 99999 }}
items={items}
ref={measureRef}
onClose={() => {
//
}}
forMeasure={true}
classNames={classNames}
/>
{position && (
<MenuPanel
position={position}
items={items}
ref={menuRef}
onClose={onClose}
classNames={classNames}
/>
)}
</>
)
}
export default BasicContextMenu

View file

@ -0,0 +1,15 @@
import AlbumContextMenu from './AlbumContextMenu'
import ArtistContextMenu from './ArtistContextMenu'
import TrackContextMenu from './TrackContextMenu'
const ContextMenus = () => {
return (
<>
<TrackContextMenu />
<AlbumContextMenu />
<ArtistContextMenu />
</>
)
}
export default ContextMenus

View file

@ -0,0 +1,112 @@
import { css, cx } from '@emotion/css'
import { ForwardedRef, forwardRef, useRef, useState } from 'react'
import Icon from '../Icon'
export interface ContextMenuItem {
type: 'item' | 'submenu' | 'divider'
label?: string
onClick?: (e: MouseEvent) => void
items?: ContextMenuItem[]
}
const MenuItem = ({
item,
index,
onClose,
onSubmenuOpen,
onSubmenuClose,
className,
}: {
item: ContextMenuItem
index: number
onClose: (e: MouseEvent) => void
onSubmenuOpen: (props: { itemRect: DOMRect; index: number }) => void
onSubmenuClose: () => void
className?: string
}) => {
const itemRef = useRef<HTMLDivElement>(null)
const [isHover, setIsHover] = useState(false)
if (item.type === 'divider') {
return (
<div className='my-2 h-px w-full px-3'>
<div className='h-full w-full bg-white/20'></div>
</div>
)
}
return (
<div
ref={itemRef}
onClick={e => {
if (!item.onClick) {
return
}
const event = e as unknown as MouseEvent
item.onClick?.(event)
onClose(event)
}}
onMouseOver={() => {
if (item.type !== 'submenu') return
setIsHover(true)
onSubmenuOpen({
itemRect: itemRef.current!.getBoundingClientRect(),
index,
})
}}
onMouseLeave={e => {
const relatedTarget = e.relatedTarget as HTMLElement | null
if (relatedTarget?.classList?.contains('submenu')) {
return
}
setIsHover(false)
onSubmenuClose()
}}
className={cx(
'relative',
className,
css`
padding-right: 9px;
padding-left: 9px;
`
)}
>
<div
className={cx(
'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]',
item.type !== 'submenu' && !isHover && 'active:bg-gray/50',
isHover && 'bg-white/[.06]'
)}
>
<div>{item.label}</div>
{item.type === 'submenu' && (
<>
<Icon
name='caret-right'
className={cx(
'ml-10 text-neutral-600',
css`
height: 8px;
width: 5px;
`
)}
/>
{/* 将item变宽一点避免移动鼠标时还没移动到submenu就关闭submenu了 */}
<div
className={cx(
'absolute h-full',
css`
left: -24px;
width: calc(100% + 48px);
`
)}
></div>
</>
)}
</div>
</div>
)
}
export default MenuItem

View file

@ -0,0 +1,173 @@
import { css, cx } from '@emotion/css'
import {
ForwardedRef,
forwardRef,
useLayoutEffect,
useRef,
useState,
} from 'react'
import { motion } from 'framer-motion'
import MenuItem, { ContextMenuItem } from './MenuItem'
interface PanelProps {
position: {
x: number
y: number
transformOrigin?: `origin-${'top' | 'bottom'}-${'left' | 'right'}`
}
items: ContextMenuItem[]
onClose: (e: MouseEvent) => void
forMeasure?: boolean
classNames?: string
isSubmenu?: boolean
}
interface SubmenuProps {
itemRect: DOMRect
index: number
}
const MenuPanel = forwardRef(
(
{ position, items, onClose, forMeasure, classNames, isSubmenu }: PanelProps,
ref: ForwardedRef<HTMLDivElement>
) => {
const [submenuProps, setSubmenuProps] = useState<SubmenuProps | null>(null)
return (
// Container (to add padding for submenus)
<motion.div
initial={{ opacity: 0, scale: forMeasure ? 1 : 0.96 }}
animate={{
opacity: 1,
scale: 1,
transition: {
duration: 0.1,
},
}}
exit={{ opacity: 0, scale: 0.96 }}
transition={{ duration: 0.2 }}
ref={ref}
className={cx(
'fixed',
position.transformOrigin || 'origin-top-left',
isSubmenu ? 'submenu z-20 px-1' : 'z-10'
)}
style={{ left: position.x, top: position.y }}
>
{/* The real panel */}
<div
className={cx(
'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;
`,
classNames
)}
>
{items.map((item, index) => (
<MenuItem
key={index}
index={index}
item={item}
onClose={onClose}
onSubmenuOpen={(props: SubmenuProps) => setSubmenuProps(props)}
onSubmenuClose={() => setSubmenuProps(null)}
className={isSubmenu ? 'submenu' : ''}
/>
))}
</div>
{/* Submenu */}
<SubMenu
items={
submenuProps?.index ? items[submenuProps?.index]?.items : undefined
}
itemRect={submenuProps?.itemRect}
onClose={onClose}
/>
</motion.div>
)
}
)
MenuPanel.displayName = 'Menu'
export default MenuPanel
const SubMenu = ({
items,
itemRect,
onClose,
}: {
items?: ContextMenuItem[]
itemRect?: DOMRect
onClose: (e: MouseEvent) => void
}) => {
const submenuRef = useRef<HTMLDivElement>(null)
const [position, setPosition] = useState<{
x: number
y: number
transformOrigin: `origin-${'top' | 'bottom'}-${'left' | 'right'}`
}>()
useLayoutEffect(() => {
if (!itemRect || !submenuRef.current) {
return
}
const item = itemRect
const submenu = submenuRef.current.getBoundingClientRect()
const isRightSide = item.x + item.width + submenu.width <= window.innerWidth
const x = isRightSide ? item.x + item.width : item.x - submenu.width
const isTopSide = item.y - 10 + submenu.height <= window.innerHeight
const y = isTopSide
? item.y - 10
: item.y + item.height + 10 - submenu.height
const transformOriginTable = {
top: {
right: 'origin-top-left',
left: 'origin-top-right',
},
bottom: {
right: 'origin-bottom-left',
left: 'origin-bottom-right',
},
} as const
setPosition({
x,
y,
transformOrigin:
transformOriginTable[isTopSide ? 'top' : 'bottom'][
isRightSide ? 'right' : 'left'
],
})
}, [itemRect])
if (!items || !itemRect) {
return <></>
}
return (
<>
<MenuPanel
position={{ x: 99999, y: 99999 }}
items={items || []}
ref={submenuRef}
onClose={() => {
// Do nothing
}}
forMeasure={true}
isSubmenu={true}
/>
<MenuPanel
position={position || { x: 99999, y: 99999 }}
items={items || []}
onClose={onClose}
isSubmenu={true}
/>
</>
)
}

View file

@ -0,0 +1,112 @@
import { fetchTracksWithReactQuery } from '@/web/api/hooks/useTracks'
import { fetchTracks } from '@/web/api/track'
import contextMenus, { closeContextMenu } from '@/web/states/contextMenus'
import { AnimatePresence } from 'framer-motion'
import toast from 'react-hot-toast'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { useCopyToClipboard } from 'react-use'
import { useSnapshot } from 'valtio'
import BasicContextMenu from './BasicContextMenu'
const TrackContextMenu = () => {
const navigate = useNavigate()
const { t } = useTranslation()
const [, copyToClipboard] = useCopyToClipboard()
const { type, dataSourceID, target, cursorPosition, options } =
useSnapshot(contextMenus)
return (
<AnimatePresence>
{type === 'track' && dataSourceID && target && cursorPosition && (
<BasicContextMenu
target={target}
cursorPosition={cursorPosition}
onClose={closeContextMenu}
options={options}
items={[
{
type: 'item',
label: t`context-menu.add-to-queue`,
onClick: () => {
toast('开发中')
},
},
{
type: 'divider',
},
{
type: 'item',
label: t`context-menu.go-to-artist`,
onClick: async () => {
const tracks = await fetchTracksWithReactQuery({
ids: [Number(dataSourceID)],
})
const track = tracks?.songs?.[0]
if (track) navigate(`/artist/${track.ar[0].id}`)
},
},
{
type: 'item',
label: t`context-menu.go-to-album`,
onClick: async () => {
const tracks = await fetchTracksWithReactQuery({
ids: [Number(dataSourceID)],
})
const track = tracks?.songs?.[0]
if (track) navigate(`/album/${track.al.id}`)
},
},
{
type: 'divider',
},
{
type: 'item',
label: t`context-menu.add-to-liked-tracks`,
onClick: () => {
toast('开发中')
},
},
{
type: 'item',
label: t`context-menu.add-to-playlist`,
onClick: () => {
toast('开发中')
},
},
{
type: 'submenu',
label: t`context-menu.share`,
items: [
{
type: 'item',
label: t`context-menu.copy-netease-link`,
onClick: () => {
copyToClipboard(
`https://music.163.com/#/album?id=${dataSourceID}`
)
toast.success(t`toasts.copied`)
},
},
{
type: 'item',
label: 'Copy YPM Link',
onClick: () => {
copyToClipboard(
`${window.location.origin}/album/${dataSourceID}`
)
toast.success(t`toasts.copied`)
},
},
],
},
]}
/>
)}
</AnimatePresence>
)
}
export default TrackContextMenu