mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-17 05:38:04 +00:00
feat: updates
This commit is contained in:
parent
a1b0bcf4d3
commit
884f3df41a
198 changed files with 4572 additions and 5336 deletions
110
packages/web/components/ContextMenus/AlbumContextMenu.tsx
Normal file
110
packages/web/components/ContextMenus/AlbumContextMenu.tsx
Normal 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
|
||||
91
packages/web/components/ContextMenus/ArtistContextMenu.tsx
Normal file
91
packages/web/components/ContextMenus/ArtistContextMenu.tsx
Normal 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
|
||||
85
packages/web/components/ContextMenus/BasicContextMenu.tsx
Normal file
85
packages/web/components/ContextMenus/BasicContextMenu.tsx
Normal 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
|
||||
15
packages/web/components/ContextMenus/ContextMenus.tsx
Normal file
15
packages/web/components/ContextMenus/ContextMenus.tsx
Normal 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
|
||||
112
packages/web/components/ContextMenus/MenuItem.tsx
Normal file
112
packages/web/components/ContextMenus/MenuItem.tsx
Normal 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
|
||||
173
packages/web/components/ContextMenus/MenuPanel.tsx
Normal file
173
packages/web/components/ContextMenus/MenuPanel.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
112
packages/web/components/ContextMenus/TrackContextMenu.tsx
Normal file
112
packages/web/components/ContextMenus/TrackContextMenu.tsx
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue