wetalk/src/stores/player.ts
ordinarthur 96eff9433c
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 28s
fix: uniform podcast player for all content types, hidden iframe for external audio
2026-04-12 22:07:03 +02:00

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