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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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小时刷新一次

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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