mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-16 21:28:06 +00:00
feat: updates
This commit is contained in:
parent
0b4baa3eff
commit
222fb02355
77 changed files with 654 additions and 551 deletions
|
|
@ -1,84 +1,56 @@
|
||||||
import { logger } from '@sentry/utils'
|
import log from './log'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
// 'https://mvod.itunes.apple.com/itunes-assets/HLSMusic116/v4/de/52/95/de52957b-fcf1-ae96-b114-0445cb8c41d4/P359420813_default.m3u8'
|
const token =
|
||||||
|
'Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IldlYlBsYXlLaWQifQ.eyJpc3MiOiJBTVBXZWJQbGF5IiwiaWF0IjoxNjQ2NjU1MDgwLCJleHAiOjE2NjIyMDcwODB9.pyOrt2FmP0cHkzYtO8KiEzQL2t1qpRszzxIYbLH7faXSddo6PQei771Ja3aGwGVU4hD99lZAw7bwat60iBcGiQ'
|
||||||
const searchAlbum = async (
|
|
||||||
keyword: string
|
|
||||||
): Promise<
|
|
||||||
| {
|
|
||||||
id: string
|
|
||||||
href: string
|
|
||||||
attributes: {
|
|
||||||
artistName: string
|
|
||||||
url: string
|
|
||||||
name: string
|
|
||||||
editorialNotes?: {
|
|
||||||
standard: string
|
|
||||||
short: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
> => {
|
|
||||||
const r = await axios.get(
|
|
||||||
`https://amp-api.music.apple.com/v1/catalog/cn/search`,
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
term: keyword,
|
|
||||||
l: 'zh-cn',
|
|
||||||
platform: 'web',
|
|
||||||
types: 'albums',
|
|
||||||
limit: 1,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
authorization: 'Bearer xxxxxx', // required token
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return r.data?.results?.albums?.data?.[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCoverVideo = async ({
|
export const getCoverVideo = async ({
|
||||||
name,
|
name,
|
||||||
artists,
|
artist,
|
||||||
}: {
|
}: {
|
||||||
name: string
|
name: string
|
||||||
artists: string[]
|
artist: string
|
||||||
}): Promise<string | undefined> => {
|
}): Promise<string | undefined> => {
|
||||||
const keyword = `${artists.join(' ')} ${name}`
|
const keyword = `${artist} ${name}`
|
||||||
logger.debug(`[appleMusic] getCoverVideo: ${keyword}`)
|
log.debug(`[appleMusic] getCoverVideo: ${keyword}`)
|
||||||
const album = await searchAlbum(keyword).catch(e => {
|
const searchResult = await axios({
|
||||||
console.log(e)
|
method: 'GET',
|
||||||
logger.debug('[appleMusic] Search album error', e)
|
url: 'https://amp-api.music.apple.com/v1/catalog/us/search',
|
||||||
|
params: {
|
||||||
|
term: keyword,
|
||||||
|
types: 'albums',
|
||||||
|
'fields[albums]': 'artistName,name,editorialVideo',
|
||||||
|
platform: 'web',
|
||||||
|
limit: '1',
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authority: 'amp-api.music.apple.com',
|
||||||
|
Accept: '*/*',
|
||||||
|
Authorization: token,
|
||||||
|
Referer: 'http://localhost:9000/',
|
||||||
|
'Sec-Fetch-Dest': 'empty',
|
||||||
|
'Sec-Fetch-Mode': 'cors',
|
||||||
|
'Sec-Fetch-Site': 'cross-site',
|
||||||
|
'User-Agent':
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Cider/1.5.1 Chrome/100.0.4896.160 Electron/18.3.3 Safari/537.36',
|
||||||
|
'Accept-Encoding': 'gzip',
|
||||||
|
},
|
||||||
|
}).catch(e => {
|
||||||
|
log.debug('[appleMusic] Search album error', e)
|
||||||
})
|
})
|
||||||
|
|
||||||
const url = album?.attributes.url
|
const album = searchResult?.data?.results?.albums?.data?.[0]
|
||||||
|
if (!album) {
|
||||||
if (!url) {
|
log.debug('[appleMusic] No album found on apple music')
|
||||||
logger.info('[appleMusic] no url')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.debug(
|
||||||
let { data: html } = await axios.get(url)
|
`[appleMusic] Got ${album?.id}: ${album?.attributes?.name} by ${album?.attributes?.artistName}`
|
||||||
if (!html) return
|
|
||||||
|
|
||||||
const regex =
|
|
||||||
/<script type="fastboot\/shoebox" id="shoebox-media-api-cache-amp-music">(.*?)<\/script>/
|
|
||||||
html = html
|
|
||||||
.match(regex)[0]
|
|
||||||
.replace(
|
|
||||||
'<script type="fastboot/shoebox" id="shoebox-media-api-cache-amp-music">',
|
|
||||||
''
|
|
||||||
)
|
)
|
||||||
.replace('</script>', '')
|
|
||||||
html = JSON.parse(html)
|
|
||||||
const data = JSON.parse(html[Object.keys(html)[1]])
|
|
||||||
const m3u8 =
|
|
||||||
data?.d?.[0]?.attributes?.editorialVideo?.motionSquareVideo1x1?.video
|
|
||||||
|
|
||||||
logger.debug(`[appleMusic] ${m3u8}`)
|
const url = album?.attributes?.editorialVideo?.motionSquareVideo1x1?.video
|
||||||
|
if (!url) {
|
||||||
return m3u8
|
log.debug('[appleMusic] Album does not have video cover')
|
||||||
|
}
|
||||||
|
return url
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class Cache {
|
||||||
case APIs.RecommendResource:
|
case APIs.RecommendResource:
|
||||||
case APIs.UserAlbums:
|
case APIs.UserAlbums:
|
||||||
case APIs.UserArtists:
|
case APIs.UserArtists:
|
||||||
|
case APIs.ListenedRecords:
|
||||||
case APIs.Likelist: {
|
case APIs.Likelist: {
|
||||||
if (!data) return
|
if (!data) return
|
||||||
db.upsert(Tables.AccountData, {
|
db.upsert(Tables.AccountData, {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import { createTray, YPMTray } from './tray'
|
||||||
import { IpcChannels } from '@/shared/IpcChannels'
|
import { IpcChannels } from '@/shared/IpcChannels'
|
||||||
import { createTaskbar, Thumbar } from './windowsTaskbar'
|
import { createTaskbar, Thumbar } from './windowsTaskbar'
|
||||||
import { createMenu } from './menu'
|
import { createMenu } from './menu'
|
||||||
import { Store as State, initialState } from '@/shared/store'
|
|
||||||
import { isDev, isWindows, isLinux, isMac } from './utils'
|
import { isDev, isWindows, isLinux, isMac } from './utils'
|
||||||
|
|
||||||
export interface TypedElectronStore {
|
export interface TypedElectronStore {
|
||||||
|
|
@ -26,7 +25,7 @@ export interface TypedElectronStore {
|
||||||
x?: number
|
x?: number
|
||||||
y?: number
|
y?: number
|
||||||
}
|
}
|
||||||
settings: State['settings']
|
// settings: State['settings']
|
||||||
}
|
}
|
||||||
|
|
||||||
class Main {
|
class Main {
|
||||||
|
|
@ -39,7 +38,7 @@ class Main {
|
||||||
width: 1440,
|
width: 1440,
|
||||||
height: 1024,
|
height: 1024,
|
||||||
},
|
},
|
||||||
settings: initialState.settings,
|
// settings: initialState.settings,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -155,19 +155,17 @@ function initOtherIpcMain() {
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 缓存动态专辑封面
|
* 获取动态封面
|
||||||
*/
|
*/
|
||||||
on(IpcChannels.SetVideoCover, (event, args) => {
|
handle(IpcChannels.GetVideoCover, async (event, { id, name, artist }) => {
|
||||||
const { id, url } = args
|
const fromCache = cache.get(APIs.VideoCover, { id })
|
||||||
cache.set(APIs.VideoCover, { id, url })
|
if (fromCache) {
|
||||||
})
|
return fromCache === 'no' ? undefined : fromCache
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
const fromApple = await getCoverVideo({ name, artist })
|
||||||
* 获取动态专辑封面
|
cache.set(APIs.VideoCover, { id, url: fromApple || 'no' })
|
||||||
*/
|
return fromApple
|
||||||
on(IpcChannels.GetVideoCover, (event, args) => {
|
|
||||||
const { id } = args
|
|
||||||
event.returnValue = cache.get(APIs.VideoCover, { id })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
} from './api/Artist'
|
} from './api/Artist'
|
||||||
import { FetchAlbumResponse } from './api/Album'
|
import { FetchAlbumResponse } from './api/Album'
|
||||||
import {
|
import {
|
||||||
|
FetchListenedRecordsResponse,
|
||||||
FetchUserAccountResponse,
|
FetchUserAccountResponse,
|
||||||
FetchUserAlbumsResponse,
|
FetchUserAlbumsResponse,
|
||||||
FetchUserArtistsResponse,
|
FetchUserArtistsResponse,
|
||||||
|
|
@ -37,6 +38,7 @@ export const enum APIs {
|
||||||
UserArtists = 'artist/sublist',
|
UserArtists = 'artist/sublist',
|
||||||
UserPlaylist = 'user/playlist',
|
UserPlaylist = 'user/playlist',
|
||||||
SimilarArtist = 'simi/artist',
|
SimilarArtist = 'simi/artist',
|
||||||
|
ListenedRecords = 'user/record',
|
||||||
|
|
||||||
// not netease api
|
// not netease api
|
||||||
CoverColor = 'cover_color',
|
CoverColor = 'cover_color',
|
||||||
|
|
@ -61,6 +63,7 @@ export interface APIsParams {
|
||||||
[APIs.SimilarArtist]: { id: number }
|
[APIs.SimilarArtist]: { id: number }
|
||||||
[APIs.CoverColor]: { id: number }
|
[APIs.CoverColor]: { id: number }
|
||||||
[APIs.VideoCover]: { id: number }
|
[APIs.VideoCover]: { id: number }
|
||||||
|
[APIs.ListenedRecords]: { id: number; type: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface APIsResponse {
|
export interface APIsResponse {
|
||||||
|
|
@ -81,4 +84,5 @@ export interface APIsResponse {
|
||||||
[APIs.SimilarArtist]: FetchSimilarArtistsResponse
|
[APIs.SimilarArtist]: FetchSimilarArtistsResponse
|
||||||
[APIs.CoverColor]: string | undefined
|
[APIs.CoverColor]: string | undefined
|
||||||
[APIs.VideoCover]: string | undefined
|
[APIs.VideoCover]: string | undefined
|
||||||
|
[APIs.ListenedRecords]: FetchListenedRecordsResponse
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { APIs } from './CacheAPIs'
|
import { APIs } from './CacheAPIs'
|
||||||
import { RepeatMode } from './playerDataTypes'
|
import { RepeatMode } from './playerDataTypes'
|
||||||
import { Store } from '@/shared/store'
|
|
||||||
|
|
||||||
export const enum IpcChannels {
|
export const enum IpcChannels {
|
||||||
ClearAPICache = 'ClearAPICache',
|
ClearAPICache = 'ClearAPICache',
|
||||||
|
|
@ -57,10 +56,10 @@ export interface IpcChannelsParams {
|
||||||
[IpcChannels.Repeat]: {
|
[IpcChannels.Repeat]: {
|
||||||
mode: RepeatMode
|
mode: RepeatMode
|
||||||
}
|
}
|
||||||
[IpcChannels.SyncSettings]: Store['settings']
|
[IpcChannels.SyncSettings]: any
|
||||||
[IpcChannels.GetAudioCacheSize]: void
|
[IpcChannels.GetAudioCacheSize]: void
|
||||||
[IpcChannels.ResetWindowSize]: void
|
[IpcChannels.ResetWindowSize]: void
|
||||||
[IpcChannels.GetVideoCover]: { id: number }
|
[IpcChannels.GetVideoCover]: { id: number; name: string; artist: string }
|
||||||
[IpcChannels.SetVideoCover]: { id: number; url: string }
|
[IpcChannels.SetVideoCover]: { id: number; url: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,3 +107,17 @@ export interface FetchUserArtistsResponse {
|
||||||
count: number
|
count: number
|
||||||
data: Artist[]
|
data: Artist[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 听歌排名
|
||||||
|
export interface FetchListenedRecordsParams {
|
||||||
|
uid: number // 用户id
|
||||||
|
type: number // type=1 时只返回 weekData, type=0 时返回 allData
|
||||||
|
}
|
||||||
|
export interface FetchListenedRecordsResponse {
|
||||||
|
code: number
|
||||||
|
weekData: {
|
||||||
|
playCount: number
|
||||||
|
score: number
|
||||||
|
song: Track
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
export interface Store {
|
|
||||||
uiStates: {
|
|
||||||
showLyricPanel: boolean
|
|
||||||
showLoginPanel: boolean
|
|
||||||
}
|
|
||||||
persistedUiStates: {
|
|
||||||
loginPhoneCountryCode: string
|
|
||||||
loginType: 'phone' | 'email' | 'qrCode'
|
|
||||||
}
|
|
||||||
settings: {
|
|
||||||
showSidebar: boolean
|
|
||||||
accentColor: string
|
|
||||||
unm: {
|
|
||||||
enabled: boolean
|
|
||||||
sources: Array<
|
|
||||||
'migu' | 'kuwo' | 'kugou' | 'ytdl' | 'qq' | 'bilibili' | 'joox'
|
|
||||||
>
|
|
||||||
searchMode: 'order-first' | 'fast-first'
|
|
||||||
proxy: null | {
|
|
||||||
protocol: 'http' | 'https' | 'socks5'
|
|
||||||
host: string
|
|
||||||
port: number
|
|
||||||
username?: string
|
|
||||||
password?: string
|
|
||||||
}
|
|
||||||
cookies: {
|
|
||||||
qq?: string
|
|
||||||
joox?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initialState: Store = {
|
|
||||||
uiStates: {
|
|
||||||
showLyricPanel: false,
|
|
||||||
showLoginPanel: false,
|
|
||||||
},
|
|
||||||
persistedUiStates: {
|
|
||||||
loginPhoneCountryCode: '+86',
|
|
||||||
loginType: 'qrCode',
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
showSidebar: true,
|
|
||||||
accentColor: 'blue',
|
|
||||||
unm: {
|
|
||||||
enabled: true,
|
|
||||||
sources: ['migu'],
|
|
||||||
searchMode: 'order-first',
|
|
||||||
proxy: null,
|
|
||||||
cookies: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -13,7 +13,7 @@ const App = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<div className='dark'>
|
<div>
|
||||||
{window.env?.isEnableTitlebar && <TitleBar />}
|
{window.env?.isEnableTitlebar && <TitleBar />}
|
||||||
{isMobile ? <LayoutMobile /> : <Layout />}
|
{isMobile ? <LayoutMobile /> : <Layout />}
|
||||||
<Toaster position='bottom-center' containerStyle={{ bottom: '5rem' }} />
|
<Toaster position='bottom-center' containerStyle={{ bottom: '5rem' }} />
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { IpcChannels } from '@/shared/IpcChannels'
|
||||||
import useUserLikedTracksIDs, {
|
import useUserLikedTracksIDs, {
|
||||||
useMutationLikeATrack,
|
useMutationLikeATrack,
|
||||||
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import useIpcRenderer from '@/web/hooks/useIpcRenderer'
|
import useIpcRenderer from '@/web/hooks/useIpcRenderer'
|
||||||
import { State as PlayerState } from '@/web/utils/player'
|
import { State as PlayerState } from '@/web/utils/player'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import {
|
import { fetchListenedRecords } from '@/web/api/user'
|
||||||
fetchListenedRecords,
|
import { UserApiNames, FetchListenedRecordsResponse } from '@/shared/api/User'
|
||||||
FetchListenedRecordsParams,
|
|
||||||
} from '@/web/api/user'
|
|
||||||
import { UserApiNames } from '@/shared/api/User'
|
|
||||||
import { APIs } from '@/shared/CacheAPIs'
|
import { APIs } from '@/shared/CacheAPIs'
|
||||||
import { IpcChannels } from '@/shared/IpcChannels'
|
import { IpcChannels } from '@/shared/IpcChannels'
|
||||||
import { useQuery } from 'react-query'
|
import { useQuery } from 'react-query'
|
||||||
|
|
@ -24,10 +21,10 @@ export default function useUserListenedRecords(params: {
|
||||||
{
|
{
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
enabled: !!uid,
|
enabled: !!uid,
|
||||||
// placeholderData: (): FetchUserArtistsResponse =>
|
placeholderData: (): FetchListenedRecordsResponse =>
|
||||||
// window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, {
|
||||||
// api: APIs.UserArtists,
|
api: APIs.UserArtists,
|
||||||
// }),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { IpcChannels } from '@/shared/IpcChannels'
|
||||||
import { APIs } from '@/shared/CacheAPIs'
|
import { APIs } from '@/shared/CacheAPIs'
|
||||||
import { fetchUserPlaylists } from '@/web/api/user'
|
import { fetchUserPlaylists } from '@/web/api/user'
|
||||||
import { FetchUserPlaylistsResponse, UserApiNames } from '@/shared/api/User'
|
import { FetchUserPlaylistsResponse, UserApiNames } from '@/shared/api/User'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
export default function useUserPlaylists() {
|
export default function useUserPlaylists() {
|
||||||
const { data: user } = useUser()
|
const { data: user } = useUser()
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import {
|
||||||
FetchUserAlbumsParams,
|
FetchUserAlbumsParams,
|
||||||
FetchUserAlbumsResponse,
|
FetchUserAlbumsResponse,
|
||||||
FetchUserArtistsResponse,
|
FetchUserArtistsResponse,
|
||||||
|
FetchListenedRecordsParams,
|
||||||
|
FetchListenedRecordsResponse,
|
||||||
} from '@/shared/api/User'
|
} from '@/shared/api/User'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -78,18 +80,7 @@ export function scrobble(params: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FetchListenedRecordsParams {
|
// 用户最近听歌排名
|
||||||
uid: number // 用户id
|
|
||||||
type: number // type=1 时只返回 weekData, type=0 时返回 allData
|
|
||||||
}
|
|
||||||
export interface FetchListenedRecordsResponse {
|
|
||||||
code: number
|
|
||||||
weekData: {
|
|
||||||
playCount: number
|
|
||||||
score: number
|
|
||||||
song: Track
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
export function fetchListenedRecords(
|
export function fetchListenedRecords(
|
||||||
params: FetchListenedRecordsParams
|
params: FetchListenedRecordsParams
|
||||||
): Promise<FetchListenedRecordsResponse> {
|
): Promise<FetchListenedRecordsResponse> {
|
||||||
|
|
|
||||||
3
packages/web/assets/icons/player-handler.svg
Normal file
3
packages/web/assets/icons/player-handler.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="47" height="10" viewBox="0 0 47 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M37.6555 8.15086C28.0962 5.28305 17.9054 5.28305 8.34605 8.15086L3.86283 9.49582C2.27585 9.97192 0.603405 9.07137 0.127311 7.48439C-0.348783 5.89741 0.551768 4.22496 2.13875 3.74887L6.62196 2.4039C17.306 -0.801301 28.6956 -0.801301 39.3796 2.4039L43.8628 3.74887C45.4498 4.22496 46.3504 5.89741 45.8743 7.48439C45.3982 9.07137 43.7257 9.97192 42.1387 9.49582L37.6555 8.15086Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 551 B |
|
|
@ -1,4 +1,4 @@
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { resizeImage } from '@/web/utils/common'
|
import { resizeImage } from '@/web/utils/common'
|
||||||
import Icon from './Icon'
|
import Icon from './Icon'
|
||||||
import ArtistInline from './ArtistsInline'
|
import ArtistInline from './ArtistsInline'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import useLyric from '@/web/api/hooks/useLyric'
|
import useLyric from '@/web/api/hooks/useLyric'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { lyricParser } from '@/web/utils/lyric'
|
import { lyricParser } from '@/web/utils/lyric'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import useLyric from '@/web/api/hooks/useLyric'
|
import useLyric from '@/web/api/hooks/useLyric'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { motion, useMotionValue } from 'framer-motion'
|
import { motion, useMotionValue } from 'framer-motion'
|
||||||
import { lyricParser } from '@/web/utils/lyric'
|
import { lyricParser } from '@/web/utils/lyric'
|
||||||
import { useWindowSize } from 'react-use'
|
import { useWindowSize } from 'react-use'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import Player from './Player'
|
import Player from './Player'
|
||||||
import { player, state } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { getCoverColor } from '@/web/utils/common'
|
import { getCoverColor } from '@/web/utils/common'
|
||||||
import { colord } from 'colord'
|
import { colord } from 'colord'
|
||||||
import IconButton from '../IconButton'
|
import IconButton from '../IconButton'
|
||||||
|
|
@ -13,7 +13,7 @@ import { useMemo } from 'react'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
|
|
||||||
const LyricPanel = () => {
|
const LyricPanel = () => {
|
||||||
const stateSnapshot = useSnapshot(state)
|
const stateSnapshot = useSnapshot(player)
|
||||||
const playerSnapshot = useSnapshot(player)
|
const playerSnapshot = useSnapshot(player)
|
||||||
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
|
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
|
||||||
|
|
||||||
|
|
@ -55,7 +55,11 @@ const LyricPanel = () => {
|
||||||
<Lyric2 className='col-span-7' />
|
<Lyric2 className='col-span-7' />
|
||||||
|
|
||||||
<div className='absolute bottom-3.5 right-7 text-white'>
|
<div className='absolute bottom-3.5 right-7 text-white'>
|
||||||
<IconButton onClick={() => (state.uiStates.showLyricPanel = false)}>
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
//
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Icon className='h-6 w-6' name='lyrics' />
|
<Icon className='h-6 w-6' name='lyrics' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import useUserLikedTracksIDs, {
|
import useUserLikedTracksIDs, {
|
||||||
useMutationLikeATrack,
|
useMutationLikeATrack,
|
||||||
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
||||||
import { player, state } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { resizeImage } from '@/web/utils/common'
|
import { resizeImage } from '@/web/utils/common'
|
||||||
|
|
||||||
import ArtistInline from '../ArtistsInline'
|
import ArtistInline from '../ArtistsInline'
|
||||||
|
|
@ -23,7 +23,6 @@ const PlayingTrack = () => {
|
||||||
const id = track?.al?.id
|
const id = track?.al?.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
navigate(`/album/${id}`)
|
navigate(`/album/${id}`)
|
||||||
state.uiStates.showLyricPanel = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackListSource = useMemo(
|
const trackListSource = useMemo(
|
||||||
|
|
@ -38,12 +37,10 @@ const PlayingTrack = () => {
|
||||||
if (!hasListSource) return
|
if (!hasListSource) return
|
||||||
|
|
||||||
navigate(`/${trackListSource.type}/${trackListSource.id}`)
|
navigate(`/${trackListSource.type}/${trackListSource.id}`)
|
||||||
state.uiStates.showLyricPanel = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toArtist = (id: number) => {
|
const toArtist = (id: number) => {
|
||||||
navigate(`/artist/${id}`)
|
navigate(`/artist/${id}`)
|
||||||
state.uiStates.showLyricPanel = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
38
packages/web/components/New/BlurBackground.tsx
Normal file
38
packages/web/components/New/BlurBackground.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { resizeImage } from '@/web/utils/common'
|
||||||
|
import { cx, css } from '@emotion/css'
|
||||||
|
import useIsMobile from '@/web/hooks/useIsMobile'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
import uiStates from '@/web/states/uiStates'
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import { ease } from '@/web/utils/const'
|
||||||
|
|
||||||
|
const BlurBackground = ({ cover }: { cover?: string }) => {
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
const { hideTopbarBackground } = useSnapshot(uiStates)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{!isMobile && cover && hideTopbarBackground && (
|
||||||
|
<motion.img
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ ease }}
|
||||||
|
className={cx(
|
||||||
|
'absolute z-0 object-cover opacity-70',
|
||||||
|
css`
|
||||||
|
top: -400px;
|
||||||
|
left: -370px;
|
||||||
|
width: 1572px;
|
||||||
|
height: 528px;
|
||||||
|
filter: blur(256px) saturate(1.2);
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
src={resizeImage(cover, 'sm')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlurBackground
|
||||||
|
|
@ -46,8 +46,6 @@ const CoverWall = ({
|
||||||
sizes[album.large ? 'large' : 'small'][breakpoint]
|
sizes[album.large ? 'large' : 'small'][breakpoint]
|
||||||
)}
|
)}
|
||||||
key={album.id}
|
key={album.id}
|
||||||
alt='Album Cover'
|
|
||||||
placeholder={null}
|
|
||||||
className={cx(
|
className={cx(
|
||||||
'aspect-square h-full w-full rounded-20 lg:rounded-24',
|
'aspect-square h-full w-full rounded-20 lg:rounded-24',
|
||||||
album.large && 'col-span-2 row-span-2'
|
album.large && 'col-span-2 row-span-2'
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ import Player from '@/web/components/New/Player'
|
||||||
import MenuBar from '@/web/components/New/MenuBar'
|
import MenuBar from '@/web/components/New/MenuBar'
|
||||||
import Topbar from '@/web/components/New/Topbar/TopbarDesktop'
|
import Topbar from '@/web/components/New/Topbar/TopbarDesktop'
|
||||||
import { css, cx } from '@emotion/css'
|
import { css, cx } from '@emotion/css'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import Login from './Login'
|
import Login from './Login'
|
||||||
|
import TrafficLight from './TrafficLight'
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
const playerSnapshot = useSnapshot(player)
|
const playerSnapshot = useSnapshot(player)
|
||||||
|
|
@ -39,6 +40,12 @@ const Layout = () => {
|
||||||
<Main />
|
<Main />
|
||||||
<Login />
|
<Login />
|
||||||
{showPlayer && <Player />}
|
{showPlayer && <Player />}
|
||||||
|
|
||||||
|
{window.env?.isMac && (
|
||||||
|
<div className='fixed top-6 left-6 z-30 translate-y-0.5'>
|
||||||
|
<TrafficLight />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import Player from '@/web/components/New/PlayerMobile'
|
import Player from '@/web/components/New/PlayerMobile'
|
||||||
import { css, cx } from '@emotion/css'
|
import { css, cx } from '@emotion/css'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import Router from '@/web/components/New/Router'
|
import Router from '@/web/components/New/Router'
|
||||||
import MenuBar from './MenuBar'
|
import MenuBar from './MenuBar'
|
||||||
|
|
@ -17,8 +17,8 @@ const LayoutMobile = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id='layout' className='bg-white select-none pb-28 dark:bg-black'>
|
<div id='layout' className='select-none bg-white pb-28 dark:bg-black'>
|
||||||
<main className='min-h-screen pb-16 overflow-x-hidden overflow-y-auto'>
|
<main className='min-h-screen overflow-y-auto overflow-x-hidden pb-16 '>
|
||||||
{location.pathname === '/' && <Topbar />}
|
{location.pathname === '/' && <Topbar />}
|
||||||
<Router />
|
<Router />
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -48,7 +48,7 @@ const LayoutMobile = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<MenuBar />
|
<MenuBar />
|
||||||
{/* <PlayingNext /> */}
|
<PlayingNext />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Login />
|
<Login />
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { cx, css } from '@emotion/css'
|
import { cx, css } from '@emotion/css'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { state } from '@/web/store'
|
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
|
import uiStates from '@/web/states/uiStates'
|
||||||
import { AnimatePresence, motion, useAnimation } from 'framer-motion'
|
import { AnimatePresence, motion, useAnimation } from 'framer-motion'
|
||||||
import { ease } from '@/web/utils/const'
|
import { ease } from '@/web/utils/const'
|
||||||
import Icon from '@/web/components/Icon'
|
import Icon from '@/web/components/Icon'
|
||||||
import LoginWithPhoneOrEmail from './LoginWithPhoneOrEmail'
|
import LoginWithPhoneOrEmail from './LoginWithPhoneOrEmail'
|
||||||
import LoginWithQRCode from './LoginWithQRCode'
|
import LoginWithQRCode from './LoginWithQRCode'
|
||||||
|
import persistedUiStates from '@/web/states/persistedUiStates'
|
||||||
|
|
||||||
const OR = ({
|
const OR = ({
|
||||||
children,
|
children,
|
||||||
|
|
@ -37,9 +37,10 @@ const OR = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const { uiStates, persistedUiStates } = useSnapshot(state)
|
const { loginType } = useSnapshot(persistedUiStates)
|
||||||
|
const { showLoginPanel } = useSnapshot(uiStates)
|
||||||
const [cardType, setCardType] = useState<'qrCode' | 'phone/email'>(
|
const [cardType, setCardType] = useState<'qrCode' | 'phone/email'>(
|
||||||
persistedUiStates.loginType === 'qrCode' ? 'qrCode' : 'phone/email'
|
loginType === 'qrCode' ? 'qrCode' : 'phone/email'
|
||||||
)
|
)
|
||||||
|
|
||||||
const animateCard = useAnimation()
|
const animateCard = useAnimation()
|
||||||
|
|
@ -52,8 +53,7 @@ const Login = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
setCardType(cardType === 'qrCode' ? 'phone/email' : 'qrCode')
|
setCardType(cardType === 'qrCode' ? 'phone/email' : 'qrCode')
|
||||||
state.persistedUiStates.loginType =
|
persistedUiStates.loginType = cardType === 'qrCode' ? 'phone' : 'qrCode'
|
||||||
cardType === 'qrCode' ? 'phone' : 'qrCode'
|
|
||||||
|
|
||||||
await animateCard.start({
|
await animateCard.start({
|
||||||
rotateY: 0,
|
rotateY: 0,
|
||||||
|
|
@ -66,7 +66,7 @@ const Login = () => {
|
||||||
<>
|
<>
|
||||||
{/* Blur bg */}
|
{/* Blur bg */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{uiStates.showLoginPanel && (
|
{showLoginPanel && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className='fixed inset-0 z-30 bg-black/80 backdrop-blur-3xl lg:rounded-24'
|
className='fixed inset-0 z-30 bg-black/80 backdrop-blur-3xl lg:rounded-24'
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
|
|
@ -79,7 +79,7 @@ const Login = () => {
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{uiStates.showLoginPanel && (
|
{showLoginPanel && (
|
||||||
<div className='fixed inset-0 z-30 flex justify-center rounded-24 pt-56'>
|
<div className='fixed inset-0 z-30 flex justify-center rounded-24 pt-56'>
|
||||||
<motion.div
|
<motion.div
|
||||||
className='flex flex-col items-center'
|
className='flex flex-col items-center'
|
||||||
|
|
@ -134,7 +134,7 @@ const Login = () => {
|
||||||
<motion.div
|
<motion.div
|
||||||
layout='position'
|
layout='position'
|
||||||
transition={{ ease }}
|
transition={{ ease }}
|
||||||
onClick={() => (state.uiStates.showLoginPanel = false)}
|
onClick={() => (uiStates.showLoginPanel = false)}
|
||||||
className='mt-10 flex h-14 w-14 items-center justify-center rounded-full bg-white/10'
|
className='mt-10 flex h-14 w-14 items-center justify-center rounded-full bg-white/10'
|
||||||
>
|
>
|
||||||
<Icon name='x' className='h-7 w-7 text-white/50' />
|
<Icon name='x' className='h-7 w-7 text-white/50' />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { cx, css } from '@emotion/css'
|
import { cx, css } from '@emotion/css'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { state } from '@/web/store'
|
|
||||||
import { useMutation } from 'react-query'
|
import { useMutation } from 'react-query'
|
||||||
import { loginWithEmail, loginWithPhone } from '@/web/api/auth'
|
import { loginWithEmail, loginWithPhone } from '@/web/api/auth'
|
||||||
import md5 from 'md5'
|
import md5 from 'md5'
|
||||||
|
|
@ -9,17 +8,20 @@ import { setCookies } from '@/web/utils/cookie'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { ease } from '@/web/utils/const'
|
import { ease } from '@/web/utils/const'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
|
import uiStates from '@/web/states/uiStates'
|
||||||
|
import persistedUiStates from '@/web/states/persistedUiStates'
|
||||||
|
|
||||||
const LoginWithPhoneOrEmail = () => {
|
const LoginWithPhoneOrEmail = () => {
|
||||||
const { persistedUiStates } = useSnapshot(state)
|
const { loginPhoneCountryCode, loginType: persistedLoginType } =
|
||||||
|
useSnapshot(persistedUiStates)
|
||||||
const [email, setEmail] = useState<string>('')
|
const [email, setEmail] = useState<string>('')
|
||||||
const [countryCode, setCountryCode] = useState<string>(
|
const [countryCode, setCountryCode] = useState<string>(
|
||||||
persistedUiStates.loginPhoneCountryCode || '+86'
|
loginPhoneCountryCode || '+86'
|
||||||
)
|
)
|
||||||
const [phone, setPhone] = useState<string>('')
|
const [phone, setPhone] = useState<string>('')
|
||||||
const [password, setPassword] = useState<string>('')
|
const [password, setPassword] = useState<string>('')
|
||||||
const [loginType, setLoginType] = useState<'phone' | 'email'>(
|
const [loginType, setLoginType] = useState<'phone' | 'email'>(
|
||||||
persistedUiStates.loginType === 'email' ? 'email' : 'phone'
|
persistedLoginType === 'email' ? 'email' : 'phone'
|
||||||
)
|
)
|
||||||
|
|
||||||
const doEmailLogin = useMutation(
|
const doEmailLogin = useMutation(
|
||||||
|
|
@ -36,7 +38,7 @@ const LoginWithPhoneOrEmail = () => {
|
||||||
}
|
}
|
||||||
setCookies(result.cookie)
|
setCookies(result.cookie)
|
||||||
|
|
||||||
state.uiStates.showLoginPanel = false
|
uiStates.showLoginPanel = false
|
||||||
},
|
},
|
||||||
onError: error => {
|
onError: error => {
|
||||||
toast(`Login failed: ${error}`)
|
toast(`Login failed: ${error}`)
|
||||||
|
|
@ -80,7 +82,7 @@ const LoginWithPhoneOrEmail = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setCookies(result.cookie)
|
setCookies(result.cookie)
|
||||||
state.uiStates.showLoginPanel = false
|
uiStates.showLoginPanel = false
|
||||||
},
|
},
|
||||||
onError: error => {
|
onError: error => {
|
||||||
toast(`Login failed: ${error}`)
|
toast(`Login failed: ${error}`)
|
||||||
|
|
@ -132,7 +134,7 @@ const LoginWithPhoneOrEmail = () => {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const type = loginType === 'phone' ? 'email' : 'phone'
|
const type = loginType === 'phone' ? 'email' : 'phone'
|
||||||
setLoginType(type)
|
setLoginType(type)
|
||||||
state.persistedUiStates.loginType = type
|
persistedUiStates.loginType = type
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Phone
|
Phone
|
||||||
|
|
@ -165,7 +167,7 @@ const LoginWithPhoneOrEmail = () => {
|
||||||
<input
|
<input
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setCountryCode(e.target.value)
|
setCountryCode(e.target.value)
|
||||||
state.persistedUiStates.loginPhoneCountryCode = e.target.value
|
persistedUiStates.loginPhoneCountryCode = e.target.value
|
||||||
}}
|
}}
|
||||||
className={cx(
|
className={cx(
|
||||||
'my-3.5 flex-shrink-0 bg-transparent',
|
'my-3.5 flex-shrink-0 bg-transparent',
|
||||||
|
|
@ -181,7 +183,7 @@ const LoginWithPhoneOrEmail = () => {
|
||||||
onChange={e => setPhone(e.target.value)}
|
onChange={e => setPhone(e.target.value)}
|
||||||
className='my-3.5 flex-grow appearance-none bg-transparent'
|
className='my-3.5 flex-grow appearance-none bg-transparent'
|
||||||
placeholder='Phone'
|
placeholder='Phone'
|
||||||
type='number'
|
type='tel'
|
||||||
value={phone}
|
value={phone}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -198,10 +200,11 @@ const LoginWithPhoneOrEmail = () => {
|
||||||
initial='hidden'
|
initial='hidden'
|
||||||
animate='show'
|
animate='show'
|
||||||
exit='hidden'
|
exit='hidden'
|
||||||
|
className='w-full'
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
onChange={e => setEmail(e.target.value)}
|
onChange={e => setEmail(e.target.value)}
|
||||||
className='flex-grow appearance-none bg-transparent'
|
className='w-full flex-grow appearance-none bg-transparent'
|
||||||
placeholder='Email'
|
placeholder='Email'
|
||||||
type='email'
|
type='email'
|
||||||
value={email}
|
value={email}
|
||||||
|
|
@ -215,7 +218,7 @@ const LoginWithPhoneOrEmail = () => {
|
||||||
<div className='mt-4 flex items-center rounded-12 bg-black/50 p-3 text-16 font-medium text-night-50'>
|
<div className='mt-4 flex items-center rounded-12 bg-black/50 p-3 text-16 font-medium text-night-50'>
|
||||||
<input
|
<input
|
||||||
onChange={e => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
className='bg-transparent'
|
className='w-full bg-transparent'
|
||||||
placeholder='Password'
|
placeholder='Password'
|
||||||
type='password'
|
type='password'
|
||||||
value={password}
|
value={password}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,11 @@
|
||||||
import { cx, css } from '@emotion/css'
|
import { cx, css } from '@emotion/css'
|
||||||
import { useEffect, useState, useMemo } from 'react'
|
import { useEffect, useState, useMemo } from 'react'
|
||||||
import qrCode from 'qrcode'
|
import qrCode from 'qrcode'
|
||||||
import { state } from '@/web/store'
|
import { useQuery } from 'react-query'
|
||||||
import { useSnapshot } from 'valtio'
|
import { checkLoginQrCodeStatus, fetchLoginQrCodeKey } from '@/web/api/auth'
|
||||||
import { useMutation, useQuery } from 'react-query'
|
|
||||||
import {
|
|
||||||
checkLoginQrCodeStatus,
|
|
||||||
fetchLoginQrCodeKey,
|
|
||||||
loginWithEmail,
|
|
||||||
loginWithPhone,
|
|
||||||
} from '@/web/api/auth'
|
|
||||||
import md5 from 'md5'
|
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { setCookies } from '@/web/utils/cookie'
|
import { setCookies } from '@/web/utils/cookie'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import uiStates from '@/web/states/uiStates'
|
||||||
import { ease } from '@/web/utils/const'
|
|
||||||
import Icon from '@/web/components/Icon'
|
|
||||||
|
|
||||||
const QRCode = ({ className, text }: { className?: string; text: string }) => {
|
const QRCode = ({ className, text }: { className?: string; text: string }) => {
|
||||||
const [image, setImage] = useState<string>('')
|
const [image, setImage] = useState<string>('')
|
||||||
|
|
@ -90,7 +80,7 @@ const LoginWithQRCode = () => {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
setCookies(status.cookie)
|
setCookies(status.cookie)
|
||||||
state.uiStates.showLoginPanel = false
|
uiStates.showLoginPanel = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,37 @@
|
||||||
import { css, cx } from '@emotion/css'
|
import { css, cx } from '@emotion/css'
|
||||||
import Router from './Router'
|
import Router from './Router'
|
||||||
|
import useIntersectionObserver from '@/web/hooks/useIntersectionObserver'
|
||||||
|
import uiStates from '@/web/states/uiStates'
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
const Main = () => {
|
const Main = () => {
|
||||||
|
// Show/hide topbar background
|
||||||
|
const observePoint = useRef<HTMLDivElement | null>(null)
|
||||||
|
const { onScreen } = useIntersectionObserver(observePoint)
|
||||||
|
useEffect(() => {
|
||||||
|
uiStates.hideTopbarBackground = onScreen
|
||||||
|
return () => {
|
||||||
|
uiStates.hideTopbarBackground = false
|
||||||
|
}
|
||||||
|
}, [onScreen])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
className={cx(
|
className={cx(
|
||||||
'no-scrollbar overflow-y-auto pb-16 pr-6 pl-10',
|
'no-scrollbar overflow-y-auto pb-16 pr-6 pl-10',
|
||||||
css`
|
css`
|
||||||
padding-top: 132px;
|
|
||||||
grid-area: main;
|
grid-area: main;
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<div ref={observePoint}></div>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
margin-top: 132px;
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
<Router />
|
<Router />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,11 +144,6 @@ const MenuBar = ({ className }: { className?: string }) => {
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{window.env?.isMac && (
|
|
||||||
<div className='fixed top-6 left-6 translate-y-0.5'>
|
|
||||||
<TrafficLight />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Tabs />
|
<Tabs />
|
||||||
{!isMobile && <TabName />}
|
{!isMobile && <TabName />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { css, cx } from '@emotion/css'
|
import { css, cx } from '@emotion/css'
|
||||||
import Icon from '../Icon'
|
import Icon from '../Icon'
|
||||||
import { formatDuration, resizeImage } from '@/web/utils/common'
|
import { formatDuration, resizeImage } from '@/web/utils/common'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { State as PlayerState, Mode as PlayerMode } from '@/web/utils/player'
|
import { State as PlayerState, Mode as PlayerMode } from '@/web/utils/player'
|
||||||
import Slider from './Slider'
|
import Slider from './Slider'
|
||||||
|
|
@ -17,7 +17,7 @@ const Progress = () => {
|
||||||
const { track, progress } = useSnapshot(player)
|
const { track, progress } = useSnapshot(player)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col w-full mt-10'>
|
<div className='mt-10 flex w-full flex-col'>
|
||||||
<Slider
|
<Slider
|
||||||
min={0}
|
min={0}
|
||||||
max={(track?.dt ?? 100000) / 1000}
|
max={(track?.dt ?? 100000) / 1000}
|
||||||
|
|
@ -28,7 +28,7 @@ const Progress = () => {
|
||||||
onlyCallOnChangeAfterDragEnded={true}
|
onlyCallOnChangeAfterDragEnded={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex justify-between mt-1 font-bold text-14 text-black/20 dark:text-white/20'>
|
<div className='mt-1 flex justify-between text-14 font-bold text-black/20 dark:text-white/20'>
|
||||||
<span>{formatDuration(progress * 1000, 'en', 'hh:mm:ss')}</span>
|
<span>{formatDuration(progress * 1000, 'en', 'hh:mm:ss')}</span>
|
||||||
<span>{formatDuration(track?.dt || 0, 'en', 'hh:mm:ss')}</span>
|
<span>{formatDuration(track?.dt || 0, 'en', 'hh:mm:ss')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -117,9 +117,9 @@ const NowPlaying = () => {
|
||||||
<Cover />
|
<Cover />
|
||||||
|
|
||||||
{/* Info & Controls */}
|
{/* Info & Controls */}
|
||||||
<div className='flex flex-col items-center p-5 m-3 font-medium rounded-20 bg-white/60 backdrop-blur-3xl dark:bg-black/70'>
|
<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 */}
|
{/* Track Info */}
|
||||||
<div className='text-lg text-black line-clamp-1 dark:text-white'>
|
<div className='line-clamp-1 text-lg text-black dark:text-white'>
|
||||||
{track?.name}
|
{track?.name}
|
||||||
</div>
|
</div>
|
||||||
<ArtistInline
|
<ArtistInline
|
||||||
|
|
@ -129,13 +129,13 @@ const NowPlaying = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Dividing line */}
|
{/* Dividing line */}
|
||||||
<div className='w-2/3 h-px mt-2 bg-black/10 dark:bg-white/10'></div>
|
<div className='mt-2 h-px w-2/3 bg-black/10 dark:bg-white/10'></div>
|
||||||
|
|
||||||
{/* Progress */}
|
{/* Progress */}
|
||||||
<Progress />
|
<Progress />
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<div className='flex items-center justify-between w-full mt-4'>
|
<div className='mt-4 flex w-full items-center justify-between'>
|
||||||
<button>
|
<button>
|
||||||
<Icon
|
<Icon
|
||||||
name='hide-list'
|
name='hide-list'
|
||||||
|
|
@ -149,7 +149,7 @@ const NowPlaying = () => {
|
||||||
disabled={!track}
|
disabled={!track}
|
||||||
className='rounded-full bg-black/10 p-2.5 dark:bg-white/10'
|
className='rounded-full bg-black/10 p-2.5 dark:bg-white/10'
|
||||||
>
|
>
|
||||||
<Icon name='previous' className='w-6 h-6 ' />
|
<Icon name='previous' className='h-6 w-6 ' />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => track && player.playOrPause()}
|
onClick={() => track && player.playOrPause()}
|
||||||
|
|
@ -161,7 +161,7 @@ const NowPlaying = () => {
|
||||||
? 'pause'
|
? 'pause'
|
||||||
: 'play'
|
: 'play'
|
||||||
}
|
}
|
||||||
className='w-6 h-6 '
|
className='h-6 w-6 '
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|
@ -169,7 +169,7 @@ const NowPlaying = () => {
|
||||||
disabled={!track}
|
disabled={!track}
|
||||||
className='rounded-full bg-black/10 p-2.5 dark:bg-white/10'
|
className='rounded-full bg-black/10 p-2.5 dark:bg-white/10'
|
||||||
>
|
>
|
||||||
<Icon name='next' className='w-6 h-6 ' />
|
<Icon name='next' className='h-6 w-6 ' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { ease } from '@/web/utils/const'
|
import { ease } from '@/web/utils/const'
|
||||||
import useIsMobile from '@/web/hooks/useIsMobile'
|
import useIsMobile from '@/web/hooks/useIsMobile'
|
||||||
import scrollPositions from '@/web/store/scrollPositions'
|
import scrollPositions from '@/web/states/scrollPositions'
|
||||||
import { useLayoutEffect } from 'react'
|
import { useLayoutEffect } from 'react'
|
||||||
|
|
||||||
const PageTransition = ({
|
const PageTransition = ({
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const Player = () => {
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<PlayingNext className='mb-3 h-full' />
|
<PlayingNext className='h-full' />
|
||||||
<div className='pb-6'>
|
<div className='pb-6'>
|
||||||
<NowPlaying />
|
<NowPlaying />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { css, cx } from '@emotion/css'
|
import { css, cx } from '@emotion/css'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import Image from '@/web/components/New/Image'
|
import Image from '@/web/components/New/Image'
|
||||||
import Icon from '@/web/components/Icon'
|
import Icon from '@/web/components/Icon'
|
||||||
import useCoverColor from '@/web/hooks/useCoverColor'
|
import useCoverColor from '@/web/hooks/useCoverColor'
|
||||||
import { resizeImage } from '@/web/utils/common'
|
import { resizeImage } from '@/web/utils/common'
|
||||||
import { motion, PanInfo, useMotionValue } from 'framer-motion'
|
import { motion, PanInfo } from 'framer-motion'
|
||||||
import { useLockBodyScroll } from 'react-use'
|
import { useLockBodyScroll } from 'react-use'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import useUserLikedTracksIDs, {
|
import useUserLikedTracksIDs, {
|
||||||
useMutationLikeATrack,
|
useMutationLikeATrack,
|
||||||
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
||||||
import PlayingNextMobile from './PlayingNextMobile'
|
import uiStates from '@/web/states/uiStates'
|
||||||
|
import { ease } from '@/web/utils/const'
|
||||||
|
|
||||||
const LikeButton = () => {
|
const LikeButton = () => {
|
||||||
const { track } = useSnapshot(player)
|
const { track } = useSnapshot(player)
|
||||||
|
|
@ -23,7 +24,7 @@ const LikeButton = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className='flex items-center h-full'
|
className='flex h-full items-center'
|
||||||
onClick={() => track?.id && likeATrack.mutateAsync(track.id)}
|
onClick={() => track?.id && likeATrack.mutateAsync(track.id)}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|
@ -39,6 +40,7 @@ const PlayerMobile = () => {
|
||||||
const bgColor = useCoverColor(track?.al?.picUrl ?? '')
|
const bgColor = useCoverColor(track?.al?.picUrl ?? '')
|
||||||
const [locked, setLocked] = useState(false)
|
const [locked, setLocked] = useState(false)
|
||||||
useLockBodyScroll(locked)
|
useLockBodyScroll(locked)
|
||||||
|
const { mobileShowPlayingNext } = useSnapshot(uiStates)
|
||||||
|
|
||||||
const onDragEnd = (
|
const onDragEnd = (
|
||||||
event: MouseEvent | TouchEvent | PointerEvent,
|
event: MouseEvent | TouchEvent | PointerEvent,
|
||||||
|
|
@ -63,33 +65,56 @@ const PlayerMobile = () => {
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Cover */}
|
{/* Handler */}
|
||||||
|
{!mobileShowPlayingNext && (
|
||||||
|
<motion.div
|
||||||
|
onClick={() => {
|
||||||
|
uiStates.mobileShowPlayingNext = true
|
||||||
|
}}
|
||||||
|
className={cx(
|
||||||
|
'absolute right-0 left-0 flex justify-center',
|
||||||
|
css`
|
||||||
|
--height: 20px;
|
||||||
|
height: var(--height);
|
||||||
|
top: calc(var(--height) * -1);
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ ease, duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<Icon name='player-handler' className='h-2.5 text-brand-700' />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cover */}
|
||||||
<div className='h-full py-2.5'>
|
<div className='h-full py-2.5'>
|
||||||
<Image
|
<Image
|
||||||
src={resizeImage(track?.al.picUrl || '', 'sm')}
|
src={resizeImage(track?.al.picUrl || '', 'sm')}
|
||||||
className='z-10 h-full rounded-lg aspect-square'
|
className='z-10 aspect-square h-full rounded-lg'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Track info */}
|
{/* Track info */}
|
||||||
<div className='relative flex items-center flex-grow h-full px-3 overflow-hidden'>
|
<div className='relative flex h-full flex-grow items-center overflow-hidden px-3'>
|
||||||
<motion.div
|
<motion.div
|
||||||
drag='x'
|
drag='x'
|
||||||
dragConstraints={{ left: 0, right: 0 }}
|
dragConstraints={{ left: 0, right: 0 }}
|
||||||
onDragStart={() => setLocked(true)}
|
onDragStart={() => setLocked(true)}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
className='flex items-center flex-grow h-full '
|
dragDirectionLock={true}
|
||||||
|
className='flex h-full flex-grow items-center '
|
||||||
>
|
>
|
||||||
<div className='flex-shrink-0'>
|
<div className='flex-shrink-0'>
|
||||||
<div className='font-bold text-white line-clamp-1 text-14'>
|
<div className='line-clamp-1 text-14 font-bold text-white'>
|
||||||
{track?.name}
|
{track?.name}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-1 font-bold line-clamp-1 text-12 text-white/60'>
|
<div className='line-clamp-1 mt-1 text-12 font-bold text-white/60'>
|
||||||
{track?.ar?.map(a => a.name).join(', ')}
|
{track?.ar?.map(a => a.name).join(', ')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-grow h-full'></div>
|
<div className='h-full flex-grow'></div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -120,7 +145,7 @@ const PlayerMobile = () => {
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name={state === 'playing' ? 'pause' : 'play'}
|
name={state === 'playing' ? 'pause' : 'play'}
|
||||||
className='w-6 h-6 text-white/80'
|
className='h-6 w-6 text-white/80'
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,25 @@
|
||||||
import { resizeImage } from '@/web/utils/common'
|
import { isIosPwa, resizeImage } from '@/web/utils/common'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { State as PlayerState } from '@/web/utils/player'
|
import { State as PlayerState } from '@/web/utils/player'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import useTracks from '@/web/api/hooks/useTracks'
|
import useTracks from '@/web/api/hooks/useTracks'
|
||||||
import { css, cx } from '@emotion/css'
|
import { css, cx } from '@emotion/css'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
|
||||||
import Image from './Image'
|
|
||||||
import Wave from './Wave'
|
import Wave from './Wave'
|
||||||
import Icon from '@/web/components/Icon'
|
import Icon from '@/web/components/Icon'
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
|
||||||
import { useRef } from 'react'
|
|
||||||
import { useWindowSize } from 'react-use'
|
import { useWindowSize } from 'react-use'
|
||||||
import { playerWidth, topbarHeight } from '@/web/utils/const'
|
import { playerWidth, topbarHeight } from '@/web/utils/const'
|
||||||
|
import useIsMobile from '@/web/hooks/useIsMobile'
|
||||||
|
import { Virtuoso } from 'react-virtuoso'
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
'absolute top-0 left-0 z-10 flex w-full items-center justify-between px-7 pb-6 text-14 font-bold text-neutral-700 dark:text-neutral-300 lg:px-4'
|
'absolute top-0 left-0 z-20 flex w-full items-center justify-between bg-contain bg-repeat-x px-7 pb-6 text-14 font-bold text-neutral-700 dark:text-neutral-300 lg:px-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<div className='w-1 h-4 mr-2 bg-brand-700'></div>
|
<div className='mr-2 h-4 w-1 bg-brand-700'></div>
|
||||||
PLAYING NEXT
|
PLAYING NEXT
|
||||||
</div>
|
</div>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
|
|
@ -48,30 +46,21 @@ const Track = ({
|
||||||
state: PlayerState
|
state: PlayerState
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<div
|
||||||
className='flex items-center justify-between'
|
className='mb-5 flex items-center justify-between'
|
||||||
// initial={{ opacity: 0 }}
|
|
||||||
// animate={{ opacity: 1 }}
|
|
||||||
// exit={{ x: '100%', opacity: 0 }}
|
|
||||||
// transition={{
|
|
||||||
// duration: 0.24,
|
|
||||||
// }}
|
|
||||||
// layout
|
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
if (e.detail === 2 && track?.id) player.playTrack(track.id)
|
if (e.detail === 2 && track?.id) player.playTrack(track.id)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Cover */}
|
{/* Cover */}
|
||||||
<Image
|
<img
|
||||||
alt='Cover'
|
alt='Cover'
|
||||||
className='flex-shrink-0 mr-4 aspect-square h-14 w-14 rounded-12'
|
className='mr-4 aspect-square h-14 w-14 flex-shrink-0 rounded-12'
|
||||||
src={resizeImage(track?.al?.picUrl || '', 'sm')}
|
src={resizeImage(track?.al?.picUrl || '', 'sm')}
|
||||||
animation={false}
|
|
||||||
placeholder={false}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Track info */}
|
{/* Track info */}
|
||||||
<div className='flex-grow mr-3'>
|
<div className='mr-3 flex-grow'>
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
'line-clamp-1 text-16 font-medium ',
|
'line-clamp-1 text-16 font-medium ',
|
||||||
|
|
@ -82,7 +71,7 @@ const Track = ({
|
||||||
>
|
>
|
||||||
{track?.name}
|
{track?.name}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-1 font-bold line-clamp-1 text-14 text-neutral-200 dark:text-neutral-700'>
|
<div className='line-clamp-1 mt-1 text-14 font-bold text-neutral-200 dark:text-neutral-700'>
|
||||||
{track?.ar.map(a => a.name).join(', ')}
|
{track?.ar.map(a => a.name).join(', ')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -91,11 +80,11 @@ const Track = ({
|
||||||
{playingTrackIndex === index ? (
|
{playingTrackIndex === index ? (
|
||||||
<Wave playing={state === 'playing'} />
|
<Wave playing={state === 'playing'} />
|
||||||
) : (
|
) : (
|
||||||
<div className='font-medium text-16 text-neutral-700 dark:text-neutral-200'>
|
<div className='text-16 font-medium text-neutral-700 dark:text-neutral-200'>
|
||||||
{String(index + 1).padStart(2, '0')}
|
{String(index + 1).padStart(2, '0')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,69 +92,57 @@ const TrackList = ({ className }: { className?: string }) => {
|
||||||
const { trackList, trackIndex, state } = useSnapshot(player)
|
const { trackList, trackIndex, state } = useSnapshot(player)
|
||||||
const { data: tracksRaw } = useTracks({ ids: trackList })
|
const { data: tracksRaw } = useTracks({ ids: trackList })
|
||||||
const tracks = tracksRaw?.songs || []
|
const tracks = tracksRaw?.songs || []
|
||||||
const parentRef = useRef<HTMLDivElement>(null)
|
|
||||||
const { height } = useWindowSize()
|
const { height } = useWindowSize()
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
const listHeight = height - topbarHeight - playerWidth - 24 - 20 // 24是封面与底部间距,20是list与封面间距
|
const listHeight = height - topbarHeight - playerWidth - 24 // 24是封面与底部间距
|
||||||
|
const listHeightMobile = height - 154 - 110 - (isIosPwa ? 34 : 0) // 154是列表距离底部的距离,110是顶部的距离
|
||||||
const rowVirtualizer = useVirtualizer({
|
|
||||||
count: tracks.length,
|
|
||||||
getScrollElement: () => parentRef.current,
|
|
||||||
estimateSize: () => 76,
|
|
||||||
overscan: 10,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
ref={parentRef}
|
className={css`
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 22px,
|
||||||
|
black 42px
|
||||||
|
); // 顶部渐变遮罩
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Virtuoso
|
||||||
style={{
|
style={{
|
||||||
height: `${listHeight}px`,
|
height: `${isMobile ? listHeightMobile : listHeight}px`,
|
||||||
}}
|
}}
|
||||||
|
totalCount={tracks.length}
|
||||||
className={cx(
|
className={cx(
|
||||||
'no-scrollbar relative z-10 w-full overflow-auto',
|
'no-scrollbar relative z-10 w-full overflow-auto',
|
||||||
className,
|
className,
|
||||||
css`
|
css`
|
||||||
padding-top: 42px;
|
|
||||||
mask-image: linear-gradient(
|
mask-image: linear-gradient(
|
||||||
to bottom,
|
to top,
|
||||||
transparent 0,
|
transparent 8px,
|
||||||
black 42px
|
black 42px
|
||||||
); // 顶部渐变遮罩
|
); // 底部渐变遮罩
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
>
|
fixedItemHeight={76}
|
||||||
<div
|
data={tracks}
|
||||||
className='relative w-full'
|
overscan={10}
|
||||||
style={{
|
components={{
|
||||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
Header: () => <div className='h-8'></div>,
|
||||||
|
Footer: () => <div className='h-1'></div>,
|
||||||
}}
|
}}
|
||||||
>
|
itemContent={(index, track) => (
|
||||||
{rowVirtualizer.getVirtualItems().map((row: any) => (
|
|
||||||
<div
|
|
||||||
key={row.index}
|
|
||||||
className='absolute top-0 left-0 w-full'
|
|
||||||
style={{
|
|
||||||
height: `${row.size}px`,
|
|
||||||
transform: `translateY(${row.start}px)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Track
|
<Track
|
||||||
track={tracks?.[row.index]}
|
key={index}
|
||||||
index={row.index}
|
track={track}
|
||||||
|
index={index}
|
||||||
playingTrackIndex={trackIndex}
|
playingTrackIndex={trackIndex}
|
||||||
state={state}
|
state={state}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
></Virtuoso>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 底部渐变遮罩 */}
|
|
||||||
<div
|
|
||||||
className='absolute left-0 right-0 z-20 hidden pointer-events-none h-14 bg-gradient-to-t from-black to-transparent lg:block'
|
|
||||||
style={{ top: `${listHeight - 56}px` }}
|
|
||||||
></div>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,38 +10,52 @@ import { useLockBodyScroll } from 'react-use'
|
||||||
import { isIosPwa } from '@/web/utils/common'
|
import { isIosPwa } from '@/web/utils/common'
|
||||||
import PlayingNext from './PlayingNext'
|
import PlayingNext from './PlayingNext'
|
||||||
import { ease } from '@/web/utils/const'
|
import { ease } from '@/web/utils/const'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
import uiStates from '@/web/states/uiStates'
|
||||||
|
import Icon from '@/web/components/Icon'
|
||||||
|
|
||||||
const PlayingNextMobile = () => {
|
const PlayingNextMobile = () => {
|
||||||
const [display, setDisplay] = useState(false)
|
const { mobileShowPlayingNext: display } = useSnapshot(uiStates)
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
useLockBodyScroll(isDragging || display)
|
useLockBodyScroll(isDragging)
|
||||||
|
|
||||||
const dragControls = useDragControls()
|
const dragControls = useDragControls()
|
||||||
const y = useMotionValue('82%')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
|
{display && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className='fixed inset-0 px-3 bg-black/80 backdrop-blur-3xl'
|
className='fixed inset-0 bg-black/80 backdrop-blur-3xl'
|
||||||
exit={{
|
exit={{
|
||||||
y: '100%',
|
y: '100%',
|
||||||
|
borderRadius: '24px',
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.6,
|
|
||||||
ease: 'easeOut',
|
ease: 'easeOut',
|
||||||
|
duration: 0.4,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ y: 0, borderRadius: 0 }}
|
||||||
initial={{ opacity: 0 }}
|
initial={{ y: '100%', borderRadius: '24px' }}
|
||||||
style={{
|
transition={{ duration: 0.6, ease }}
|
||||||
borderRadius: isDragging ? '24px' : '0px',
|
|
||||||
y,
|
|
||||||
}}
|
|
||||||
drag='y'
|
|
||||||
dragControls={dragControls}
|
dragControls={dragControls}
|
||||||
dragListener={false}
|
dragListener={false}
|
||||||
|
whileDrag={{
|
||||||
|
borderRadius: '24px',
|
||||||
|
transition: {
|
||||||
|
duration: 0.2,
|
||||||
|
ease: 'linear',
|
||||||
|
},
|
||||||
|
}}
|
||||||
dragConstraints={{ top: 0, bottom: 0 }}
|
dragConstraints={{ top: 0, bottom: 0 }}
|
||||||
dragDirectionLock={true}
|
dragDirectionLock={true}
|
||||||
onDrag={(event, info) => console.log(info.point.y)}
|
onDragEnd={(event, info) => {
|
||||||
|
setIsDragging(false)
|
||||||
|
const offset = info.offset.y
|
||||||
|
if (offset > 150) {
|
||||||
|
uiStates.mobileShowPlayingNext = false
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
drag='y'
|
||||||
>
|
>
|
||||||
{/* Indictor */}
|
{/* Indictor */}
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
@ -49,33 +63,28 @@ const PlayingNextMobile = () => {
|
||||||
setIsDragging(true)
|
setIsDragging(true)
|
||||||
dragControls.start(e)
|
dragControls.start(e)
|
||||||
}}
|
}}
|
||||||
onDragEnd={() => setIsDragging(false)}
|
onClick={() => {
|
||||||
dragConstraints={{ top: 0, bottom: 0 }}
|
uiStates.mobileShowPlayingNext = false
|
||||||
|
}}
|
||||||
className={cx(
|
className={cx(
|
||||||
'mx-7 flex justify-center',
|
'flex flex-col justify-end',
|
||||||
css`
|
css`
|
||||||
--height: 30px;
|
height: 108px;
|
||||||
bottom: calc(
|
|
||||||
70px + 64px +
|
|
||||||
${isIosPwa ? '24px' : 'env(safe-area-inset-bottom)'}
|
|
||||||
); // 拖动条到导航栏的距离 + 导航栏高度 + safe-area-inset-bottom
|
|
||||||
height: var(--height);
|
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
layout
|
|
||||||
>
|
>
|
||||||
<motion.div
|
<Icon
|
||||||
className='mt-3.5 h-1.5 w-10 rounded-full bg-brand-700'
|
name='player-handler'
|
||||||
layout
|
className='mb-5 h-2.5 rotate-180 text-brand-700'
|
||||||
style={{ width: isDragging || display ? '80px' : '40px' }}
|
/>
|
||||||
></motion.div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* List */}
|
{/* List */}
|
||||||
<div className='relative'>
|
<div className='relative h-full px-7'>
|
||||||
<PlayingNext />
|
<PlayingNext />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useLayoutEffect } from 'react'
|
import { useLayoutEffect } from 'react'
|
||||||
import scrollPositions from '@/web/store/scrollPositions'
|
import scrollPositions from '@/web/states/scrollPositions'
|
||||||
import { throttle } from 'lodash-es'
|
import { throttle } from 'lodash-es'
|
||||||
|
|
||||||
const ScrollRestoration = () => {
|
const ScrollRestoration = () => {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css'
|
||||||
import Icon from '../../Icon'
|
import Icon from '../../Icon'
|
||||||
import { resizeImage } from '@/web/utils/common'
|
import { resizeImage } from '@/web/utils/common'
|
||||||
import useUser from '@/web/api/hooks/useUser'
|
import useUser from '@/web/api/hooks/useUser'
|
||||||
import { state } from '@/web/store'
|
import uiStates from '@/web/states/uiStates'
|
||||||
|
|
||||||
const Avatar = ({ className }: { className?: string }) => {
|
const Avatar = ({ className }: { className?: string }) => {
|
||||||
const { data: user } = useUser()
|
const { data: user } = useUser()
|
||||||
|
|
@ -16,7 +16,7 @@ const Avatar = ({ className }: { className?: string }) => {
|
||||||
{avatarUrl ? (
|
{avatarUrl ? (
|
||||||
<img
|
<img
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
onClick={() => (state.uiStates.showLoginPanel = true)}
|
onClick={() => (uiStates.showLoginPanel = true)}
|
||||||
className={cx(
|
className={cx(
|
||||||
'app-region-no-drag rounded-full',
|
'app-region-no-drag rounded-full',
|
||||||
className || 'h-12 w-12'
|
className || 'h-12 w-12'
|
||||||
|
|
@ -24,7 +24,7 @@ const Avatar = ({ className }: { className?: string }) => {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
onClick={() => (state.uiStates.showLoginPanel = true)}
|
onClick={() => (uiStates.showLoginPanel = true)}
|
||||||
className={cx(
|
className={cx(
|
||||||
'rounded-full bg-day-600 p-2.5 dark:bg-night-600',
|
'rounded-full bg-day-600 p-2.5 dark:bg-night-600',
|
||||||
className || 'h-12 w-12'
|
className || 'h-12 w-12'
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,55 @@
|
||||||
import { css, cx } from '@emotion/css'
|
import { css, cx } from '@emotion/css'
|
||||||
import { useLocation } from 'react-router-dom'
|
|
||||||
import Avatar from './Avatar'
|
import Avatar from './Avatar'
|
||||||
import SearchBox from './SearchBox'
|
import SearchBox from './SearchBox'
|
||||||
import SettingsButton from './SettingsButton'
|
import SettingsButton from './SettingsButton'
|
||||||
import NavigationButtons from './NavigationButtons'
|
import NavigationButtons from './NavigationButtons'
|
||||||
import topbarBackground from '@/web/assets/images/topbar-background.png'
|
import topbarBackground from '@/web/assets/images/topbar-background.png'
|
||||||
|
import uiStates from '@/web/states/uiStates'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import { ease } from '@/web/utils/const'
|
||||||
|
|
||||||
const TopbarDesktop = () => {
|
const Background = () => {
|
||||||
const location = useLocation()
|
const { hideTopbarBackground } = useSnapshot(uiStates)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
|
<AnimatePresence>
|
||||||
|
{!hideTopbarBackground && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ ease }}
|
||||||
className={cx(
|
className={cx(
|
||||||
'app-region-drag fixed top-0 right-0 z-20 flex items-center justify-between overflow-hidden rounded-tr-24 bg-contain pt-11 pb-10 pr-6 pl-10',
|
'absolute inset-0 z-0 bg-contain bg-repeat-x',
|
||||||
css`
|
|
||||||
left: 104px;
|
|
||||||
`,
|
|
||||||
!location.pathname.startsWith('/album/') &&
|
|
||||||
!location.pathname.startsWith('/playlist/') &&
|
|
||||||
!location.pathname.startsWith('/browse') &&
|
|
||||||
!location.pathname.startsWith('/artist') &&
|
|
||||||
css`
|
css`
|
||||||
background-image: url(${topbarBackground});
|
background-image: url(${topbarBackground});
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
></motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopbarDesktop = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
'app-region-drag fixed top-0 left-0 right-0 z-20 flex items-center justify-between overflow-hidden bg-contain pt-11 pb-10 pr-6',
|
||||||
|
css`
|
||||||
|
padding-left: 144px;
|
||||||
|
`,
|
||||||
|
window.env?.isElectron && 'rounded-t-24'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
|
{/* Background */}
|
||||||
|
<Background />
|
||||||
|
|
||||||
{/* Left Part */}
|
{/* Left Part */}
|
||||||
<div className='flex items-center'>
|
<div className='z-10 flex items-center'>
|
||||||
<NavigationButtons />
|
<NavigationButtons />
|
||||||
|
|
||||||
{/* Dividing line */}
|
{/* Dividing line */}
|
||||||
|
|
@ -36,7 +59,7 @@ const TopbarDesktop = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Part */}
|
{/* Right Part */}
|
||||||
<div className='flex'>
|
<div className='z-10 flex'>
|
||||||
<SettingsButton />
|
<SettingsButton />
|
||||||
<Avatar className='ml-3 h-12 w-12' />
|
<Avatar className='ml-3 h-12 w-12' />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { formatDuration } from '@/web/utils/common'
|
import { formatDuration } from '@/web/utils/common'
|
||||||
import { css, cx } from '@emotion/css'
|
import { css, cx } from '@emotion/css'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import Wave from './Wave'
|
import Wave from './Wave'
|
||||||
import Icon from '@/web/components/Icon'
|
import Icon from '@/web/components/Icon'
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { motion } from 'framer-motion'
|
||||||
import { ease } from '@/web/utils/const'
|
import { ease } from '@/web/utils/const'
|
||||||
import { injectGlobal } from '@emotion/css'
|
import { injectGlobal } from '@emotion/css'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import BlurBackground from '@/web/components/New/BlurBackground'
|
||||||
|
|
||||||
injectGlobal`
|
injectGlobal`
|
||||||
.plyr__video-wrapper,
|
.plyr__video-wrapper,
|
||||||
|
|
@ -86,21 +87,7 @@ const Cover = memo(
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Blur bg */}
|
{/* Blur bg */}
|
||||||
{!isMobile && (
|
<BlurBackground cover={cover} />
|
||||||
<img
|
|
||||||
className={cx(
|
|
||||||
'absolute z-0 object-cover opacity-70',
|
|
||||||
css`
|
|
||||||
top: -400px;
|
|
||||||
left: -370px;
|
|
||||||
width: 1572px;
|
|
||||||
height: 528px;
|
|
||||||
filter: blur(256px) saturate(1.2);
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
src={resizeImage(cover, 'sm')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -111,10 +98,12 @@ const TrackListHeader = ({
|
||||||
album,
|
album,
|
||||||
playlist,
|
playlist,
|
||||||
onPlay,
|
onPlay,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
album?: Album
|
album?: Album
|
||||||
playlist?: Playlist
|
playlist?: Playlist
|
||||||
onPlay: () => void
|
onPlay: () => void
|
||||||
|
className?: string
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
|
@ -126,6 +115,7 @@ const TrackListHeader = ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
|
className,
|
||||||
'z-10 mx-2.5 rounded-48 p-8 dark:bg-white/10',
|
'z-10 mx-2.5 rounded-48 p-8 dark:bg-white/10',
|
||||||
'lg:mx-0 lg:grid lg:grid-rows-1 lg:gap-10 lg:rounded-none lg:p-0 lg:dark:bg-transparent',
|
'lg:mx-0 lg:grid lg:grid-rows-1 lg:gap-10 lg:rounded-none lg:p-0 lg:dark:bg-transparent',
|
||||||
!isMobile &&
|
!isMobile &&
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import Icon from './Icon'
|
||||||
import useUserLikedTracksIDs, {
|
import useUserLikedTracksIDs, {
|
||||||
useMutationLikeATrack,
|
useMutationLikeATrack,
|
||||||
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
||||||
import { player, state } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { resizeImage } from '@/web/utils/common'
|
import { resizeImage } from '@/web/utils/common'
|
||||||
import { State as PlayerState, Mode as PlayerMode } from '@/web/utils/player'
|
import { State as PlayerState, Mode as PlayerMode } from '@/web/utils/player'
|
||||||
import { RepeatMode as PlayerRepeatMode } from '@/shared/playerDataTypes'
|
import { RepeatMode as PlayerRepeatMode } from '@/shared/playerDataTypes'
|
||||||
|
|
@ -180,7 +180,11 @@ const Others = () => {
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
{/* Lyric */}
|
{/* Lyric */}
|
||||||
<IconButton onClick={() => (state.uiStates.showLyricPanel = true)}>
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
//
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Icon className='h-6 w-6' name='lyrics' />
|
<Icon className='h-6 w-6' name='lyrics' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import Icon from './Icon'
|
||||||
import useUserPlaylists from '@/web/api/hooks/useUserPlaylists'
|
import useUserPlaylists from '@/web/api/hooks/useUserPlaylists'
|
||||||
import { scrollToTop } from '@/web/utils/common'
|
import { scrollToTop } from '@/web/utils/common'
|
||||||
import { prefetchPlaylist } from '@/web/api/hooks/usePlaylist'
|
import { prefetchPlaylist } from '@/web/api/hooks/usePlaylist'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { Mode, TrackListSourceType } from '@/web/utils/player'
|
import { Mode, TrackListSourceType } from '@/web/utils/player'
|
||||||
import { cx } from '@emotion/css'
|
import { cx } from '@emotion/css'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import Icon from './Icon'
|
import Icon from './Icon'
|
||||||
import { IpcChannels } from '@/shared/IpcChannels'
|
import { IpcChannels } from '@/shared/IpcChannels'
|
||||||
import useIpcRenderer from '@/web/hooks/useIpcRenderer'
|
import useIpcRenderer from '@/web/hooks/useIpcRenderer'
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import Icon from '@/web/components/Icon'
|
||||||
import useUserLikedTracksIDs, {
|
import useUserLikedTracksIDs, {
|
||||||
useMutationLikeATrack,
|
useMutationLikeATrack,
|
||||||
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { formatDuration } from '@/web/utils/common'
|
import { formatDuration } from '@/web/utils/common'
|
||||||
import { State as PlayerState } from '@/web/utils/player'
|
import { State as PlayerState } from '@/web/utils/player'
|
||||||
import { cx } from '@emotion/css'
|
import { cx } from '@emotion/css'
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import ArtistInline from '@/web/components/ArtistsInline'
|
import ArtistInline from '@/web/components/ArtistsInline'
|
||||||
import Skeleton from '@/web/components/Skeleton'
|
import Skeleton from '@/web/components/Skeleton'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { resizeImage } from '@/web/utils/common'
|
import { resizeImage } from '@/web/utils/common'
|
||||||
import Icon from './Icon'
|
import Icon from './Icon'
|
||||||
import { cx } from '@emotion/css'
|
import { cx } from '@emotion/css'
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import useUserLikedTracksIDs, {
|
||||||
useMutationLikeATrack,
|
useMutationLikeATrack,
|
||||||
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
} from '@/web/api/hooks/useUserLikedTracksIDs'
|
||||||
import { formatDuration, resizeImage } from '@/web/utils/common'
|
import { formatDuration, resizeImage } from '@/web/utils/common'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { cx } from '@emotion/css'
|
import { cx } from '@emotion/css'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
|
|
||||||
|
|
|
||||||
25
packages/web/hooks/useIntersectionObserver.ts
Normal file
25
packages/web/hooks/useIntersectionObserver.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { useState, useEffect, RefObject } from 'react'
|
||||||
|
|
||||||
|
const useIntersectionObserver = (
|
||||||
|
element: RefObject<Element>
|
||||||
|
): { onScreen: boolean } => {
|
||||||
|
const [onScreen, setOnScreen] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (element.current) {
|
||||||
|
const observer = new IntersectionObserver(([entry]) =>
|
||||||
|
setOnScreen(entry.isIntersecting)
|
||||||
|
)
|
||||||
|
observer.observe(element.current)
|
||||||
|
return () => {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [element, setOnScreen])
|
||||||
|
|
||||||
|
return {
|
||||||
|
onScreen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useIntersectionObserver
|
||||||
|
|
@ -13,23 +13,21 @@ export default function useVideoCover(props: {
|
||||||
async () => {
|
async () => {
|
||||||
if (!id || !name || !artist) return
|
if (!id || !name || !artist) return
|
||||||
|
|
||||||
const fromCache = window.ipcRenderer?.sendSync(
|
const fromMainProcess = await window.ipcRenderer?.invoke(
|
||||||
IpcChannels.GetVideoCover,
|
IpcChannels.GetVideoCover,
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
|
name,
|
||||||
|
artist,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (fromCache) {
|
if (fromMainProcess) {
|
||||||
return fromCache === 'no' ? undefined : fromCache
|
return fromMainProcess
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromRemote = await axios.get('/yesplaymusic/video-cover', {
|
const fromRemote = await axios.get('/yesplaymusic/video-cover', {
|
||||||
params: props,
|
params: props,
|
||||||
})
|
})
|
||||||
window.ipcRenderer?.send(IpcChannels.SetVideoCover, {
|
|
||||||
id,
|
|
||||||
url: fromRemote.data.url || '',
|
|
||||||
})
|
|
||||||
if (fromRemote?.data?.url) {
|
if (fromRemote?.data?.url) {
|
||||||
return fromRemote.data.url
|
return fromRemote.data.url
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import {
|
import {
|
||||||
IpcChannels,
|
IpcChannels,
|
||||||
IpcChannelsReturns,
|
IpcChannelsReturns,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import ReactGA from 'react-ga4'
|
||||||
import { ipcRenderer } from './ipcRenderer'
|
import { ipcRenderer } from './ipcRenderer'
|
||||||
import { QueryClientProvider } from 'react-query'
|
import { QueryClientProvider } from 'react-query'
|
||||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
|
|
||||||
ReactGA.initialize('G-KMJJCFZDKF')
|
ReactGA.initialize('G-KMJJCFZDKF')
|
||||||
|
|
||||||
|
|
@ -35,23 +34,12 @@ ipcRenderer()
|
||||||
const container = document.getElementById('root') as HTMLElement
|
const container = document.getElementById('root') as HTMLElement
|
||||||
const root = ReactDOMClient.createRoot(container)
|
const root = ReactDOMClient.createRoot(container)
|
||||||
|
|
||||||
// root.render(
|
root.render(
|
||||||
// <StrictMode>
|
|
||||||
// <BrowserRouter>
|
|
||||||
// <QueryClientProvider client={reactQueryClient}>
|
|
||||||
// <App />
|
|
||||||
// </QueryClientProvider>
|
|
||||||
// </BrowserRouter>
|
|
||||||
// </StrictMode>
|
|
||||||
// )
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<QueryClientProvider client={reactQueryClient}>
|
<QueryClientProvider client={reactQueryClient}>
|
||||||
<App />
|
<App />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</StrictMode>,
|
</StrictMode>
|
||||||
document.getElementById('root')
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,15 @@
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"test:types": "tsc --noEmit --project src/renderer/tsconfig.json",
|
"test:types": "tsc --noEmit --project ./tsconfig.json",
|
||||||
"lint": "eslint --ext .ts,.js,.tsx,.jsx ./",
|
"lint": "eslint --ext .ts,.js,.tsx,.jsx ./",
|
||||||
"analyze:css": "npx windicss-analysis",
|
"analyze:css": "npx windicss-analysis",
|
||||||
"analyze:js": "npm run build && open-cli bundle-stats-renderer.html",
|
"analyze:js": "npm run build && open-cli bundle-stats-renderer.html",
|
||||||
"storybook": "start-storybook -p 6006",
|
"storybook": "start-storybook -p 6006",
|
||||||
"storybook:build": "build-storybook",
|
"storybook:build": "build-storybook",
|
||||||
"generate:accent-color-css": "node ./scripts/generate.accent.color.css.js",
|
"generate:accent-color-css": "node ./scripts/generate.accent.color.css.js",
|
||||||
"api:netease": "npx NeteaseCloudMusicApi@latest"
|
"api:netease": "npx NeteaseCloudMusicApi@latest",
|
||||||
|
"format": "prettier --config ../../prettier.config.js --write './**/*.{ts,tsx,js,jsx,css}' --ignore-path ../../.prettierignore"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.13.1 || >=16.0.0"
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
|
@ -25,7 +26,6 @@
|
||||||
"@emotion/css": "^11.9.0",
|
"@emotion/css": "^11.9.0",
|
||||||
"@sentry/react": "^6.19.7",
|
"@sentry/react": "^6.19.7",
|
||||||
"@sentry/tracing": "^6.19.7",
|
"@sentry/tracing": "^6.19.7",
|
||||||
"@tanstack/react-virtual": "3.0.0-beta.2",
|
|
||||||
"ahooks": "^3.4.1",
|
"ahooks": "^3.4.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"color.js": "^1.2.0",
|
"color.js": "^1.2.0",
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
"react-query": "^3.38.0",
|
"react-query": "^3.38.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
|
"react-virtuoso": "^2.16.1",
|
||||||
"valtio": "^1.6.1"
|
"valtio": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import Icon from '@/web/components/Icon'
|
||||||
import TracksAlbum from '@/web/components/TracksAlbum'
|
import TracksAlbum from '@/web/components/TracksAlbum'
|
||||||
import useAlbum from '@/web/api/hooks/useAlbum'
|
import useAlbum from '@/web/api/hooks/useAlbum'
|
||||||
import useArtistAlbums from '@/web/api/hooks/useArtistAlbums'
|
import useArtistAlbums from '@/web/api/hooks/useArtistAlbums'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import {
|
import {
|
||||||
Mode as PlayerMode,
|
Mode as PlayerMode,
|
||||||
State as PlayerState,
|
State as PlayerState,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import TracksGrid from '@/web/components/TracksGrid'
|
||||||
import CoverRow, { Subtitle } from '@/web/components/CoverRow'
|
import CoverRow, { Subtitle } from '@/web/components/CoverRow'
|
||||||
import Skeleton from '@/web/components/Skeleton'
|
import Skeleton from '@/web/components/Skeleton'
|
||||||
import useTracks from '@/web/api/hooks/useTracks'
|
import useTracks from '@/web/api/hooks/useTracks'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { cx } from '@emotion/css'
|
import { cx } from '@emotion/css'
|
||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import useLyric from '@/web/api/hooks/useLyric'
|
||||||
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
||||||
import useUser from '@/web/api/hooks/useUser'
|
import useUser from '@/web/api/hooks/useUser'
|
||||||
import useUserPlaylists from '@/web/api/hooks/useUserPlaylists'
|
import useUserPlaylists from '@/web/api/hooks/useUserPlaylists'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { resizeImage } from '@/web/utils/common'
|
import { resizeImage } from '@/web/utils/common'
|
||||||
import { sample, chunk } from 'lodash-es'
|
import { sample, chunk } from 'lodash-es'
|
||||||
import useUserArtists from '@/web/api/hooks/useUserArtists'
|
import useUserArtists from '@/web/api/hooks/useUserArtists'
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import useTracks from '@/web/api/hooks/useTracks'
|
||||||
import { NavLink, useParams } from 'react-router-dom'
|
import { NavLink, useParams } from 'react-router-dom'
|
||||||
import PageTransition from '@/web/components/New/PageTransition'
|
import PageTransition from '@/web/components/New/PageTransition'
|
||||||
import TrackList from '@/web/components/New/TrackList'
|
import TrackList from '@/web/components/New/TrackList'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import useArtistAlbums from '@/web/api/hooks/useArtistAlbums'
|
import useArtistAlbums from '@/web/api/hooks/useArtistAlbums'
|
||||||
|
|
@ -113,9 +113,13 @@ const Album = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageTransition>
|
<PageTransition>
|
||||||
<TrackListHeader album={album?.album} onPlay={onPlay} />
|
<TrackListHeader
|
||||||
|
album={album?.album}
|
||||||
|
onPlay={onPlay}
|
||||||
|
className='mt-2.5 lg:mt-0'
|
||||||
|
/>
|
||||||
<TrackList
|
<TrackList
|
||||||
tracks={tracks?.songs || album?.songs || album?.album.songs}
|
tracks={tracks?.songs || album?.album.songs || album?.songs}
|
||||||
className='z-10 mx-2.5 mt-3 lg:mx-0 lg:mt-10'
|
className='z-10 mx-2.5 mt-3 lg:mx-0 lg:mt-10'
|
||||||
onPlay={onPlay}
|
onPlay={onPlay}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const ArtistInfo = ({ artist }: { artist?: Artist }) => {
|
||||||
<div className='text-28 font-semibold text-night-50 lg:text-32'>
|
<div className='text-28 font-semibold text-night-50 lg:text-32'>
|
||||||
{artist?.name}
|
{artist?.name}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-2.5 text-24 font-medium text-night-400 lg:mt-6'>
|
<div className='text-white-400 mt-2.5 text-24 font-medium lg:mt-6'>
|
||||||
Artist
|
Artist
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-1 text-12 font-medium text-night-400'>
|
<div className='mt-1 text-12 font-medium text-night-400'>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { resizeImage } from '@/web/utils/common'
|
|
||||||
import { cx, css } from '@emotion/css'
|
|
||||||
import useIsMobile from '@/web/hooks/useIsMobile'
|
|
||||||
|
|
||||||
const BlurBackground = ({ cover }: { cover?: string }) => {
|
|
||||||
const isMobile = useIsMobile()
|
|
||||||
return isMobile || !cover ? (
|
|
||||||
<></>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
className={cx(
|
|
||||||
'absolute z-0 object-cover opacity-70',
|
|
||||||
css`
|
|
||||||
top: -400px;
|
|
||||||
left: -370px;
|
|
||||||
width: 1572px;
|
|
||||||
height: 528px;
|
|
||||||
filter: blur(256px) saturate(1.2);
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
src={resizeImage(cover, 'sm')}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BlurBackground
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { resizeImage } from '@/web/utils/common'
|
import { resizeImage } from '@/web/utils/common'
|
||||||
import { cx, css } from '@emotion/css'
|
import { cx, css } from '@emotion/css'
|
||||||
import Image from '@/web/components/New/Image'
|
import Image from '@/web/components/New/Image'
|
||||||
import { useMemo } from 'react'
|
|
||||||
import { breakpoint as bp } from '@/web/utils/const'
|
import { breakpoint as bp } from '@/web/utils/const'
|
||||||
import BlurBackground from './BlurBackground'
|
import BlurBackground from '@/web/components/New/BlurBackground'
|
||||||
import ArtistInfo from './ArtistInfo'
|
import ArtistInfo from './ArtistInfo'
|
||||||
import Actions from './Actions'
|
import Actions from './Actions'
|
||||||
import LatestRelease from './LatestRelease'
|
import LatestRelease from './LatestRelease'
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,11 @@ const Album = () => {
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className='ml-2 flex-shrink-1'>
|
<div className='flex-shrink-1 ml-2'>
|
||||||
<div className='font-medium line-clamp-1 text-16 text-night-100'>
|
<div className='line-clamp-1 text-16 font-medium text-night-100'>
|
||||||
{album.name}
|
{album.name}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-1 font-bold text-14 text-night-500'>
|
<div className='mt-1 text-14 font-bold text-night-500'>
|
||||||
{album.type}
|
{album.type}
|
||||||
{album.size > 1 ? `· ${album.size} Tracks` : ''}
|
{album.size > 1 ? `· ${album.size} Tracks` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -69,8 +69,8 @@ const Video = () => {
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className='ml-2 flex-shrink-1'>
|
<div className='flex-shrink-1 ml-2'>
|
||||||
<div className='font-medium line-clamp-2 text-16 text-night-100'>
|
<div className='line-clamp-2 text-16 font-medium text-night-100'>
|
||||||
Swedish House Mafia & The Weeknd Live at C...
|
Swedish House Mafia & The Weeknd Live at C...
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-1.5 text-12 font-medium text-night-500'>
|
<div className='mt-1.5 text-12 font-medium text-night-500'>
|
||||||
|
|
@ -84,7 +84,7 @@ const Video = () => {
|
||||||
const LatestRelease = () => {
|
const LatestRelease = () => {
|
||||||
return (
|
return (
|
||||||
<div className='mx-2.5 lg:mx-0'>
|
<div className='mx-2.5 lg:mx-0'>
|
||||||
<div className='mb-3 font-bold mt-7 text-14 text-neutral-300'>
|
<div className='mb-3 mt-7 text-14 font-bold text-neutral-300'>
|
||||||
Latest Releases
|
Latest Releases
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { resizeImage } from '@/web/utils/common'
|
import { resizeImage } from '@/web/utils/common'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { State as PlayerState } from '@/web/utils/player'
|
import { State as PlayerState } from '@/web/utils/player'
|
||||||
import useTracks from '@/web/api/hooks/useTracks'
|
import useTracks from '@/web/api/hooks/useTracks'
|
||||||
import { css, cx } from '@emotion/css'
|
import { css, cx } from '@emotion/css'
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ const Browse = () => {
|
||||||
tabs={categories}
|
tabs={categories}
|
||||||
value={active}
|
value={active}
|
||||||
onChange={category => setActive(category)}
|
onChange={category => setActive(category)}
|
||||||
className='sticky top-0 z-10 px-2.5 lg:px-0'
|
className='sticky top-0 z-10 mt-2.5 px-2.5 lg:mt-0 lg:px-0'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='relative mx-2.5 mt-5 lg:mx-0'>
|
<div className='relative mx-2.5 mt-5 lg:mx-0'>
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ const Discover = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageTransition disableEnterAnimation={true}>
|
<PageTransition disableEnterAnimation={true}>
|
||||||
<div className='mx-2.5 pb-10 lg:mx-0 lg:pb-16'>
|
<div className='mx-2.5 mt-2.5 pb-10 lg:mx-0 lg:mt-0 lg:pb-16'>
|
||||||
<CoverWall albums={albums || []} />
|
<CoverWall albums={albums || []} />
|
||||||
</div>
|
</div>
|
||||||
</PageTransition>
|
</PageTransition>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { useMemo, useState } from 'react'
|
||||||
import CoverRow from '@/web/components/New/CoverRow'
|
import CoverRow from '@/web/components/New/CoverRow'
|
||||||
import useUserPlaylists from '@/web/api/hooks/useUserPlaylists'
|
import useUserPlaylists from '@/web/api/hooks/useUserPlaylists'
|
||||||
import useUserAlbums from '@/web/api/hooks/useUserAlbums'
|
import useUserAlbums from '@/web/api/hooks/useUserAlbums'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
import uiStates from '@/web/states/uiStates'
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
|
|
@ -39,7 +41,12 @@ const Playlists = () => {
|
||||||
|
|
||||||
const Collections = () => {
|
const Collections = () => {
|
||||||
// const { data: artists } = useUserArtists()
|
// const { data: artists } = useUserArtists()
|
||||||
const [selectedTab, setSelectedTab] = useState(tabs[0].id)
|
const { librarySelectedTab: selectedTab } = useSnapshot(uiStates)
|
||||||
|
const setSelectedTab = (
|
||||||
|
id: 'playlists' | 'albums' | 'artists' | 'videos'
|
||||||
|
) => {
|
||||||
|
uiStates.librarySelectedTab = id
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import useLyric from '@/web/api/hooks/useLyric'
|
import useLyric from '@/web/api/hooks/useLyric'
|
||||||
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
||||||
import useUserPlaylists from '@/web/api/hooks/useUserPlaylists'
|
import useUserPlaylists from '@/web/api/hooks/useUserPlaylists'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { sample, chunk, sampleSize } from 'lodash-es'
|
import { sample, chunk, sampleSize } from 'lodash-es'
|
||||||
import { css, cx } from '@emotion/css'
|
import { css, cx } from '@emotion/css'
|
||||||
import { useState, useEffect, useMemo, useCallback, memo } from 'react'
|
import { useState, useEffect, useMemo, useCallback, memo } from 'react'
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import TrackListHeader from '@/web/components/New/TrackListHeader'
|
||||||
import { NavLink, useParams } from 'react-router-dom'
|
import { NavLink, useParams } from 'react-router-dom'
|
||||||
import PageTransition from '@/web/components/New/PageTransition'
|
import PageTransition from '@/web/components/New/PageTransition'
|
||||||
import TrackList from '@/web/components/New/TrackList'
|
import TrackList from '@/web/components/New/TrackList'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { memo, useEffect, useMemo } from 'react'
|
import { memo, useEffect, useMemo } from 'react'
|
||||||
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
||||||
import useTracksInfinite from '@/web/api/hooks/useTracksInfinite'
|
import useTracksInfinite from '@/web/api/hooks/useTracksInfinite'
|
||||||
import useScroll from '@/web/hooks/useScroll'
|
|
||||||
const Playlist = () => {
|
const Playlist = () => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const { data: playlist, isLoading } = usePlaylist({
|
const { data: playlist, isLoading } = usePlaylist({
|
||||||
|
|
@ -35,7 +35,11 @@ const Playlist = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageTransition>
|
<PageTransition>
|
||||||
<TrackListHeader playlist={playlist?.playlist} onPlay={onPlay} />
|
<TrackListHeader
|
||||||
|
playlist={playlist?.playlist}
|
||||||
|
onPlay={onPlay}
|
||||||
|
className='mt-2.5 lg:mt-0'
|
||||||
|
/>
|
||||||
<TrackList
|
<TrackList
|
||||||
tracks={playlist?.playlist?.tracks ?? []}
|
tracks={playlist?.playlist?.tracks ?? []}
|
||||||
onPlay={onPlay}
|
onPlay={onPlay}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import TracksList from '@/web/components/TracksList'
|
||||||
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
import usePlaylist from '@/web/api/hooks/usePlaylist'
|
||||||
import useScroll from '@/web/hooks/useScroll'
|
import useScroll from '@/web/hooks/useScroll'
|
||||||
import useTracksInfinite from '@/web/api/hooks/useTracksInfinite'
|
import useTracksInfinite from '@/web/api/hooks/useTracksInfinite'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { formatDate, resizeImage } from '@/web/utils/common'
|
import { formatDate, resizeImage } from '@/web/utils/common'
|
||||||
import useUserPlaylists, {
|
import useUserPlaylists, {
|
||||||
useMutationLikeAPlaylist,
|
useMutationLikeAPlaylist,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { multiMatchSearch, search } from '@/web/api/search'
|
import { multiMatchSearch, search } from '@/web/api/search'
|
||||||
import Cover from '@/web/components/Cover'
|
import Cover from '@/web/components/Cover'
|
||||||
import TrackGrid from '@/web/components/TracksGrid'
|
import TrackGrid from '@/web/components/TracksGrid'
|
||||||
import { player } from '@/web/store'
|
import player from '@/web/states/player'
|
||||||
import { resizeImage } from '@/web/utils/common'
|
import { resizeImage } from '@/web/utils/common'
|
||||||
import { SearchTypes, SearchApiNames } from '@/shared/api/Search'
|
import { SearchTypes, SearchApiNames } from '@/shared/api/Search'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { state } from '@/web/store'
|
import settings from '@/web/states/settings'
|
||||||
import { changeAccentColor } from '@/web/utils/theme'
|
import { changeAccentColor } from '@/web/utils/theme'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { cx } from '@emotion/css'
|
import { cx } from '@emotion/css'
|
||||||
|
|
@ -25,11 +25,11 @@ const AccentColor = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeColor = (color: string) => {
|
const changeColor = (color: string) => {
|
||||||
state.settings.accentColor = color
|
settings.accentColor = color
|
||||||
changeAccentColor(color)
|
changeAccentColor(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
const accentColor = useSnapshot(state).settings.accentColor
|
const accentColor = useSnapshot(settings).accentColor
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
<div className='mb-2 dark:text-white'>强调色</div>
|
<div className='mb-2 dark:text-white'>强调色</div>
|
||||||
|
|
|
||||||
19
packages/web/states/persistedUiStates.ts
Normal file
19
packages/web/states/persistedUiStates.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { proxy, subscribe } from 'valtio'
|
||||||
|
|
||||||
|
interface PersistedUiStates {
|
||||||
|
loginPhoneCountryCode: string
|
||||||
|
loginType: 'phone' | 'email' | 'qrCode'
|
||||||
|
}
|
||||||
|
|
||||||
|
const initPersistedUiStates: PersistedUiStates = {
|
||||||
|
loginPhoneCountryCode: '+86',
|
||||||
|
loginType: 'qrCode',
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistedUiStates = proxy<PersistedUiStates>(initPersistedUiStates)
|
||||||
|
|
||||||
|
subscribe(persistedUiStates, () => {
|
||||||
|
localStorage.setItem('persistedUiStates', JSON.stringify(persistedUiStates))
|
||||||
|
})
|
||||||
|
|
||||||
|
export default persistedUiStates
|
||||||
18
packages/web/states/player.ts
Normal file
18
packages/web/states/player.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Player } from '@/web/utils/player'
|
||||||
|
import { proxy, subscribe } from 'valtio'
|
||||||
|
|
||||||
|
const playerInLocalStorage = localStorage.getItem('player')
|
||||||
|
const player = proxy(new Player())
|
||||||
|
|
||||||
|
player.init((playerInLocalStorage && JSON.parse(playerInLocalStorage)) || {})
|
||||||
|
|
||||||
|
subscribe(player, () => {
|
||||||
|
localStorage.setItem('player', JSON.stringify(player))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-extra-semi
|
||||||
|
;(window as any).player = player
|
||||||
|
}
|
||||||
|
|
||||||
|
export default player
|
||||||
53
packages/web/states/settings.ts
Normal file
53
packages/web/states/settings.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { IpcChannels } from '@/shared/IpcChannels'
|
||||||
|
import { merge } from 'lodash-es'
|
||||||
|
import { proxy, subscribe } from 'valtio'
|
||||||
|
|
||||||
|
interface Settings {
|
||||||
|
accentColor: string
|
||||||
|
unm: {
|
||||||
|
enabled: boolean
|
||||||
|
sources: Array<
|
||||||
|
'migu' | 'kuwo' | 'kugou' | 'ytdl' | 'qq' | 'bilibili' | 'joox'
|
||||||
|
>
|
||||||
|
searchMode: 'order-first' | 'fast-first'
|
||||||
|
proxy: null | {
|
||||||
|
protocol: 'http' | 'https' | 'socks5'
|
||||||
|
host: string
|
||||||
|
port: number
|
||||||
|
username?: string
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
|
cookies: {
|
||||||
|
qq?: string
|
||||||
|
joox?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initSettings: Settings = {
|
||||||
|
accentColor: 'blue',
|
||||||
|
unm: {
|
||||||
|
enabled: true,
|
||||||
|
sources: ['migu'],
|
||||||
|
searchMode: 'order-first',
|
||||||
|
proxy: null,
|
||||||
|
cookies: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsInLocalStorage = localStorage.getItem('settings')
|
||||||
|
const settings = proxy<Settings>(
|
||||||
|
merge(
|
||||||
|
initSettings,
|
||||||
|
settingsInLocalStorage ? JSON.parse(settingsInLocalStorage) : {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
subscribe(settings, () => {
|
||||||
|
localStorage.setItem('settings', JSON.stringify(settings))
|
||||||
|
})
|
||||||
|
subscribe(settings, () => {
|
||||||
|
window.ipcRenderer?.send(IpcChannels.SyncSettings, settings)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default settings
|
||||||
19
packages/web/states/uiStates.ts
Normal file
19
packages/web/states/uiStates.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { proxy } from 'valtio'
|
||||||
|
|
||||||
|
interface UIStates {
|
||||||
|
showLyricPanel: boolean
|
||||||
|
showLoginPanel: boolean
|
||||||
|
hideTopbarBackground: boolean
|
||||||
|
librarySelectedTab: 'playlists' | 'albums' | 'artists' | 'videos'
|
||||||
|
mobileShowPlayingNext: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const initUIStates: UIStates = {
|
||||||
|
showLyricPanel: false,
|
||||||
|
showLoginPanel: false,
|
||||||
|
hideTopbarBackground: false,
|
||||||
|
librarySelectedTab: 'playlists',
|
||||||
|
mobileShowPlayingNext: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default proxy<UIStates>(initUIStates)
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { proxy, subscribe } from 'valtio'
|
|
||||||
import { Player } from '@/web/utils/player'
|
|
||||||
import { merge } from 'lodash-es'
|
|
||||||
import { IpcChannels } from '@/shared/IpcChannels'
|
|
||||||
import { Store, initialState } from '@/shared/store'
|
|
||||||
|
|
||||||
const stateInLocalStorage = localStorage.getItem('state')
|
|
||||||
export const state = proxy<Store>(
|
|
||||||
merge(
|
|
||||||
initialState,
|
|
||||||
stateInLocalStorage ? JSON.parse(stateInLocalStorage) : {},
|
|
||||||
{
|
|
||||||
uiStates: initialState.uiStates,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
subscribe(state, () => {
|
|
||||||
localStorage.setItem('state', JSON.stringify(state))
|
|
||||||
})
|
|
||||||
subscribe(state.settings, () => {
|
|
||||||
window.ipcRenderer?.send(IpcChannels.SyncSettings, { ...state.settings })
|
|
||||||
})
|
|
||||||
|
|
||||||
// player
|
|
||||||
const playerInLocalStorage = localStorage.getItem('player')
|
|
||||||
export const player = proxy(new Player())
|
|
||||||
player.init((playerInLocalStorage && JSON.parse(playerInLocalStorage)) || {})
|
|
||||||
subscribe(player, () => {
|
|
||||||
localStorage.setItem('player', JSON.stringify(player))
|
|
||||||
})
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-extra-semi
|
|
||||||
;(window as any).player = player
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,17 @@
|
||||||
|
export const changeTheme = (theme: 'light' | 'dark') => {
|
||||||
|
document.body.setAttribute('class', theme)
|
||||||
|
if (!window.env?.isElectron) {
|
||||||
|
document.documentElement.style.background =
|
||||||
|
theme === 'dark' ? '#000' : '#fff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const changeAccentColor = (color: string) => {
|
export const changeAccentColor = (color: string) => {
|
||||||
document.body.setAttribute('data-accent-color', color)
|
document.body.setAttribute('data-accent-color', color)
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateString = localStorage.getItem('state')
|
const settingsInStorage = localStorage.getItem('settings')
|
||||||
const stateInLocalStorage = stateString ? JSON.parse(stateString) : {}
|
const settings = settingsInStorage ? JSON.parse(settingsInStorage) : {}
|
||||||
|
|
||||||
changeAccentColor(stateInLocalStorage?.settings?.accentColor || 'blue')
|
changeTheme(settings.theme || 'dark')
|
||||||
|
changeAccentColor(settings?.accentColor || 'green')
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,8 @@ export default defineConfig({
|
||||||
strictPort: IS_ELECTRON ? true : false,
|
strictPort: IS_ELECTRON ? true : false,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/netease/': {
|
'/netease/': {
|
||||||
target: `http://192.168.2.111:${
|
target: `http://192.168.50.111:${
|
||||||
|
// target: `http://127.0.0.1:${
|
||||||
process.env.ELECTRON_DEV_NETEASE_API_PORT || 3000
|
process.env.ELECTRON_DEV_NETEASE_API_PORT || 3000
|
||||||
}`,
|
}`,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
|
@ -93,7 +94,6 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
'/yesplaymusic/video-cover': {
|
'/yesplaymusic/video-cover': {
|
||||||
target: `http://168.138.40.199:51324`,
|
target: `http://168.138.40.199:51324`,
|
||||||
// target: `http://127.0.0.1:51324`,
|
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'/yesplaymusic/': {
|
'/yesplaymusic/': {
|
||||||
|
|
|
||||||
42
pnpm-lock.yaml
generated
42
pnpm-lock.yaml
generated
|
|
@ -117,7 +117,6 @@ importers:
|
||||||
'@storybook/builder-vite': ^0.1.35
|
'@storybook/builder-vite': ^0.1.35
|
||||||
'@storybook/react': ^6.5.5
|
'@storybook/react': ^6.5.5
|
||||||
'@storybook/testing-library': ^0.0.11
|
'@storybook/testing-library': ^0.0.11
|
||||||
'@tanstack/react-virtual': 3.0.0-beta.2
|
|
||||||
'@testing-library/react': ^13.3.0
|
'@testing-library/react': ^13.3.0
|
||||||
'@types/howler': ^2.2.7
|
'@types/howler': ^2.2.7
|
||||||
'@types/js-cookie': ^3.0.2
|
'@types/js-cookie': ^3.0.2
|
||||||
|
|
@ -163,6 +162,7 @@ importers:
|
||||||
react-query: ^3.38.0
|
react-query: ^3.38.0
|
||||||
react-router-dom: ^6.3.0
|
react-router-dom: ^6.3.0
|
||||||
react-use: ^17.4.0
|
react-use: ^17.4.0
|
||||||
|
react-virtuoso: ^2.16.1
|
||||||
rollup-plugin-visualizer: ^5.6.0
|
rollup-plugin-visualizer: ^5.6.0
|
||||||
storybook-tailwind-dark-mode: ^1.0.12
|
storybook-tailwind-dark-mode: ^1.0.12
|
||||||
tailwindcss: ^3.0.24
|
tailwindcss: ^3.0.24
|
||||||
|
|
@ -176,7 +176,6 @@ importers:
|
||||||
'@emotion/css': 11.9.0
|
'@emotion/css': 11.9.0
|
||||||
'@sentry/react': 6.19.7_react@18.1.0
|
'@sentry/react': 6.19.7_react@18.1.0
|
||||||
'@sentry/tracing': 6.19.7
|
'@sentry/tracing': 6.19.7
|
||||||
'@tanstack/react-virtual': 3.0.0-beta.2
|
|
||||||
ahooks: 3.4.1_react@18.1.0
|
ahooks: 3.4.1_react@18.1.0
|
||||||
axios: 0.27.2
|
axios: 0.27.2
|
||||||
color.js: 1.2.0
|
color.js: 1.2.0
|
||||||
|
|
@ -199,6 +198,7 @@ importers:
|
||||||
react-query: 3.39.1_ef5jwxihqo6n7gxfmzogljlgcm
|
react-query: 3.39.1_ef5jwxihqo6n7gxfmzogljlgcm
|
||||||
react-router-dom: 6.3.0_ef5jwxihqo6n7gxfmzogljlgcm
|
react-router-dom: 6.3.0_ef5jwxihqo6n7gxfmzogljlgcm
|
||||||
react-use: 17.4.0_ef5jwxihqo6n7gxfmzogljlgcm
|
react-use: 17.4.0_ef5jwxihqo6n7gxfmzogljlgcm
|
||||||
|
react-virtuoso: 2.16.1_ef5jwxihqo6n7gxfmzogljlgcm
|
||||||
valtio: 1.6.1_react@18.1.0+vite@2.9.9
|
valtio: 1.6.1_react@18.1.0+vite@2.9.9
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@storybook/addon-actions': 6.5.5_ef5jwxihqo6n7gxfmzogljlgcm
|
'@storybook/addon-actions': 6.5.5_ef5jwxihqo6n7gxfmzogljlgcm
|
||||||
|
|
@ -4539,10 +4539,6 @@ packages:
|
||||||
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
|
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@reach/observe-rect/1.2.0:
|
|
||||||
resolution: {integrity: sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@rollup/plugin-babel/5.3.1_4kojsos35jimftt7mhjohcqk6y:
|
/@rollup/plugin-babel/5.3.1_4kojsos35jimftt7mhjohcqk6y:
|
||||||
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
|
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
@ -6402,13 +6398,6 @@ packages:
|
||||||
defer-to-connect: 2.0.1
|
defer-to-connect: 2.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@tanstack/react-virtual/3.0.0-beta.2:
|
|
||||||
resolution: {integrity: sha512-pwA9URTHYXX/2PgIISoMcf1P77hxf5oI3L/IDQ19Q1xuAc76o2R2CwHv6vvl5fDhwVj5klOfBxJvuT61Lhy9/w==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
dependencies:
|
|
||||||
'@reach/observe-rect': 1.2.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@testing-library/dom/8.13.0:
|
/@testing-library/dom/8.13.0:
|
||||||
resolution: {integrity: sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==}
|
resolution: {integrity: sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
@ -7151,6 +7140,20 @@ packages:
|
||||||
url-toolkit: 2.2.5
|
url-toolkit: 2.2.5
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@virtuoso.dev/react-urx/0.2.13_react@18.1.0:
|
||||||
|
resolution: {integrity: sha512-MY0ugBDjFb5Xt8v2HY7MKcRGqw/3gTpMlLXId2EwQvYJoC8sP7nnXjAxcBtTB50KTZhO0SbzsFimaZ7pSdApwA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16'
|
||||||
|
dependencies:
|
||||||
|
'@virtuoso.dev/urx': 0.2.13
|
||||||
|
react: 18.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@virtuoso.dev/urx/0.2.13:
|
||||||
|
resolution: {integrity: sha512-iirJNv92A1ZWxoOHHDYW/1KPoi83939o83iUBQHIim0i3tMeSKEh+bxhJdTHQ86Mr4uXx9xGUTq69cp52ZP8Xw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@vitejs/plugin-react/1.3.2:
|
/@vitejs/plugin-react/1.3.2:
|
||||||
resolution: {integrity: sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==}
|
resolution: {integrity: sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
@ -16373,6 +16376,19 @@ packages:
|
||||||
tslib: 2.4.0
|
tslib: 2.4.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-virtuoso/2.16.1_ef5jwxihqo6n7gxfmzogljlgcm:
|
||||||
|
resolution: {integrity: sha512-WpcHZedUe00XYSQ56KcdYmWy/oaiPPuweTYemC9gl8CbjchLTKqLPCJa51Yv32U9oj1XPAEMfxuaXM7NTtjOiw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16 || >=17 || >= 18'
|
||||||
|
react-dom: '>=16 || >=17 || >= 18'
|
||||||
|
dependencies:
|
||||||
|
'@virtuoso.dev/react-urx': 0.2.13_react@18.1.0
|
||||||
|
'@virtuoso.dev/urx': 0.2.13
|
||||||
|
react: 18.1.0
|
||||||
|
react-dom: 18.1.0_react@18.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react/18.1.0:
|
/react/18.1.0:
|
||||||
resolution: {integrity: sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==}
|
resolution: {integrity: sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue