147 lines
3.6 KiB
TypeScript
147 lines
3.6 KiB
TypeScript
import { create } from 'zustand'
|
|
import type { Podcast } from '@/types'
|
|
import { isExternalUrl, getEmbedInfo } from '@/lib/embed'
|
|
|
|
// Hidden iframe manager for external content
|
|
let hiddenIframe: HTMLIFrameElement | null = null
|
|
|
|
function destroyIframe() {
|
|
if (hiddenIframe) {
|
|
hiddenIframe.remove()
|
|
hiddenIframe = null
|
|
}
|
|
}
|
|
|
|
function createHiddenIframe(embedUrl: string) {
|
|
destroyIframe()
|
|
const iframe = document.createElement('iframe')
|
|
iframe.src = embedUrl
|
|
iframe.allow = 'autoplay; encrypted-media'
|
|
iframe.style.cssText = 'position:fixed;width:1px;height:1px;left:-10px;top:-10px;opacity:0;pointer-events:none;'
|
|
document.body.appendChild(iframe)
|
|
hiddenIframe = iframe
|
|
}
|
|
|
|
interface PlayerState {
|
|
current: Podcast | null
|
|
isPlaying: boolean
|
|
isExternal: boolean
|
|
progress: number
|
|
duration: number
|
|
volume: number
|
|
audio: HTMLAudioElement | null
|
|
|
|
play: (podcast: Podcast) => void
|
|
toggle: () => void
|
|
pause: () => void
|
|
seek: (time: number) => void
|
|
setVolume: (vol: number) => void
|
|
setProgress: (progress: number) => void
|
|
setDuration: (duration: number) => void
|
|
}
|
|
|
|
export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
current: null,
|
|
isPlaying: false,
|
|
isExternal: false,
|
|
progress: 0,
|
|
duration: 0,
|
|
volume: 0.8,
|
|
audio: null,
|
|
|
|
play: (podcast) => {
|
|
const { audio, current } = get()
|
|
const external = isExternalUrl(podcast.audio_url)
|
|
|
|
// Resume same podcast
|
|
if (current?.id === podcast.id) {
|
|
if (external) {
|
|
const embed = getEmbedInfo(podcast.audio_url)
|
|
if (embed) createHiddenIframe(embed.embedUrl)
|
|
set({ isPlaying: true })
|
|
return
|
|
}
|
|
if (audio) {
|
|
audio.play()
|
|
set({ isPlaying: true })
|
|
return
|
|
}
|
|
}
|
|
|
|
// Stop previous
|
|
if (audio) {
|
|
audio.pause()
|
|
audio.removeAttribute('src')
|
|
}
|
|
destroyIframe()
|
|
|
|
// External: play via hidden iframe
|
|
if (external) {
|
|
const embed = getEmbedInfo(podcast.audio_url)
|
|
if (embed) createHiddenIframe(embed.embedUrl)
|
|
set({ audio: null, current: podcast, isPlaying: true, isExternal: true, progress: 0, duration: podcast.duration_seconds || 0 })
|
|
return
|
|
}
|
|
|
|
const newAudio = new Audio(podcast.audio_url)
|
|
newAudio.volume = get().volume
|
|
|
|
newAudio.addEventListener('timeupdate', () => {
|
|
set({ progress: newAudio.currentTime })
|
|
})
|
|
newAudio.addEventListener('loadedmetadata', () => {
|
|
set({ duration: newAudio.duration })
|
|
})
|
|
newAudio.addEventListener('ended', () => {
|
|
set({ isPlaying: false, progress: 0 })
|
|
})
|
|
|
|
newAudio.play()
|
|
set({ audio: newAudio, current: podcast, isPlaying: true, isExternal: false, progress: 0 })
|
|
},
|
|
|
|
toggle: () => {
|
|
const { audio, isPlaying, isExternal, current } = get()
|
|
if (isExternal) {
|
|
if (isPlaying) {
|
|
destroyIframe()
|
|
} else {
|
|
const embed = current ? getEmbedInfo(current.audio_url) : null
|
|
if (embed) createHiddenIframe(embed.embedUrl)
|
|
}
|
|
set({ isPlaying: !isPlaying })
|
|
return
|
|
}
|
|
if (!audio) return
|
|
if (isPlaying) {
|
|
audio.pause()
|
|
} else {
|
|
audio.play()
|
|
}
|
|
set({ isPlaying: !isPlaying })
|
|
},
|
|
|
|
pause: () => {
|
|
const { isExternal } = get()
|
|
if (isExternal) destroyIframe()
|
|
get().audio?.pause()
|
|
set({ isPlaying: false })
|
|
},
|
|
|
|
seek: (time) => {
|
|
const { audio } = get()
|
|
if (!audio) return
|
|
audio.currentTime = time
|
|
set({ progress: time })
|
|
},
|
|
|
|
setVolume: (vol) => {
|
|
const { audio } = get()
|
|
if (audio) audio.volume = vol
|
|
set({ volume: vol })
|
|
},
|
|
|
|
setProgress: (progress) => set({ progress }),
|
|
setDuration: (duration) => set({ duration }),
|
|
}))
|