mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-16 13:17:46 +00:00
feat: updates
This commit is contained in:
parent
ce757215a3
commit
c1cd31840e
86 changed files with 1048 additions and 778 deletions
|
|
@ -5,7 +5,7 @@ import TrackListHeader from '@/web/components/TrackListHeader'
|
|||
import player from '@/web/states/player'
|
||||
import { formatDuration } from '@/web/utils/common'
|
||||
import dayjs from 'dayjs'
|
||||
import { useMemo } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -68,18 +68,21 @@ const Header = () => {
|
|||
return !!userLikedAlbums?.data?.find(item => item.id === id)
|
||||
}, [params.id, userLikedAlbums?.data])
|
||||
|
||||
const onPlay = async (trackID: number | null = null) => {
|
||||
if (!album?.id) {
|
||||
toast('无法播放专辑,该专辑不存在')
|
||||
return
|
||||
}
|
||||
player.playAlbum(album.id, trackID)
|
||||
}
|
||||
const onPlay = useCallback(
|
||||
async (trackID: number | null = null) => {
|
||||
if (!album?.id) {
|
||||
toast('无法播放专辑,该专辑不存在')
|
||||
return
|
||||
}
|
||||
player.playAlbum(album.id, trackID)
|
||||
},
|
||||
[album?.id]
|
||||
)
|
||||
|
||||
const likeAAlbum = useMutationLikeAAlbum()
|
||||
const onLike = async () => {
|
||||
const onLike = useCallback(async () => {
|
||||
likeAAlbum.mutateAsync(album?.id || Number(params.id))
|
||||
}
|
||||
}, [likeAAlbum.mutateAsync, album?.id, params.id])
|
||||
|
||||
return (
|
||||
<TrackListHeader
|
||||
|
|
|
|||
|
|
@ -14,12 +14,9 @@ const MoreByArtist = ({ album }: { album?: Album }) => {
|
|||
if (!albums) return []
|
||||
const allReleases = albums?.hotAlbums || []
|
||||
const filteredAlbums = allReleases.filter(
|
||||
album =>
|
||||
['专辑', 'EP/Single', 'EP'].includes(album.type) && album.size > 1
|
||||
)
|
||||
const singles = allReleases.filter(
|
||||
album => album.type === 'Single' || album.size === 1
|
||||
album => ['专辑', 'EP/Single', 'EP'].includes(album.type) && album.size > 1
|
||||
)
|
||||
const singles = allReleases.filter(album => album.type === 'Single' || album.size === 1)
|
||||
|
||||
const qualifiedAlbums = [...filteredAlbums, ...singles]
|
||||
|
||||
|
|
@ -41,10 +38,7 @@ const MoreByArtist = ({ album }: { album?: Album }) => {
|
|||
}
|
||||
|
||||
// 去除 remix 专辑
|
||||
if (
|
||||
a.name.toLowerCase().includes('remix)') ||
|
||||
a.name.toLowerCase().includes('remixes)')
|
||||
) {
|
||||
if (a.name.toLowerCase().includes('remix)') || a.name.toLowerCase().includes('remixes)')) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import useUserArtists, {
|
||||
useMutationLikeAArtist,
|
||||
} from '@/web/api/hooks/useUserArtists'
|
||||
import useUserArtists, { useMutationLikeAArtist } from '@/web/api/hooks/useUserArtists'
|
||||
import Icon from '@/web/components/Icon'
|
||||
import { openContextMenu } from '@/web/states/contextMenus'
|
||||
import player from '@/web/states/player'
|
||||
import { cx } from '@emotion/css'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
|
|
@ -50,10 +47,7 @@ const Actions = ({ isLoading }: { isLoading: boolean }) => {
|
|||
: 'text-white/40 hover:text-white/70 hover:dark:bg-white/30 '
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name={isLiked ? 'heart' : 'heart-outline'}
|
||||
className='h-7 w-7'
|
||||
/>
|
||||
<Icon name={isLiked ? 'heart' : 'heart-outline'} className='h-7 w-7' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ const ArtistInfo = ({ artist, isLoading }: { artist?: Artist; isLoading: boolean
|
|||
className={cx(
|
||||
'line-clamp-5 mt-6 text-14 font-bold text-transparent',
|
||||
css`
|
||||
height: 86px;
|
||||
min-height: 85px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
|
|
@ -68,15 +68,14 @@ const ArtistInfo = ({ artist, isLoading }: { artist?: Artist; isLoading: boolean
|
|||
) : (
|
||||
<div
|
||||
className={cx(
|
||||
'line-clamp-5 mt-6 overflow-hidden whitespace-pre-wrap text-14 font-bold text-white/40 transition-colors duration-500 hover:text-white/60'
|
||||
// css`
|
||||
// height: 86px;
|
||||
// `
|
||||
'line-clamp-5 mt-6 overflow-hidden whitespace-pre-wrap text-14 font-bold text-white/40 transition-colors duration-500 hover:text-white/60',
|
||||
css`
|
||||
height: 85px;
|
||||
`
|
||||
)}
|
||||
onClick={() => setIsOpenDescription(true)}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
></div>
|
||||
))}
|
||||
|
||||
<DescriptionViewer
|
||||
|
|
|
|||
|
|
@ -104,11 +104,7 @@ const LatestRelease = () => {
|
|||
return (
|
||||
<>
|
||||
{!isLoadingVideos && !isLoadingAlbums && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className='mx-2.5 lg:mx-0'
|
||||
>
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className='mx-2.5 lg:mx-0'>
|
||||
<div className='mb-3 mt-7 text-14 font-bold text-neutral-300'>
|
||||
{t`artist.latest-releases`}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -37,9 +37,7 @@ const Track = ({
|
|||
<div
|
||||
className={cx(
|
||||
'line-clamp-1 text-16 font-medium ',
|
||||
isPlaying
|
||||
? 'text-brand-700'
|
||||
: 'text-neutral-700 dark:text-neutral-200'
|
||||
isPlaying ? 'text-brand-700' : 'text-neutral-700 dark:text-neutral-200'
|
||||
)}
|
||||
>
|
||||
{track?.name}
|
||||
|
|
@ -71,9 +69,7 @@ const Popular = () => {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-4 text-12 font-medium uppercase text-neutral-300'>
|
||||
{t`artist.popular`}
|
||||
</div>
|
||||
<div className='mb-4 text-12 font-medium uppercase text-neutral-300'>{t`artist.popular`}</div>
|
||||
|
||||
<div className='grid grid-cols-3 grid-rows-3 gap-4 overflow-hidden'>
|
||||
{tracks?.slice(0, 9)?.map(t => (
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import CoverWall from '@/web/components/CoverWall'
|
||||
import PageTransition from '@/web/components/PageTransition'
|
||||
import {
|
||||
fetchPlaylistWithReactQuery,
|
||||
fetchFromCache,
|
||||
} from '@/web/api/hooks/usePlaylist'
|
||||
import { fetchPlaylistWithReactQuery, fetchFromCache } from '@/web/api/hooks/usePlaylist'
|
||||
import { fetchTracksWithReactQuery } from '@/web/api/hooks/useTracks'
|
||||
import { sampleSize } from 'lodash-es'
|
||||
import { FetchPlaylistResponse } from '@/shared/api/Playlists'
|
||||
|
|
@ -39,9 +36,7 @@ const getAlbumsFromAPI = async () => {
|
|||
)
|
||||
|
||||
let ids: number[] = []
|
||||
playlists.forEach(playlist =>
|
||||
playlist?.playlist?.trackIds?.forEach(t => ids.push(t.id))
|
||||
)
|
||||
playlists.forEach(playlist => playlist?.playlist?.trackIds?.forEach(t => ids.push(t.id)))
|
||||
if (!ids.length) return []
|
||||
ids = sampleSize(ids, 100)
|
||||
|
||||
|
|
@ -77,8 +72,7 @@ const Discover = () => {
|
|||
const { data: albums } = useQuery(
|
||||
['DiscoveryAlbums'],
|
||||
async () => {
|
||||
const albumsInLocalStorageTime =
|
||||
localStorage.getItem('discoverAlbumsTime')
|
||||
const albumsInLocalStorageTime = localStorage.getItem('discoverAlbumsTime')
|
||||
if (
|
||||
!albumsInLocalStorageTime ||
|
||||
Date.now() - Number(albumsInLocalStorageTime) > 1000 * 60 * 60 * 2 // 2小时刷新一次
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import VideoRow from '@/web/components/VideoRow'
|
|||
import useUserVideos from '@/web/api/hooks/useUserVideos'
|
||||
import persistedUiStates from '@/web/states/persistedUiStates'
|
||||
import settings from '@/web/states/settings'
|
||||
import useUser from '@/web/api/hooks/useUser'
|
||||
|
||||
const collections = ['playlists', 'albums', 'artists', 'videos'] as const
|
||||
type Collection = typeof collections[number]
|
||||
|
|
@ -31,8 +32,37 @@ const Albums = () => {
|
|||
|
||||
const Playlists = () => {
|
||||
const { data: playlists } = useUserPlaylists()
|
||||
const p = useMemo(() => playlists?.playlist?.slice(1), [playlists])
|
||||
return <CoverRow playlists={p} />
|
||||
const user = useUser()
|
||||
const myPlaylists = useMemo(
|
||||
() => playlists?.playlist?.slice(1).filter(p => p.userId === user?.data?.account?.id),
|
||||
[playlists, user]
|
||||
)
|
||||
const otherPlaylists = useMemo(
|
||||
() => playlists?.playlist?.slice(1).filter(p => p.userId !== user?.data?.account?.id),
|
||||
[playlists, user]
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
{/* My playlists */}
|
||||
{myPlaylists && (
|
||||
<>
|
||||
<div className='mb-4 mt-2 text-14 font-medium uppercase text-neutral-400'>
|
||||
Created BY ME
|
||||
</div>
|
||||
<CoverRow playlists={myPlaylists} />
|
||||
</>
|
||||
)}
|
||||
{/* Other playlists */}
|
||||
{otherPlaylists && (
|
||||
<>
|
||||
<div className='mb-4 mt-8 text-14 font-medium uppercase text-neutral-400'>
|
||||
Created BY OTHERS
|
||||
</div>
|
||||
<CoverRow playlists={otherPlaylists} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Artists = () => {
|
||||
|
|
@ -50,14 +80,14 @@ const CollectionTabs = ({ showBg }: { showBg: boolean }) => {
|
|||
const { displayPlaylistsFromNeteaseMusic } = useSnapshot(settings)
|
||||
|
||||
const tabs: { id: Collection; name: string }[] = [
|
||||
{
|
||||
id: 'playlists',
|
||||
name: t`common.playlist_other`,
|
||||
},
|
||||
{
|
||||
id: 'albums',
|
||||
name: t`common.album_other`,
|
||||
},
|
||||
{
|
||||
id: 'playlists',
|
||||
name: t`common.playlist_other`,
|
||||
},
|
||||
{
|
||||
id: 'artists',
|
||||
name: t`common.artist_other`,
|
||||
|
|
@ -75,7 +105,7 @@ const CollectionTabs = ({ showBg }: { showBg: boolean }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='relative'>
|
||||
{/* Topbar background */}
|
||||
<AnimatePresence>
|
||||
{showBg && (
|
||||
|
|
@ -84,14 +114,14 @@ const CollectionTabs = ({ showBg }: { showBg: boolean }) => {
|
|||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className={cx(
|
||||
'pointer-events-none fixed top-0 right-0 left-10 z-10',
|
||||
'pointer-events-none absolute right-0 left-0 z-10',
|
||||
css`
|
||||
height: 230px;
|
||||
background-repeat: repeat;
|
||||
`
|
||||
)}
|
||||
style={{
|
||||
right: `${minimizePlayer ? 0 : playerWidth + 32}px`,
|
||||
top: '-132px',
|
||||
backgroundImage: `url(${topbarBackground})`,
|
||||
}}
|
||||
></motion.div>
|
||||
|
|
@ -115,7 +145,7 @@ const CollectionTabs = ({ showBg }: { showBg: boolean }) => {
|
|||
top: `${topbarHeight}px`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +161,7 @@ const Collections = () => {
|
|||
}, 500)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<motion.div layout>
|
||||
<CollectionTabs showBg={isScrollReachBottom} />
|
||||
<div
|
||||
className={cx('no-scrollbar overflow-y-auto px-2.5 pt-16 pb-16 lg:px-0')}
|
||||
|
|
@ -146,7 +176,7 @@ const Collections = () => {
|
|||
{selectedTab === 'videos' && <Videos />}
|
||||
</div>
|
||||
<div ref={observePoint}></div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import PageTransition from '@/web/components/PageTransition'
|
|||
import RecentlyListened from './RecentlyListened'
|
||||
import Collections from './Collections'
|
||||
import { useIsLoggedIn } from '@/web/api/hooks/useUser'
|
||||
import { LayoutGroup, motion } from 'framer-motion'
|
||||
|
||||
function PleaseLogin() {
|
||||
return <></>
|
||||
|
|
@ -13,11 +14,13 @@ const My = () => {
|
|||
return (
|
||||
<PageTransition>
|
||||
{isLoggedIn ? (
|
||||
<div className='grid grid-cols-1 gap-10'>
|
||||
<PlayLikedSongsCard />
|
||||
<RecentlyListened />
|
||||
<Collections />
|
||||
</div>
|
||||
<LayoutGroup>
|
||||
<div className='grid grid-cols-1 gap-10'>
|
||||
<PlayLikedSongsCard />
|
||||
<RecentlyListened />
|
||||
<Collections />
|
||||
</div>
|
||||
</LayoutGroup>
|
||||
) : (
|
||||
<PleaseLogin />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { resizeImage } from '@/web/utils/common'
|
|||
import { breakpoint as bp } from '@/web/utils/const'
|
||||
import useUser from '@/web/api/hooks/useUser'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
const Lyrics = ({ tracksIDs }: { tracksIDs: number[] }) => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -104,7 +105,8 @@ const PlayLikedSongsCard = () => {
|
|||
}, [likedSongsPlaylist?.playlist?.tracks, sampledTracks])
|
||||
|
||||
return (
|
||||
<div
|
||||
<motion.div
|
||||
layout
|
||||
className={cx(
|
||||
'mx-2.5 flex flex-col justify-between rounded-24 p-8 dark:bg-white/10 lg:mx-0',
|
||||
css`
|
||||
|
|
@ -141,7 +143,7 @@ const PlayLikedSongsCard = () => {
|
|||
<Icon name='forward' className='h-7 w-7 ' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import useUserListenedRecords from '@/web/api/hooks/useUserListenedRecords'
|
||||
import useArtists from '@/web/api/hooks/useArtists'
|
||||
import { useMemo } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import ArtistRow from '@/web/components/ArtistRow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
|
||||
const RecentlyListened = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: listenedRecords } = useUserListenedRecords({ type: 'week' })
|
||||
const { data: listenedRecords, isLoading } = useUserListenedRecords({ type: 'week' })
|
||||
const recentListenedArtistsIDs = useMemo(() => {
|
||||
const artists: {
|
||||
id: number
|
||||
|
|
@ -31,10 +32,26 @@ const RecentlyListened = () => {
|
|||
.slice(0, 5)
|
||||
.map(artist => artist.id)
|
||||
}, [listenedRecords])
|
||||
const { data: recentListenedArtists } = useArtists(recentListenedArtistsIDs)
|
||||
const artist = useMemo(() => recentListenedArtists?.map(a => a.artist), [recentListenedArtists])
|
||||
const { data: recentListenedArtists, isLoading: isLoadingArtistsDetail } =
|
||||
useArtists(recentListenedArtistsIDs)
|
||||
const artists = useMemo(() => recentListenedArtists?.map(a => a.artist), [recentListenedArtists])
|
||||
|
||||
return <ArtistRow artists={artist} placeholderRow={1} title={t`my.recently-listened`} />
|
||||
const show = useMemo(() => {
|
||||
if (listenedRecords?.weekData?.length === 0) return false
|
||||
if (isLoading || isLoadingArtistsDetail) return true
|
||||
if (artists?.length) return true
|
||||
return false
|
||||
}, [isLoading, artists, listenedRecords, isLoadingArtistsDetail])
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{show && (
|
||||
<motion.div layout exit={{ opacity: 0 }} transition={{ duration: 0.2 }}>
|
||||
<ArtistRow artists={artists} placeholderRow={1} title={t`my.recently-listened`} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecentlyListened
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
||||
import useUser from '@/web/api/hooks/useUser'
|
||||
import useUserPlaylists, {
|
||||
useMutationLikeAPlaylist,
|
||||
} from '@/web/api/hooks/useUserPlaylists'
|
||||
import useUserPlaylists, { useMutationLikeAPlaylist } from '@/web/api/hooks/useUserPlaylists'
|
||||
import TrackListHeader from '@/web/components/TrackListHeader'
|
||||
import player from '@/web/states/player'
|
||||
import { formatDate } from '@/web/utils/common'
|
||||
|
|
@ -32,8 +30,7 @@ const Header = () => {
|
|||
const extraInfo = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
Updated at {formatDate(playlist?.updateTime || 0, 'en')} ·{' '}
|
||||
{playlist?.trackCount} tracks
|
||||
Updated at {formatDate(playlist?.updateTime || 0, 'en')} · {playlist?.trackCount} tracks
|
||||
</>
|
||||
)
|
||||
}, [playlist])
|
||||
|
|
@ -64,8 +61,7 @@ const Header = () => {
|
|||
extraInfo,
|
||||
cover,
|
||||
isLiked,
|
||||
onLike:
|
||||
user?.account?.id === playlist?.creator?.userId ? undefined : onLike,
|
||||
onLike: user?.account?.id === playlist?.creator?.userId ? undefined : onLike,
|
||||
onPlay,
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import TrackList from './TrackList'
|
|||
import player from '@/web/states/player'
|
||||
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
||||
import Header from './Header'
|
||||
import useTracks from '@/web/api/hooks/useTracks'
|
||||
|
||||
const Playlist = () => {
|
||||
const params = useParams()
|
||||
|
|
@ -11,6 +12,13 @@ const Playlist = () => {
|
|||
id: Number(params.id),
|
||||
})
|
||||
|
||||
// TODO: 分页加载
|
||||
const { data: playlistTracks } = useTracks({
|
||||
ids: playlist?.playlist?.trackIds?.map(t => t.id) ?? [],
|
||||
})
|
||||
|
||||
console.log(playlistTracks)
|
||||
|
||||
const onPlay = async (trackID: number | null = null) => {
|
||||
await player.playPlaylist(playlist?.playlist?.id, trackID)
|
||||
}
|
||||
|
|
@ -20,7 +28,7 @@ const Playlist = () => {
|
|||
<Header />
|
||||
<div className='pb-10'>
|
||||
<TrackList
|
||||
tracks={playlist?.playlist?.tracks ?? []}
|
||||
tracks={playlistTracks?.songs ?? playlist?.playlist?.tracks ?? []}
|
||||
onPlay={onPlay}
|
||||
className='z-10 mt-10'
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import player from '@/web/states/player'
|
|||
import { formatDuration, resizeImage } from '@/web/utils/common'
|
||||
import { State as PlayerState } from '@/web/utils/player'
|
||||
import { css, cx } from '@emotion/css'
|
||||
import { Fragment } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
|
|
@ -58,7 +59,17 @@ const Track = ({
|
|||
)}
|
||||
</div>
|
||||
<div className='line-clamp-1 mt-1 text-14 font-bold text-white/30'>
|
||||
{track?.ar.map(a => a.name).join(', ')}
|
||||
{track?.ar.map((a, index) => (
|
||||
<Fragment key={a.id}>
|
||||
{index > 0 && ', '}
|
||||
<NavLink
|
||||
className='transition-all duration-300 hover:text-white/70'
|
||||
to={`/artist/${a.id}`}
|
||||
>
|
||||
{a.name}
|
||||
</NavLink>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -102,7 +113,7 @@ function TrackList({
|
|||
placeholderRows?: number
|
||||
}) {
|
||||
const { trackID, state } = useSnapshot(player)
|
||||
const playingTrackIndex = tracks?.findIndex(track => track.id === trackID) ?? -1
|
||||
const playingTrack = tracks?.find(track => track.id === trackID)
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLElement>, trackID: number) => {
|
||||
if (isLoading) return
|
||||
|
|
@ -129,7 +140,7 @@ function TrackList({
|
|||
key={track.id}
|
||||
track={track}
|
||||
index={index}
|
||||
playingTrackIndex={playingTrackIndex}
|
||||
playingTrackID={playingTrack?.id || 0}
|
||||
state={state}
|
||||
handleClick={handleClick}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ function Player() {
|
|||
}
|
||||
|
||||
function FindTrackOnYouTube() {
|
||||
const { t } = useTranslation()
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const { enableFindTrackOnYouTube, httpProxyForYouTube } = useSnapshot(settings)
|
||||
|
||||
|
|
@ -21,12 +21,18 @@ function FindTrackOnYouTube() {
|
|||
<div>
|
||||
<BlockTitle>{t`settings.player-youtube-unlock`}</BlockTitle>
|
||||
<BlockDescription>
|
||||
Find alternative track on YouTube if not available on NetEase.
|
||||
{t`settings.player-find-alternative-track-on-youtube-if-not-available-on-netease`}
|
||||
{i18n.language === 'zh-CN' && (
|
||||
<>
|
||||
<br />
|
||||
此功能需要开启 Clash for Windows 的 TUN Mode 或 ClashX Pro 的增强模式。
|
||||
</>
|
||||
)}
|
||||
</BlockDescription>
|
||||
|
||||
{/* Switch */}
|
||||
<Option>
|
||||
<OptionText>Enable YouTube Unlock </OptionText>
|
||||
<OptionText>Enable YouTube Unlock</OptionText>
|
||||
<Switch
|
||||
enabled={enableFindTrackOnYouTube}
|
||||
onChange={value => (settings.enableFindTrackOnYouTube = value)}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import useUser from '@/web/api/hooks/useUser'
|
||||
import Appearance from './Appearance'
|
||||
import { css, cx } from '@emotion/css'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import UserCard from './UserCard'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { motion, useAnimationControls } from 'framer-motion'
|
||||
|
|
@ -10,7 +10,7 @@ import Player from './Player'
|
|||
import PageTransition from '@/web/components/PageTransition'
|
||||
import { ease } from '@/web/utils/const'
|
||||
|
||||
export const categoryIds = ['general', 'appearance', 'player', 'lyrics', 'lab'] as const
|
||||
export const categoryIds = ['general', 'appearance', 'player', 'lab', 'about'] as const
|
||||
export type Category = typeof categoryIds[number]
|
||||
|
||||
const Sidebar = ({
|
||||
|
|
@ -25,22 +25,22 @@ const Sidebar = ({
|
|||
{ name: t`settings.general`, id: 'general' },
|
||||
{ name: t`settings.appearance`, id: 'appearance' },
|
||||
{ name: t`settings.player`, id: 'player' },
|
||||
{ name: t`settings.lyrics`, id: 'lyrics' },
|
||||
{ name: t`settings.lab`, id: 'lab' },
|
||||
{ name: t`settings.about`, id: 'about' },
|
||||
]
|
||||
const animation = useAnimationControls()
|
||||
|
||||
const onClick = (categoryId: Category) => {
|
||||
setActiveCategory(categoryId)
|
||||
const index = categories.findIndex(category => category.id === categoryId)
|
||||
animation.start({ y: index * 40 + 11.5 })
|
||||
}
|
||||
// Indicator animation
|
||||
const indicatorAnimation = useAnimationControls()
|
||||
useEffect(() => {
|
||||
const index = categories.findIndex(category => category.id === activeCategory)
|
||||
indicatorAnimation.start({ y: index * 40 + 11.5 })
|
||||
}, [activeCategory])
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<motion.div
|
||||
initial={{ y: 11.5 }}
|
||||
animate={animation}
|
||||
animate={indicatorAnimation}
|
||||
transition={{ type: 'spring', duration: 0.6, bounce: 0.36 }}
|
||||
className='absolute top-0 left-3 mr-2 h-4 w-1 rounded-full bg-brand-700'
|
||||
></motion.div>
|
||||
|
|
@ -48,7 +48,7 @@ const Sidebar = ({
|
|||
{categories.map(category => (
|
||||
<motion.div
|
||||
key={category.id}
|
||||
onClick={() => onClick(category.id)}
|
||||
onClick={() => setActiveCategory(category.id)}
|
||||
initial={{ x: activeCategory === category.id ? 12 : 0 }}
|
||||
animate={{ x: activeCategory === category.id ? 12 : 0 }}
|
||||
className={cx(
|
||||
|
|
@ -71,8 +71,8 @@ const Settings = () => {
|
|||
{ id: 'general', component: <General /> },
|
||||
{ id: 'appearance', component: <Appearance /> },
|
||||
{ id: 'player', component: <Player /> },
|
||||
{ id: 'lyrics', component: <span className='text-white'>开发中</span> },
|
||||
{ id: 'lab', component: <span className='text-white'>开发中</span> },
|
||||
{ id: 'about', component: <span className='text-white'>开发中</span> },
|
||||
]
|
||||
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue