mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-16 13:17:46 +00:00
feat: 实现私人FM (#1453)
* feat: 实现私人FM * 根据建议修改 * fix: APP启动时FMCard无法加载数据 * fix: coverUrl使用useMemo * fix: 在私人FM模式下禁用单曲循环 * fix: 私人FM模式下禁用部分按钮 * fix: 限制FMCard的歌手长度 * fix: 移除ArtistsInline的clamp参数,并将其作为隐式Fallback
This commit is contained in:
parent
e99c4833f7
commit
719a3a60d4
6 changed files with 351 additions and 40 deletions
116
src/renderer/api/personalFM.ts
Normal file
116
src/renderer/api/personalFM.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export enum PersonalFMApiNames {
|
||||||
|
FETCH_PERSONAL_FM = 'fetchPersonalFM',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PersonalMusic {
|
||||||
|
name: null | string
|
||||||
|
id: number
|
||||||
|
size: number
|
||||||
|
extension: 'mp3' | 'flac' | null
|
||||||
|
sr: number
|
||||||
|
dfsId: number
|
||||||
|
bitrate: number
|
||||||
|
playTime: number
|
||||||
|
volumeDelta: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetchPersonalFMResponse {
|
||||||
|
code: number
|
||||||
|
popAdjust: boolean
|
||||||
|
data: {
|
||||||
|
name: string
|
||||||
|
id: number
|
||||||
|
position: number
|
||||||
|
alias: string[]
|
||||||
|
status: number
|
||||||
|
fee: number
|
||||||
|
copyrightId: number
|
||||||
|
disc?: string
|
||||||
|
no: number
|
||||||
|
artists: Artist[]
|
||||||
|
album: Album
|
||||||
|
starred: boolean
|
||||||
|
popularity: number
|
||||||
|
score: number
|
||||||
|
starredNum: number
|
||||||
|
duration: number
|
||||||
|
playedNum: number
|
||||||
|
dayPlays: number
|
||||||
|
hearTime: number
|
||||||
|
ringtone: null
|
||||||
|
crbt: null
|
||||||
|
audition: null
|
||||||
|
copyFrom: string
|
||||||
|
commentThreadId: string
|
||||||
|
rtUrl: string | null
|
||||||
|
ftype: number
|
||||||
|
rtUrls: (string | null)[]
|
||||||
|
copyright: number
|
||||||
|
transName: null | string
|
||||||
|
sign: null
|
||||||
|
mark: number
|
||||||
|
originCoverType: number
|
||||||
|
originSongSimpleData: null
|
||||||
|
single: number
|
||||||
|
noCopyrightRcmd: null
|
||||||
|
mvid: number
|
||||||
|
bMusic?: PersonalMusic
|
||||||
|
lMusic?: PersonalMusic
|
||||||
|
mMusic?: PersonalMusic
|
||||||
|
hMusic?: PersonalMusic
|
||||||
|
reason: string
|
||||||
|
privilege: {
|
||||||
|
id: number
|
||||||
|
fee: number
|
||||||
|
payed: number
|
||||||
|
st: number
|
||||||
|
pl: number
|
||||||
|
dl: number
|
||||||
|
sp: number
|
||||||
|
cp: number
|
||||||
|
subp: number
|
||||||
|
cs: boolean
|
||||||
|
maxbr: number
|
||||||
|
fl: number
|
||||||
|
toast: boolean
|
||||||
|
flag: number
|
||||||
|
preShell: boolean
|
||||||
|
playMaxbr: number
|
||||||
|
downloadMaxbr: number
|
||||||
|
rscl: null
|
||||||
|
freeTrialPrivilege: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
chargeInfoList: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
alg: string
|
||||||
|
s_ctrp: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchPersonalFM(): Promise<FetchPersonalFMResponse> {
|
||||||
|
return request({
|
||||||
|
url: '/personal/fm',
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FMTrashResponse {
|
||||||
|
songs: null[]
|
||||||
|
code: number
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fmTrash(id: number): Promise<FMTrashResponse> {
|
||||||
|
return request({
|
||||||
|
url: '/fm/trash',
|
||||||
|
method: 'post',
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,12 @@ const ArtistInline = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('line-clamp-1', className)}>
|
<div
|
||||||
|
className={classNames(
|
||||||
|
!className?.includes('line-clamp') && 'line-clamp-1',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
{artists.map((artist, index) => (
|
{artists.map((artist, index) => (
|
||||||
<span key={`${artist.id}-${artist.name}`}>
|
<span key={`${artist.id}-${artist.name}`}>
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,81 @@
|
||||||
import { average } from 'color.js'
|
import { average } from 'color.js'
|
||||||
import { colord } from 'colord'
|
import { colord } from 'colord'
|
||||||
|
import { player } from '@/store'
|
||||||
|
import { resizeImage } from '@/utils/common'
|
||||||
import SvgIcon from '@/components/SvgIcon'
|
import SvgIcon from '@/components/SvgIcon'
|
||||||
|
import ArtistInline from '@/components/ArtistsInline'
|
||||||
|
import { State as PlayerState, Mode as PlayerMode } from '@/utils/player'
|
||||||
|
|
||||||
enum ACTION {
|
const MediaControls = () => {
|
||||||
DISLIKE = 'dislike',
|
const classes =
|
||||||
PLAY = 'play',
|
'btn-pressed-animation btn-hover-animation mr-1 cursor-default rounded-lg p-1.5 transition duration-200 after:bg-white/10'
|
||||||
NEXT = 'next',
|
|
||||||
|
const playerSnapshot = useSnapshot(player)
|
||||||
|
const state = useMemo(() => playerSnapshot.state, [playerSnapshot.state])
|
||||||
|
|
||||||
|
const playOrPause = () => {
|
||||||
|
if (playerSnapshot.mode === PlayerMode.FM) {
|
||||||
|
player.playOrPause()
|
||||||
|
} else {
|
||||||
|
player.playFM()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
key='dislike'
|
||||||
|
className={classes}
|
||||||
|
onClick={() => player.fmTrash()}
|
||||||
|
>
|
||||||
|
<SvgIcon name='dislike' className='h-6 w-6' />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button key='play' className={classes} onClick={playOrPause}>
|
||||||
|
<SvgIcon
|
||||||
|
className='h-6 w-6'
|
||||||
|
name={
|
||||||
|
playerSnapshot.mode === PlayerMode.FM &&
|
||||||
|
[PlayerState.PLAYING, PlayerState.LOADING].includes(state)
|
||||||
|
? 'pause'
|
||||||
|
: 'play'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
key='next'
|
||||||
|
className={classes}
|
||||||
|
onClick={() => player.nextTrack(true)}
|
||||||
|
>
|
||||||
|
<SvgIcon name='next' className='h-6 w-6' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FMCard = () => {
|
const FMCard = () => {
|
||||||
const coverUrl =
|
|
||||||
'https://p1.music.126.net/lEzPSOjusKaRXKXT3987lQ==/109951166035876388.jpg?param=512y512'
|
|
||||||
const [background, setBackground] = useState('')
|
const [background, setBackground] = useState('')
|
||||||
|
|
||||||
|
const playerSnapshot = useSnapshot(player)
|
||||||
|
const track = useMemo(() => playerSnapshot.fmTrack, [playerSnapshot.fmTrack])
|
||||||
|
const coverUrl = useMemo(
|
||||||
|
() => resizeImage(playerSnapshot.fmTrack?.al?.picUrl ?? '', 'md'),
|
||||||
|
[playerSnapshot.fmTrack]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
average(coverUrl, { amount: 1, format: 'hex', sample: 1 }).then(color => {
|
if (coverUrl) {
|
||||||
const to = colord(color as string)
|
average(coverUrl, { amount: 1, format: 'hex', sample: 1 }).then(color => {
|
||||||
.darken(0.15)
|
const to = colord(color as string)
|
||||||
.rotate(-5)
|
.darken(0.15)
|
||||||
.toHex()
|
.rotate(-5)
|
||||||
setBackground(`linear-gradient(to bottom right, ${color}, ${to})`)
|
.toHex()
|
||||||
})
|
setBackground(`linear-gradient(to bottom right, ${color}, ${to})`)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setBackground(`linear-gradient(to bottom right, #66ccff, #ee0000)`)
|
||||||
|
}
|
||||||
}, [coverUrl])
|
}, [coverUrl])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -28,28 +83,20 @@ const FMCard = () => {
|
||||||
className='relative flex h-[198px] overflow-hidden rounded-2xl p-4'
|
className='relative flex h-[198px] overflow-hidden rounded-2xl p-4'
|
||||||
style={{ background }}
|
style={{ background }}
|
||||||
>
|
>
|
||||||
<img className='rounded-lg shadow-2xl' src={coverUrl} />
|
{coverUrl && <img className='rounded-lg shadow-2xl' src={coverUrl} />}
|
||||||
|
|
||||||
<div className='ml-5 flex w-full flex-col justify-between text-white'>
|
<div className='ml-5 flex w-full flex-col justify-between text-white'>
|
||||||
{/* Track info */}
|
{/* Track info */}
|
||||||
<div>
|
<div>
|
||||||
<div className='text-xl font-semibold'>How Can I Make It OK?</div>
|
<div className='text-xl font-semibold'>{track?.name}</div>
|
||||||
<div className='opacity-75'>Wolf Alice</div>
|
<ArtistInline
|
||||||
|
className='line-clamp-2 opacity-75'
|
||||||
|
artists={track?.ar ?? []}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='-mb-1 flex items-center justify-between'>
|
<div className='-mb-1 flex items-center justify-between'>
|
||||||
{/* Actions */}
|
<MediaControls />
|
||||||
|
|
||||||
<div>
|
|
||||||
{Object.values(ACTION).map(action => (
|
|
||||||
<button
|
|
||||||
key={action}
|
|
||||||
className='btn-pressed-animation btn-hover-animation mr-1 cursor-default rounded-lg p-1.5 transition duration-200 after:bg-white/10'
|
|
||||||
>
|
|
||||||
<SvgIcon name={action} className='h-6 w-6' />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* FM logo */}
|
{/* FM logo */}
|
||||||
<div className='right-4 bottom-5 flex text-white opacity-20'>
|
<div className='right-4 bottom-5 flex text-white opacity-20'>
|
||||||
|
|
@ -62,4 +109,11 @@ const FMCard = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不能在player的构造函数中调用initFM
|
||||||
|
* 那样在APP启动时不会加载私人FM的数据
|
||||||
|
* 只能在这里进行数据的初始化
|
||||||
|
*/
|
||||||
|
player.initFM()
|
||||||
|
|
||||||
export default FMCard
|
export default FMCard
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import Slider from '@/components/Slider'
|
||||||
import SvgIcon from '@/components/SvgIcon'
|
import SvgIcon from '@/components/SvgIcon'
|
||||||
import { player } from '@/store'
|
import { player } from '@/store'
|
||||||
import { resizeImage } from '@/utils/common'
|
import { resizeImage } from '@/utils/common'
|
||||||
import { State as PlayerState } from '@/utils/player'
|
import { State as PlayerState, Mode as PlayerMode } from '@/utils/player'
|
||||||
|
|
||||||
const PlayingTrack = () => {
|
const PlayingTrack = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
@ -74,11 +74,22 @@ const MediaControls = () => {
|
||||||
const playerSnapshot = useSnapshot(player)
|
const playerSnapshot = useSnapshot(player)
|
||||||
const state = useMemo(() => playerSnapshot.state, [playerSnapshot.state])
|
const state = useMemo(() => playerSnapshot.state, [playerSnapshot.state])
|
||||||
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
|
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
|
||||||
|
const mode = useMemo(() => playerSnapshot.mode, [playerSnapshot.mode])
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center justify-center gap-2 text-black dark:text-white'>
|
<div className='flex items-center justify-center gap-2 text-black dark:text-white'>
|
||||||
<IconButton onClick={() => track && player.prevTrack()} disabled={!track}>
|
{mode === PlayerMode.PLAYLIST && (
|
||||||
<SvgIcon className='h-6 w-6' name='previous' />
|
<IconButton
|
||||||
</IconButton>
|
onClick={() => track && player.prevTrack()}
|
||||||
|
disabled={!track}
|
||||||
|
>
|
||||||
|
<SvgIcon className='h-6 w-6' name='previous' />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
{mode === PlayerMode.FM && (
|
||||||
|
<IconButton onClick={() => player.fmTrash()}>
|
||||||
|
<SvgIcon className='h-6 w-6' name='dislike' />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => track && player.playOrPause()}
|
onClick={() => track && player.playOrPause()}
|
||||||
disabled={!track}
|
disabled={!track}
|
||||||
|
|
@ -101,15 +112,20 @@ const MediaControls = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Others = () => {
|
const Others = () => {
|
||||||
|
const playerSnapshot = useSnapshot(player)
|
||||||
|
const mode = useMemo(() => playerSnapshot.mode, [playerSnapshot.mode])
|
||||||
|
|
||||||
|
const isFM = () => mode === PlayerMode.FM
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center justify-end gap-2 pr-2 text-black dark:text-white'>
|
<div className='flex items-center justify-end gap-2 pr-2 text-black dark:text-white'>
|
||||||
<IconButton onClick={() => toast('Work in progress')}>
|
<IconButton onClick={() => toast('Work in progress')} disabled={isFM()}>
|
||||||
<SvgIcon className='h-6 w-6' name='playlist' />
|
<SvgIcon className='h-6 w-6' name='playlist' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={() => toast('Work in progress')}>
|
<IconButton onClick={() => toast('Work in progress')} disabled={isFM()}>
|
||||||
<SvgIcon className='h-6 w-6' name='repeat' />
|
<SvgIcon className='h-6 w-6' name='repeat' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={() => toast('Work in progress')}>
|
<IconButton onClick={() => toast('Work in progress')} disabled={isFM()}>
|
||||||
<SvgIcon className='h-6 w-6' name='shuffle' />
|
<SvgIcon className='h-6 w-6' name='shuffle' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={() => toast('Work in progress')}>
|
<IconButton onClick={() => toast('Work in progress')}>
|
||||||
|
|
|
||||||
14
src/renderer/hooks/usePersonalFM.ts
Normal file
14
src/renderer/hooks/usePersonalFM.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { fetchPersonalFM, PersonalFMApiNames } from '@/api/personalFM'
|
||||||
|
import reactQueryClient from '@/utils/reactQueryClient'
|
||||||
|
|
||||||
|
export function fetchPersonalFMWithReactQuery() {
|
||||||
|
return reactQueryClient.fetchQuery(
|
||||||
|
PersonalFMApiNames.FETCH_PERSONAL_FM,
|
||||||
|
() => {
|
||||||
|
return fetchPersonalFM()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,8 @@ import {
|
||||||
fetchAudioSourceWithReactQuery,
|
fetchAudioSourceWithReactQuery,
|
||||||
fetchTracksWithReactQuery,
|
fetchTracksWithReactQuery,
|
||||||
} from '@/hooks/useTracks'
|
} from '@/hooks/useTracks'
|
||||||
|
import { fetchPersonalFMWithReactQuery } from '@/hooks/usePersonalFM'
|
||||||
|
import { fmTrash } from '@/api/personalFM'
|
||||||
import { cacheAudio } from '@/api/yesplaymusic'
|
import { cacheAudio } from '@/api/yesplaymusic'
|
||||||
import { clamp } from 'lodash-es'
|
import { clamp } from 'lodash-es'
|
||||||
|
|
||||||
|
|
@ -41,11 +43,14 @@ export class Player {
|
||||||
private _progress: number = 0
|
private _progress: number = 0
|
||||||
private _progressInterval: ReturnType<typeof setInterval> | undefined
|
private _progressInterval: ReturnType<typeof setInterval> | undefined
|
||||||
private _volume: number = 1 // 0 to 1
|
private _volume: number = 1 // 0 to 1
|
||||||
|
private _fmTrack: Track | null = null
|
||||||
|
private _fmInited = false
|
||||||
|
|
||||||
state: State = State.INITIALIZING
|
state: State = State.INITIALIZING
|
||||||
mode: Mode = Mode.PLAYLIST
|
mode: Mode = Mode.PLAYLIST
|
||||||
trackList: TrackID[] = []
|
trackList: TrackID[] = []
|
||||||
trackListSource: TrackListSource | null = null
|
trackListSource: TrackListSource | null = null
|
||||||
|
fmTrackList: TrackID[] = []
|
||||||
shuffle: boolean = false
|
shuffle: boolean = false
|
||||||
repeatMode: RepeatMode = RepeatMode.OFF
|
repeatMode: RepeatMode = RepeatMode.OFF
|
||||||
|
|
||||||
|
|
@ -89,8 +94,11 @@ export class Player {
|
||||||
* Get current playing track ID
|
* Get current playing track ID
|
||||||
*/
|
*/
|
||||||
get trackID(): TrackID {
|
get trackID(): TrackID {
|
||||||
const { trackList, _trackIndex } = this
|
if (this.mode === Mode.PLAYLIST) {
|
||||||
return trackList[_trackIndex] ?? 0
|
const { trackList, _trackIndex } = this
|
||||||
|
return trackList[_trackIndex] ?? 0
|
||||||
|
}
|
||||||
|
return this.fmTrackList[0] ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -100,6 +108,10 @@ export class Player {
|
||||||
return this._track ?? null
|
return this._track ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get fmTrack(): Track | null {
|
||||||
|
return this._fmTrack ?? null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get/Set progress of current track
|
* Get/Set progress of current track
|
||||||
*/
|
*/
|
||||||
|
|
@ -154,6 +166,7 @@ export class Player {
|
||||||
private async _playTrack() {
|
private async _playTrack() {
|
||||||
const track = await this._fetchTrack(this.trackID)
|
const track = await this._fetchTrack(this.trackID)
|
||||||
if (track) this._track = track
|
if (track) this._track = track
|
||||||
|
if (track && this.mode === Mode.FM) this._fmTrack = track
|
||||||
this._playAudio()
|
this._playAudio()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,7 +201,7 @@ export class Player {
|
||||||
|
|
||||||
private _howlerOnEndCallback() {
|
private _howlerOnEndCallback() {
|
||||||
console.log('_howlerOnEndCallback')
|
console.log('_howlerOnEndCallback')
|
||||||
if (this.repeatMode === RepeatMode.ONE) {
|
if (this.mode !== Mode.FM && this.repeatMode === RepeatMode.ONE) {
|
||||||
_howler.seek(0)
|
_howler.seek(0)
|
||||||
_howler.play()
|
_howler.play()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -201,6 +214,27 @@ export class Player {
|
||||||
cacheAudio(id, audio)
|
cacheAudio(id, audio)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _nextFMTrack() {
|
||||||
|
if (this.fmTrackList.length <= 1) {
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const response = await fetchPersonalFMWithReactQuery()
|
||||||
|
if (!response?.data?.length) continue
|
||||||
|
this.fmTrackList.shift()
|
||||||
|
this.fmTrackList.push(...response?.data?.map(r => r.id))
|
||||||
|
|
||||||
|
this._playTrack()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.fmTrackList.shift()
|
||||||
|
this._playTrack()
|
||||||
|
if (this.fmTrackList.length <= 1) {
|
||||||
|
const response = await fetchPersonalFMWithReactQuery()
|
||||||
|
this.fmTrackList.push(...response?.data?.map(r => r.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play current track
|
* Play current track
|
||||||
* @param {boolean} fade fade in
|
* @param {boolean} fade fade in
|
||||||
|
|
@ -255,6 +289,10 @@ export class Player {
|
||||||
* Play previous track
|
* Play previous track
|
||||||
*/
|
*/
|
||||||
prevTrack() {
|
prevTrack() {
|
||||||
|
if (this.mode === Mode.FM) {
|
||||||
|
toast('Personal FM not support previous track')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (this._prevTrackIndex === undefined) {
|
if (this._prevTrackIndex === undefined) {
|
||||||
toast('No previous track')
|
toast('No previous track')
|
||||||
return
|
return
|
||||||
|
|
@ -266,8 +304,13 @@ export class Player {
|
||||||
/**
|
/**
|
||||||
* Play next track
|
* Play next track
|
||||||
*/
|
*/
|
||||||
nextTrack() {
|
nextTrack(forceFM: boolean = false) {
|
||||||
console.log(this)
|
console.log(this)
|
||||||
|
if (forceFM || this.mode === Mode.FM) {
|
||||||
|
this.mode = Mode.FM
|
||||||
|
this._nextFMTrack()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (this._nextTrackIndex === undefined) {
|
if (this._nextTrackIndex === undefined) {
|
||||||
toast('No next track')
|
toast('No next track')
|
||||||
this.pause()
|
this.pause()
|
||||||
|
|
@ -316,6 +359,69 @@ export class Player {
|
||||||
this._playTrack()
|
this._playTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play personal fm
|
||||||
|
*/
|
||||||
|
async playFM() {
|
||||||
|
this.mode = Mode.FM
|
||||||
|
if (
|
||||||
|
this.fmTrackList.length > 0 &&
|
||||||
|
this._fmTrack?.id === this.fmTrackList[0]
|
||||||
|
) {
|
||||||
|
this._track = this._fmTrack
|
||||||
|
this._playAudio()
|
||||||
|
} else {
|
||||||
|
this._playTrack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init personal fm
|
||||||
|
* should only be called in components/FMCard
|
||||||
|
*/
|
||||||
|
async initFM() {
|
||||||
|
if (this._fmInited) return
|
||||||
|
const response = await fetchPersonalFMWithReactQuery()
|
||||||
|
this.fmTrackList.push(...response?.data?.map(r => r.id))
|
||||||
|
|
||||||
|
const trackId = this.fmTrackList[0]
|
||||||
|
const track = await this._fetchTrack(trackId)
|
||||||
|
if (track) this._fmTrack = track
|
||||||
|
this._fmInited = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trash current PersonalFMTrack
|
||||||
|
*/
|
||||||
|
async fmTrash() {
|
||||||
|
let trashId = this.fmTrackList.shift() ?? 0
|
||||||
|
if (trashId === 0) return
|
||||||
|
|
||||||
|
if (this.mode === Mode.FM) {
|
||||||
|
await this._nextFMTrack()
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < 5 && this.fmTrackList.length <= 1; i++) {
|
||||||
|
const response = await fetchPersonalFMWithReactQuery()
|
||||||
|
this.fmTrackList.push(...response?.data?.map(r => r.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
let track = await this._fetchTrack(this.fmTrackList.at(0) ?? 0)
|
||||||
|
if (track) {
|
||||||
|
this._fmTrack = track
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
this.fmTrackList.shift()
|
||||||
|
if (this.fmTrackList.length <= 1) {
|
||||||
|
const response = await fetchPersonalFMWithReactQuery()
|
||||||
|
this.fmTrackList.push(...response?.data?.map(r => r.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmTrash(trashId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play track in trackList by id
|
* Play track in trackList by id
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue