mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-17 13:48:02 +00:00
feat: updates
This commit is contained in:
parent
c6c59b2cd9
commit
7ce516877e
63 changed files with 6591 additions and 1107 deletions
1
packages/web/.gitignore
vendored
Normal file
1
packages/web/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.vercel
|
||||
|
|
@ -8,11 +8,12 @@ import ScrollRestoration from '@/web/components/ScrollRestoration'
|
|||
import Toaster from './components/Toaster'
|
||||
|
||||
const App = () => {
|
||||
const isMobile = useIsMobile()
|
||||
// const isMobile = useIsMobile()
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
{isMobile ? <LayoutMobile /> : <Layout />}
|
||||
{/* {isMobile ? <LayoutMobile /> : <Layout />} */}
|
||||
<Layout />
|
||||
<Toaster />
|
||||
<ScrollRestoration />
|
||||
<IpcRendererReact />
|
||||
|
|
|
|||
25
packages/web/api/appleMusic.ts
Normal file
25
packages/web/api/appleMusic.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import request from '../utils/request'
|
||||
|
||||
// AppleMusic专辑
|
||||
export function fetchAppleMusicAlbum(
|
||||
params: FetchAppleMusicAlbumParams
|
||||
): Promise<FetchAppleMusicAlbumResponse> {
|
||||
return request({
|
||||
url: '/r3play/apple-music/album',
|
||||
method: 'get',
|
||||
params,
|
||||
baseURL: '/',
|
||||
})
|
||||
}
|
||||
|
||||
// AppleMusic艺人
|
||||
export function fetchAppleMusicArtist(
|
||||
params: FetchAppleMusicArtistParams
|
||||
): Promise<FetchAppleMusicArtistResponse> {
|
||||
return request({
|
||||
url: '/r3play/apple-music/artist',
|
||||
method: 'get',
|
||||
params,
|
||||
baseURL: '/',
|
||||
})
|
||||
}
|
||||
19
packages/web/api/hooks/useAppleMusicAlbum.ts
Normal file
19
packages/web/api/hooks/useAppleMusicAlbum.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchAppleMusicAlbum } from '../appleMusic'
|
||||
|
||||
const useAppleMusicAlbum = (id: string | number) => {
|
||||
return useQuery(
|
||||
['useAppleMusicAlbum', id],
|
||||
async () => {
|
||||
if (!id) return
|
||||
return fetchAppleMusicAlbum({ neteaseId: id })
|
||||
},
|
||||
{
|
||||
enabled: !!id,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchInterval: false,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export default useAppleMusicAlbum
|
||||
19
packages/web/api/hooks/useAppleMusicArtist.ts
Normal file
19
packages/web/api/hooks/useAppleMusicArtist.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchAppleMusicArtist } from '../appleMusic'
|
||||
|
||||
const useAppleMusicArtist = (id: string | number) => {
|
||||
return useQuery(
|
||||
['useAppleMusicArtist', id],
|
||||
async () => {
|
||||
if (!id) return
|
||||
return fetchAppleMusicArtist({ neteaseId: id })
|
||||
},
|
||||
{
|
||||
enabled: !!id,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchInterval: false,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export default useAppleMusicArtist
|
||||
|
|
@ -11,9 +11,7 @@ import {
|
|||
} from '@/shared/api/Track'
|
||||
|
||||
// 获取歌曲详情
|
||||
export function fetchTracks(
|
||||
params: FetchTracksParams
|
||||
): Promise<FetchTracksResponse> {
|
||||
export function fetchTracks(params: FetchTracksParams): Promise<FetchTracksResponse> {
|
||||
return request({
|
||||
url: '/song/detail',
|
||||
method: 'get',
|
||||
|
|
@ -28,16 +26,18 @@ export function fetchAudioSource(
|
|||
params: FetchAudioSourceParams
|
||||
): Promise<FetchAudioSourceResponse> {
|
||||
return request({
|
||||
url: '/song/url',
|
||||
url: '/song/url/v1',
|
||||
method: 'get',
|
||||
params,
|
||||
params: {
|
||||
level: 'exhigh',
|
||||
...params,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 获取歌词
|
||||
export function fetchLyric(
|
||||
params: FetchLyricParams
|
||||
): Promise<FetchLyricResponse> {
|
||||
export function fetchLyric(params: FetchLyricParams): Promise<FetchLyricResponse> {
|
||||
return request({
|
||||
url: '/lyric',
|
||||
method: 'get',
|
||||
|
|
@ -46,9 +46,7 @@ export function fetchLyric(
|
|||
}
|
||||
|
||||
// 收藏歌曲
|
||||
export function likeATrack(
|
||||
params: LikeATrackParams
|
||||
): Promise<LikeATrackResponse> {
|
||||
export function likeATrack(params: LikeATrackParams): Promise<LikeATrackResponse> {
|
||||
return request({
|
||||
url: '/like',
|
||||
method: 'post',
|
||||
|
|
|
|||
|
|
@ -28,12 +28,7 @@ const ArtistInline = ({
|
|||
if (!artists) return <div></div>
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
!className?.includes('line-clamp') && 'line-clamp-1',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={cx(!className?.includes('line-clamp') && 'line-clamp-1', className)}>
|
||||
{artists.map((artist, index) => (
|
||||
<span key={`${artist.id}-${artist.name}`}>
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { prefetchAlbum } from '@/web/api/hooks/useAlbum'
|
|||
import { prefetchPlaylist } from '@/web/api/hooks/usePlaylist'
|
||||
import { memo, useCallback } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import ArtistInline from './ArtistsInLine'
|
||||
import ArtistInline from './ArtistsInline'
|
||||
|
||||
type ItemTitle = undefined | 'name'
|
||||
type ItemSubTitle = undefined | 'artist' | 'year'
|
||||
|
|
@ -56,15 +56,9 @@ const Album = ({
|
|||
onMouseOver={prefetch}
|
||||
/>
|
||||
{title && (
|
||||
<div className='line-clamp-2 mt-2 text-14 font-medium text-neutral-300'>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{subtitle && (
|
||||
<div className='mt-1 text-14 font-medium text-neutral-700'>
|
||||
{subtitle}
|
||||
</div>
|
||||
<div className='line-clamp-2 mt-2 text-14 font-medium text-neutral-300'>{title}</div>
|
||||
)}
|
||||
{subtitle && <div className='mt-1 text-14 font-medium text-neutral-700'>{subtitle}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -107,21 +101,12 @@ 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>}
|
||||
|
||||
{/* Items */}
|
||||
<div className='grid grid-cols-3 gap-4 lg:gap-6 xl:grid-cols-4 2xl:grid-cols-5'>
|
||||
{albums?.map(album => (
|
||||
<Album
|
||||
key={album.id}
|
||||
album={album}
|
||||
itemTitle={itemTitle}
|
||||
itemSubtitle={itemSubtitle}
|
||||
/>
|
||||
<Album key={album.id} album={album} itemTitle={itemTitle} itemSubtitle={itemSubtitle} />
|
||||
))}
|
||||
{playlists?.map(playlist => (
|
||||
<Playlist key={playlist.id} playlist={playlist} />
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import Main from '@/web/components/Main'
|
|||
import Player from '@/web/components/Player'
|
||||
import MenuBar from '@/web/components/MenuBar'
|
||||
import Topbar from '@/web/components/Topbar/TopbarDesktop'
|
||||
import { cx } from '@emotion/css'
|
||||
import { css, cx } from '@emotion/css'
|
||||
import player from '@/web/states/player'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import Login from './Login'
|
||||
|
|
@ -22,7 +22,10 @@ const Layout = () => {
|
|||
id='layout'
|
||||
className={cx(
|
||||
'relative grid h-screen select-none overflow-hidden bg-white dark:bg-black',
|
||||
window.env?.isElectron && !fullscreen && 'rounded-24'
|
||||
window.env?.isElectron && !fullscreen && 'rounded-24',
|
||||
css`
|
||||
min-width: 720px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<BlurBackground />
|
||||
|
|
@ -40,9 +43,7 @@ const Layout = () => {
|
|||
|
||||
{(window.env?.isWindows ||
|
||||
window.env?.isLinux ||
|
||||
window.localStorage.getItem('showWindowsTitleBar') === 'true') && (
|
||||
<TitleBar />
|
||||
)}
|
||||
window.localStorage.getItem('showWindowsTitleBar') === 'true') && <TitleBar />}
|
||||
|
||||
<ContextMenus />
|
||||
|
||||
|
|
|
|||
|
|
@ -23,25 +23,19 @@ const LayoutMobile = () => {
|
|||
<Router />
|
||||
</main>
|
||||
<div
|
||||
className={cx(
|
||||
'fixed bottom-0 left-0 right-0 z-20 pt-3 dark:bg-black',
|
||||
css`
|
||||
padding-bottom: calc(
|
||||
className={cx('fixed bottom-0 left-0 right-0 z-20 pt-3 dark:bg-black')}
|
||||
style={{
|
||||
paddingBottom: `calc(
|
||||
${isIosPwa ? '24px' : 'env(safe-area-inset-bottom)'} + 0.75rem
|
||||
);
|
||||
`
|
||||
)}
|
||||
)`,
|
||||
}}
|
||||
>
|
||||
{showPlayer && (
|
||||
<div
|
||||
className={cx(
|
||||
'absolute left-7 right-7 z-20',
|
||||
css`
|
||||
top: calc(
|
||||
-100% - 6px + ${isIosPwa ? '24px' : 'env(safe-area-inset-bottom)'}
|
||||
);
|
||||
`
|
||||
)}
|
||||
className={cx('absolute left-7 right-7 z-20')}
|
||||
style={{
|
||||
top: `calc(-100% - 6px + ${isIosPwa ? '24px' : 'env(safe-area-inset-bottom)'})`,
|
||||
}}
|
||||
>
|
||||
<Player />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,13 +11,7 @@ import persistedUiStates from '@/web/states/persistedUiStates'
|
|||
import useUser from '@/web/api/hooks/useUser'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const OR = ({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick: () => void
|
||||
}) => {
|
||||
const OR = ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
|
|
@ -125,10 +119,9 @@ const Login = () => {
|
|||
<motion.div
|
||||
animate={animateCard}
|
||||
className={cx(
|
||||
'relative rounded-48 bg-white/10 p-9',
|
||||
'relative h-fit rounded-48 bg-white/10 p-9',
|
||||
css`
|
||||
width: 392px;
|
||||
height: fit-content;
|
||||
`
|
||||
)}
|
||||
>
|
||||
|
|
@ -136,9 +129,7 @@ const Login = () => {
|
|||
{cardType === 'phone/email' && <LoginWithPhoneOrEmail />}
|
||||
|
||||
<OR onClick={handleSwitchCard}>
|
||||
{cardType === 'qrCode'
|
||||
? t`auth.use-phone-or-email`
|
||||
: t`auth.scan-qr-code`}
|
||||
{cardType === 'qrCode' ? t`auth.use-phone-or-email` : t`auth.scan-qr-code`}
|
||||
</OR>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import useUserLikedTracksIDs, {
|
||||
useMutationLikeATrack,
|
||||
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
||||
import useUserLikedTracksIDs, { useMutationLikeATrack } from '@/web/api/hooks/useUserLikedTracksIDs'
|
||||
import player from '@/web/states/player'
|
||||
import { resizeImage } from '@/web/utils/common'
|
||||
|
||||
|
|
@ -30,8 +28,7 @@ const PlayingTrack = () => {
|
|||
[playerSnapshot.trackListSource]
|
||||
)
|
||||
|
||||
const hasListSource =
|
||||
playerSnapshot.mode !== PlayerMode.FM && trackListSource?.type
|
||||
const hasListSource = playerSnapshot.mode !== PlayerMode.FM && trackListSource?.type
|
||||
|
||||
const toTrackListSource = () => {
|
||||
if (!hasListSource) return
|
||||
|
|
@ -76,16 +73,10 @@ const LikeButton = ({ track }: { track: Track | undefined | null }) => {
|
|||
|
||||
return (
|
||||
<div className='mr-1 '>
|
||||
<IconButton
|
||||
onClick={() => track?.id && mutationLikeATrack.mutate(track.id)}
|
||||
>
|
||||
<IconButton onClick={() => track?.id && mutationLikeATrack.mutate(track.id)}>
|
||||
<Icon
|
||||
className='h-6 w-6 text-white'
|
||||
name={
|
||||
track?.id && userLikedSongs?.ids?.includes(track.id)
|
||||
? 'heart'
|
||||
: 'heart-outline'
|
||||
}
|
||||
name={track?.id && userLikedSongs?.ids?.includes(track.id) ? 'heart' : 'heart-outline'}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
|
|
@ -101,10 +92,7 @@ const Controls = () => {
|
|||
return (
|
||||
<div className='flex items-center justify-center gap-2 text-white'>
|
||||
{mode === PlayerMode.TrackList && (
|
||||
<IconButton
|
||||
onClick={() => track && player.prevTrack()}
|
||||
disabled={!track}
|
||||
>
|
||||
<IconButton onClick={() => track && player.prevTrack()} disabled={!track}>
|
||||
<Icon className='h-6 w-6' name='previous' />
|
||||
</IconButton>
|
||||
)}
|
||||
|
|
@ -120,11 +108,7 @@ const Controls = () => {
|
|||
>
|
||||
<Icon
|
||||
className='h-7 w-7'
|
||||
name={
|
||||
[PlayerState.Playing, PlayerState.Loading].includes(state)
|
||||
? 'pause'
|
||||
: 'play'
|
||||
}
|
||||
name={[PlayerState.Playing, PlayerState.Loading].includes(state) ? 'pause' : 'play'}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton onClick={() => track && player.nextTrack()} disabled={!track}>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css'
|
|||
import player from '@/web/states/player'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import ArtistInline from '@/web/components/ArtistsInline'
|
||||
import ArtistInline from '../ArtistsInline'
|
||||
import persistedUiStates from '@/web/states/persistedUiStates'
|
||||
import Controls from './Controls'
|
||||
import Cover from './Cover'
|
||||
|
|
@ -34,9 +34,7 @@ const NowPlaying = () => {
|
|||
{/* Info & Controls */}
|
||||
<div className='m-3 flex flex-col items-center rounded-20 bg-white/60 p-5 font-medium backdrop-blur-3xl dark:bg-black/70'>
|
||||
{/* Track Info */}
|
||||
<div className='line-clamp-1 text-lg text-black dark:text-white'>
|
||||
{track?.name}
|
||||
</div>
|
||||
<div className='line-clamp-1 text-lg text-black dark:text-white'>{track?.name}</div>
|
||||
<ArtistInline
|
||||
artists={track?.ar || []}
|
||||
className='text-black/30 dark:text-white/30'
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const Tabs = ({
|
|||
value,
|
||||
onChange,
|
||||
className,
|
||||
style,
|
||||
}: {
|
||||
tabs: {
|
||||
id: string
|
||||
|
|
@ -13,9 +14,10 @@ const Tabs = ({
|
|||
value: string
|
||||
onChange: (id: string) => void
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
}) => {
|
||||
return (
|
||||
<div className={cx('no-scrollbar flex overflow-y-auto', className)}>
|
||||
<div className={cx('no-scrollbar flex overflow-y-auto', className)} style={style}>
|
||||
{tabs.map(tab => (
|
||||
<div
|
||||
key={tab.id}
|
||||
|
|
|
|||
|
|
@ -30,11 +30,9 @@ const Background = () => {
|
|||
transition={{ ease }}
|
||||
className={cx(
|
||||
'absolute inset-0 z-0 bg-contain bg-repeat-x',
|
||||
window.env?.isElectron && 'rounded-t-24',
|
||||
css`
|
||||
background-image: url(${topbarBackground});
|
||||
`
|
||||
window.env?.isElectron && 'rounded-t-24'
|
||||
)}
|
||||
style={{ backgroundImage: `url(${topbarBackground})` }}
|
||||
></motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -67,14 +67,14 @@ const Info = ({
|
|||
)}
|
||||
|
||||
{/* Description */}
|
||||
{!isMobile && (
|
||||
{!isMobile && description && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className='line-clamp-3 mt-6 whitespace-pre-wrap text-14 font-bold dark:text-white/40'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: description || '',
|
||||
__html: description,
|
||||
}}
|
||||
></motion.div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
export default function useAppleMusicAlbum(props: {
|
||||
id?: number
|
||||
name?: string
|
||||
artist?: string
|
||||
}) {
|
||||
const { id, name, artist } = props
|
||||
return useQuery(
|
||||
['useAppleMusicAlbum', props],
|
||||
async () => {
|
||||
if (!id || !name || !artist) return
|
||||
return window.ipcRenderer?.invoke(IpcChannels.GetAlbumFromAppleMusic, {
|
||||
id,
|
||||
name,
|
||||
artist,
|
||||
})
|
||||
},
|
||||
{
|
||||
enabled: !!id && !!name && !!artist,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchInterval: false,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import { AppleMusicArtist } from '@/shared/AppleMusic'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
export default function useAppleMusicArtist(props: {
|
||||
id?: number
|
||||
name?: string
|
||||
}) {
|
||||
const { id, name } = props
|
||||
return useQuery(
|
||||
['useAppleMusicArtist', props],
|
||||
async () => {
|
||||
if (!id || !name) return
|
||||
|
||||
const cache = await window.ipcRenderer?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.AppleMusicArtist,
|
||||
query: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
|
||||
if (cache) return cache
|
||||
|
||||
return window.ipcRenderer?.invoke(IpcChannels.GetArtistFromAppleMusic, {
|
||||
id,
|
||||
name,
|
||||
})
|
||||
},
|
||||
{
|
||||
enabled: !!id && !!name,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchInterval: false,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
content="script-src 'self' 'unsafe-inline' www.googletagmanager.com blob:;" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<title>R3Play</title>
|
||||
<title>R3PLAY</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import useAlbum from '@/web/api/hooks/useAlbum'
|
||||
import useUserAlbums, {
|
||||
useMutationLikeAAlbum,
|
||||
} from '@/web/api/hooks/useUserAlbums'
|
||||
import useUserAlbums, { useMutationLikeAAlbum } from '@/web/api/hooks/useUserAlbums'
|
||||
import Icon from '@/web/components/Icon'
|
||||
import TrackListHeader from '@/web/components/TrackListHeader'
|
||||
import useAppleMusicAlbum from '@/web/hooks/useAppleMusicAlbum'
|
||||
import useVideoCover from '@/web/hooks/useVideoCover'
|
||||
import player from '@/web/states/player'
|
||||
import { formatDuration } from '@/web/utils/common'
|
||||
|
|
@ -13,6 +10,7 @@ import { useMemo } from 'react'
|
|||
import toast from 'react-hot-toast'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useAppleMusicAlbum from '@/web/api/hooks/useAppleMusicAlbum'
|
||||
|
||||
const Header = () => {
|
||||
const { t, i18n } = useTranslation()
|
||||
|
|
@ -25,51 +23,31 @@ const Header = () => {
|
|||
})
|
||||
const album = useMemo(() => albumRaw?.album, [albumRaw])
|
||||
|
||||
const { data: albumFromApple, isLoading: isLoadingAlbumFromApple } =
|
||||
useAppleMusicAlbum({
|
||||
id: album?.id,
|
||||
name: album?.name,
|
||||
artist: album?.artist.name,
|
||||
})
|
||||
|
||||
const { data: videoCoverFromRemote } = useVideoCover({
|
||||
id: album?.id,
|
||||
name: album?.name,
|
||||
artist: album?.artist.name,
|
||||
enabled: !window.env?.isElectron,
|
||||
})
|
||||
const { data: appleMusicAlbum, isLoading: isLoadingAppleMusicAlbum } = useAppleMusicAlbum(
|
||||
album?.id || 0
|
||||
)
|
||||
|
||||
// For <Cover />
|
||||
const cover = album?.picUrl
|
||||
const videoCover =
|
||||
albumFromApple?.attributes?.editorialVideo?.motionSquareVideo1x1?.video ||
|
||||
videoCoverFromRemote?.video
|
||||
const videoCover = appleMusicAlbum?.editorialVideo
|
||||
|
||||
// For <Info />
|
||||
const title = album?.name
|
||||
const creatorName = album?.artist.name
|
||||
const creatorLink = `/artist/${album?.artist.id}`
|
||||
const description = isLoadingAlbumFromApple
|
||||
const description = isLoadingAppleMusicAlbum
|
||||
? ''
|
||||
: albumFromApple?.attributes?.editorialNotes?.standard || album?.description
|
||||
: appleMusicAlbum?.editorialNote?.[i18n.language.replace('-', '_')] || album?.description
|
||||
const extraInfo = useMemo(() => {
|
||||
const duration = album?.songs?.reduce((acc, cur) => acc + cur.dt, 0) || 0
|
||||
const albumDuration = formatDuration(
|
||||
duration,
|
||||
i18n.language,
|
||||
'hh[hr] mm[min]'
|
||||
)
|
||||
const albumDuration = formatDuration(duration, i18n.language, 'hh[hr] mm[min]')
|
||||
return (
|
||||
<>
|
||||
{album?.mark === 1056768 && (
|
||||
<Icon
|
||||
name='explicit'
|
||||
className='mb-px mr-1 h-3 w-3 lg:h-3.5 lg:w-3.5'
|
||||
/>
|
||||
<Icon name='explicit' className='mb-px mr-1 h-3 w-3 lg:h-3.5 lg:w-3.5' />
|
||||
)}{' '}
|
||||
{dayjs(album?.publishTime || 0).year()} ·{' '}
|
||||
{t('common.track-with-count', { count: album?.songs?.length })},{' '}
|
||||
{albumDuration}
|
||||
{t('common.track-with-count', { count: album?.songs?.length })}, {albumDuration}
|
||||
</>
|
||||
)
|
||||
}, [album?.mark, album?.publishTime, album?.songs, i18n.language, t])
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ const ArtistVideos = () => {
|
|||
const params = useParams()
|
||||
const { data: videos } = useArtistMV({ id: Number(params.id) || 0 })
|
||||
|
||||
if (!videos?.mvs?.length) return null
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-6 mt-10 text-12 font-medium uppercase text-neutral-300'>
|
||||
|
|
@ -16,17 +18,12 @@ const ArtistVideos = () => {
|
|||
|
||||
<div className='grid grid-cols-3 gap-6'>
|
||||
{videos?.mvs?.slice(0, 6)?.map(video => (
|
||||
<div
|
||||
key={video.id}
|
||||
onClick={() => (uiStates.playingVideoID = video.id)}
|
||||
>
|
||||
<div key={video.id} onClick={() => (uiStates.playingVideoID = video.id)}>
|
||||
<img
|
||||
src={video.imgurl16v9}
|
||||
className='aspect-video w-full rounded-24 border border-white/5 object-contain'
|
||||
/>
|
||||
<div className='mt-2 text-12 font-medium text-neutral-600'>
|
||||
{video.name}
|
||||
</div>
|
||||
<div className='mt-2 text-12 font-medium text-neutral-600'>{video.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,16 @@
|
|||
import useIsMobile from '@/web/hooks/useIsMobile'
|
||||
import useAppleMusicArtist from '@/web/hooks/useAppleMusicArtist'
|
||||
import useAppleMusicArtist from '@/web/api/hooks/useAppleMusicArtist'
|
||||
import { cx, css } from '@emotion/css'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import i18next from 'i18next'
|
||||
|
||||
const ArtistInfo = ({
|
||||
artist,
|
||||
isLoading,
|
||||
}: {
|
||||
artist?: Artist
|
||||
isLoading: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const ArtistInfo = ({ artist, isLoading }: { artist?: Artist; isLoading: boolean }) => {
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const isMobile = useIsMobile()
|
||||
const { data: artistFromApple, isLoading: isLoadingArtistFromApple } =
|
||||
useAppleMusicArtist({
|
||||
id: artist?.id,
|
||||
name: artist?.name,
|
||||
})
|
||||
const { data: artistFromApple, isLoading: isLoadingArtistFromApple } = useAppleMusicArtist(
|
||||
artist?.id || 0
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -27,9 +20,7 @@ const ArtistInfo = ({
|
|||
<span className='rounded-full bg-white/10'>PLACEHOLDER</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-28 font-semibold text-white/70 lg:text-32'>
|
||||
{artist?.name}
|
||||
</div>
|
||||
<div className='text-28 font-semibold text-white/70 lg:text-32'>{artist?.name}</div>
|
||||
)}
|
||||
|
||||
{/* Type */}
|
||||
|
|
@ -38,9 +29,7 @@ const ArtistInfo = ({
|
|||
<span className='rounded-full bg-white/10'>Artist</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='mt-2.5 text-24 font-medium text-white/40 lg:mt-6'>
|
||||
Artist
|
||||
</div>
|
||||
<div className='mt-2.5 text-24 font-medium text-white/40 lg:mt-6'>Artist</div>
|
||||
)}
|
||||
|
||||
{/* Counts */}
|
||||
|
|
@ -67,9 +56,7 @@ const ArtistInfo = ({
|
|||
`
|
||||
)}
|
||||
>
|
||||
<span className='rounded-full bg-white/10'>
|
||||
PLACEHOLDER1234567890
|
||||
</span>
|
||||
<span className='rounded-full bg-white/10'>PLACEHOLDER1234567890</span>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
|
|
@ -80,7 +67,7 @@ const ArtistInfo = ({
|
|||
`
|
||||
)}
|
||||
>
|
||||
{artistFromApple?.attributes?.artistBio || artist?.briefDesc}
|
||||
{artistFromApple?.artistBio?.[i18n.language.replace('-', '_')] || artist?.briefDesc}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,18 @@
|
|||
import { isIOS, isSafari, resizeImage } from '@/web/utils/common'
|
||||
import { resizeImage } from '@/web/utils/common'
|
||||
import Image from '@/web/components/Image'
|
||||
import { cx, css } from '@emotion/css'
|
||||
import useAppleMusicArtist from '@/web/hooks/useAppleMusicArtist'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Hls from 'hls.js'
|
||||
import { motion } from 'framer-motion'
|
||||
import useAppleMusicArtist from '@/web/api/hooks/useAppleMusicArtist'
|
||||
import { useEffect } from 'react'
|
||||
import uiStates from '@/web/states/uiStates'
|
||||
import VideoCover from '@/web/components/VideoCover'
|
||||
|
||||
const Cover = ({ artist }: { artist?: Artist }) => {
|
||||
const { data: artistFromApple, isLoading: isLoadingArtistFromApple } =
|
||||
useAppleMusicArtist({
|
||||
id: artist?.id,
|
||||
name: artist?.name,
|
||||
})
|
||||
const { data: artistFromApple, isLoading: isLoadingArtistFromApple } = useAppleMusicArtist(
|
||||
artist?.id || 0
|
||||
)
|
||||
|
||||
const video =
|
||||
artistFromApple?.attributes?.editorialVideo?.motionArtistSquare1x1?.video
|
||||
const cover = isLoadingArtistFromApple
|
||||
? ''
|
||||
: artistFromApple?.attributes?.artwork?.url || artist?.img1v1Url
|
||||
const video = artistFromApple?.editorialVideo
|
||||
const cover = isLoadingArtistFromApple ? '' : artistFromApple?.artwork || artist?.img1v1Url || ''
|
||||
|
||||
useEffect(() => {
|
||||
if (cover) uiStates.blurBackgroundImage = cover
|
||||
|
|
@ -40,14 +33,7 @@ const Cover = ({ artist }: { artist?: Artist }) => {
|
|||
'aspect-square h-full w-full lg:z-10',
|
||||
video ? 'opacity-0' : 'opacity-100'
|
||||
)}
|
||||
src={resizeImage(
|
||||
isLoadingArtistFromApple
|
||||
? ''
|
||||
: artistFromApple?.attributes?.artwork?.url ||
|
||||
artist?.img1v1Url ||
|
||||
'',
|
||||
'lg'
|
||||
)}
|
||||
src={resizeImage(isLoadingArtistFromApple ? '' : cover, 'lg')}
|
||||
/>
|
||||
|
||||
{video && <VideoCover source={video} />}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import Tabs from '@/web/components/Tabs'
|
||||
import {
|
||||
fetchDailyRecommendPlaylists,
|
||||
fetchRecommendedPlaylists,
|
||||
} from '@/web/api/playlist'
|
||||
import { fetchDailyRecommendPlaylists, fetchRecommendedPlaylists } from '@/web/api/playlist'
|
||||
import { PlaylistApiNames } from '@/shared/api/Playlists'
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
|
@ -33,10 +30,7 @@ const Recommend = () => {
|
|||
const playlists =
|
||||
isLoadingDaily || isLoading
|
||||
? []
|
||||
: [
|
||||
...(dailyRecommendPlaylists?.recommend || []),
|
||||
...(recommendedPlaylists?.result || []),
|
||||
]
|
||||
: [...(dailyRecommendPlaylists?.recommend || []), ...(recommendedPlaylists?.result || [])]
|
||||
|
||||
return <CoverRowVirtual playlists={playlists} />
|
||||
|
||||
|
|
@ -69,10 +63,12 @@ const Browse = () => {
|
|||
'pointer-events-none fixed top-0 left-10 z-10 hidden lg:block',
|
||||
css`
|
||||
height: 230px;
|
||||
right: ${playerWidth + 32}px;
|
||||
background-image: url(${topbarBackground});
|
||||
`
|
||||
)}
|
||||
style={{
|
||||
right: `${playerWidth + 32}px`,
|
||||
backgroundImage: `url(${topbarBackground})`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
<Tabs
|
||||
|
|
|
|||
|
|
@ -17,13 +17,12 @@ import { throttle } from 'lodash-es'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import VideoRow from '@/web/components/VideoRow'
|
||||
import useUserVideos from '@/web/api/hooks/useUserVideos'
|
||||
import persistedUiStates from '@/web/states/persistedUiStates'
|
||||
|
||||
const Albums = () => {
|
||||
const { data: albums } = useUserAlbums()
|
||||
|
||||
return (
|
||||
<CoverRow albums={albums?.data} itemTitle='name' itemSubtitle='artist' />
|
||||
)
|
||||
return <CoverRow albums={albums?.data} itemTitle='name' itemSubtitle='artist' />
|
||||
}
|
||||
|
||||
const Playlists = () => {
|
||||
|
|
@ -65,9 +64,8 @@ const CollectionTabs = ({ showBg }: { showBg: boolean }) => {
|
|||
]
|
||||
|
||||
const { librarySelectedTab: selectedTab } = useSnapshot(uiStates)
|
||||
const setSelectedTab = (
|
||||
id: 'playlists' | 'albums' | 'artists' | 'videos'
|
||||
) => {
|
||||
const { minimizePlayer } = useSnapshot(persistedUiStates)
|
||||
const setSelectedTab = (id: 'playlists' | 'albums' | 'artists' | 'videos') => {
|
||||
uiStates.librarySelectedTab = id
|
||||
}
|
||||
|
||||
|
|
@ -81,13 +79,16 @@ const CollectionTabs = ({ showBg }: { showBg: boolean }) => {
|
|||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className={cx(
|
||||
'pointer-events-none fixed top-0 left-10 z-10 hidden lg:block',
|
||||
'pointer-events-none fixed top-0 right-0 left-10 z-10 hidden lg:block',
|
||||
css`
|
||||
height: 230px;
|
||||
right: ${playerWidth + 32}px;
|
||||
background-image: url(${topbarBackground});
|
||||
background-repeat: repeat;
|
||||
`
|
||||
)}
|
||||
style={{
|
||||
right: `${minimizePlayer ? 0 : playerWidth + 32}px`,
|
||||
backgroundImage: `url(${topbarBackground})`,
|
||||
}}
|
||||
></motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
@ -99,12 +100,10 @@ const CollectionTabs = ({ showBg }: { showBg: boolean }) => {
|
|||
setSelectedTab(id)
|
||||
scrollToBottom(true)
|
||||
}}
|
||||
className={cx(
|
||||
'sticky z-10 -mb-10 px-2.5 lg:px-0',
|
||||
css`
|
||||
top: ${topbarHeight}px;
|
||||
`
|
||||
)}
|
||||
className={cx('sticky z-10 -mb-10 px-2.5 lg:px-0')}
|
||||
style={{
|
||||
top: `${topbarHeight}px`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
@ -114,8 +113,7 @@ const Collections = () => {
|
|||
const { librarySelectedTab: selectedTab } = useSnapshot(uiStates)
|
||||
|
||||
const observePoint = useRef<HTMLDivElement | null>(null)
|
||||
const { onScreen: isScrollReachBottom } =
|
||||
useIntersectionObserver(observePoint)
|
||||
const { onScreen: isScrollReachBottom } = useIntersectionObserver(observePoint)
|
||||
|
||||
const onScroll = throttle(() => {
|
||||
if (isScrollReachBottom) return
|
||||
|
|
@ -126,13 +124,11 @@ const Collections = () => {
|
|||
<div>
|
||||
<CollectionTabs showBg={isScrollReachBottom} />
|
||||
<div
|
||||
className={cx(
|
||||
'no-scrollbar overflow-y-auto px-2.5 pt-16 pb-16 lg:px-0',
|
||||
css`
|
||||
height: calc(100vh - ${topbarHeight}px);
|
||||
`
|
||||
)}
|
||||
className={cx('no-scrollbar overflow-y-auto px-2.5 pt-16 pb-16 lg:px-0')}
|
||||
onScroll={onScroll}
|
||||
style={{
|
||||
height: `calc(100vh - ${topbarHeight}px)`,
|
||||
}}
|
||||
>
|
||||
{selectedTab === 'albums' && <Albums />}
|
||||
{selectedTab === 'playlists' && <Playlists />}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const appName = 'R3Play'
|
||||
export const appName = 'R3PLAY'
|
||||
|
||||
// 动画曲线
|
||||
export const ease: [number, number, number, number] = [0.4, 0, 0.2, 1]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/// <reference types="vitest" />
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import dotenv from 'dotenv'
|
||||
import path, { join } from 'path'
|
||||
import { join } from 'path'
|
||||
import { defineConfig } from 'vite'
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
|
|
@ -9,7 +9,7 @@ import { VitePWA } from 'vite-plugin-pwa'
|
|||
import filenamesToType from './vitePluginFilenamesToType'
|
||||
import { appName } from './utils/const'
|
||||
|
||||
dotenv.config({ path: path.resolve(process.cwd(), '../../.env') })
|
||||
dotenv.config({ path: join(__dirname, '../../.env') })
|
||||
const IS_ELECTRON = process.env.IS_ELECTRON
|
||||
|
||||
/**
|
||||
|
|
@ -37,33 +37,33 @@ export default defineConfig({
|
|||
/**
|
||||
* @see https://vite-plugin-pwa.netlify.app/guide/generate.html
|
||||
*/
|
||||
// VitePWA({
|
||||
// registerType: 'autoUpdate',
|
||||
// manifest: {
|
||||
// name: appName,
|
||||
// short_name: appName,
|
||||
// description: 'Description of your app',
|
||||
// theme_color: '#000',
|
||||
// icons: [
|
||||
// {
|
||||
// src: 'pwa-192x192.png',
|
||||
// sizes: '192x192',
|
||||
// type: 'image/png',
|
||||
// },
|
||||
// {
|
||||
// src: 'pwa-512x512.png',
|
||||
// sizes: '512x512',
|
||||
// type: 'image/png',
|
||||
// },
|
||||
// {
|
||||
// src: 'pwa-512x512.png',
|
||||
// sizes: '512x512',
|
||||
// type: 'image/png',
|
||||
// purpose: 'any maskable',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// }),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
manifest: {
|
||||
name: appName,
|
||||
short_name: appName,
|
||||
description: 'Description of your app',
|
||||
theme_color: '#000',
|
||||
icons: [
|
||||
{
|
||||
src: 'pwa-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'pwa-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'pwa-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* @see https://github.com/vbenjs/vite-plugin-svg-icons
|
||||
|
|
@ -90,27 +90,36 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
server: {
|
||||
port: Number(process.env['ELECTRON_WEB_SERVER_PORT'] || 42710),
|
||||
port: Number(process.env.ELECTRON_WEB_SERVER_PORT || 42710),
|
||||
strictPort: IS_ELECTRON ? true : false,
|
||||
proxy: {
|
||||
'/netease/': {
|
||||
// target: `http://192.168.50.111:${
|
||||
target: `http://127.0.0.1:${process.env.ELECTRON_DEV_NETEASE_API_PORT || 30001}`,
|
||||
changeOrigin: true,
|
||||
rewrite: path => (IS_ELECTRON ? path : path.replace(/^\/netease/, '')),
|
||||
},
|
||||
[`/${appName.toLowerCase()}/video-cover`]: {
|
||||
target: `http://168.138.40.199:51324`,
|
||||
changeOrigin: true,
|
||||
},
|
||||
[`/${appName.toLowerCase()}/`]: {
|
||||
'/r3play/': {
|
||||
target: `http://127.0.0.1:${process.env.ELECTRON_DEV_NETEASE_API_PORT || 30001}`,
|
||||
changeOrigin: true,
|
||||
},
|
||||
// [`/${appName.toLowerCase()}/apple-music/`]: {
|
||||
// target: `http://168.138.174.244:35530/`,
|
||||
// changeOrigin: true,
|
||||
// rewrite: path => path.replace(/^\/r3play/, ''),
|
||||
// },
|
||||
// [`/${appName.toLowerCase()}/`]: {
|
||||
// target: `http://127.0.0.1:${process.env.ELECTRON_DEV_NETEASE_API_PORT || 30001}`,
|
||||
// changeOrigin: true,
|
||||
// },
|
||||
// '/': {
|
||||
// target: `http://127.0.0.1:${process.env.ELECTRON_DEV_NETEASE_API_PORT || 30001}`,
|
||||
// changeOrigin: true,
|
||||
// // rewrite: path => (IS_ELECTRON ? path : path.replace(/^\/netease/, '')),
|
||||
// },
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
port: Number(process.env['ELECTRON_WEB_SERVER_PORT'] || 42710),
|
||||
port: Number(process.env.ELECTRON_WEB_SERVER_PORT || 42710),
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue