8408fd6466
ci/woodpecker/push/woodpecker Pipeline was successful
SEO : - composable useSeoPage() : og:*, Twitter Cards, canonical sur toutes les pages (15 pages) - app.vue : JSON-LD Organization + Book, og:image global og-default.png - og-default.png 1200×630 : logo § calligraphique + texte (Pillow) - nuxt.config.ts : @nuxtjs/sitemap avec 26 URLs statiques Analytics Umami : - useTracking() : helpers typés audio/pdf/player/scroll/cta - useScrollTracking() : scroll depth 25/50/75/100% + liens externes auto - useAudioPlayer : trackAudioPlay/Progress/Complete - BookPdfReader : trackPdfOpen/Close avec durée - BookPlayer : trackPlayerOpen/Chapter/Mode - docker-compose : variables NUXT_PUBLIC_UMAMI_* passées au container Images : - Couv-Economie-du-don.jpg ajoutée dans public/images/ - bookplayer.config.yml + home.yml : références mises à jour Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
168 lines
3.8 KiB
TypeScript
168 lines
3.8 KiB
TypeScript
import type { Song } from '~/types/song'
|
|
|
|
let audio: HTMLAudioElement | null = null
|
|
let animationFrameId: number | null = null
|
|
// Track which milestones have been fired for the current song
|
|
const firedMilestones = new Set<number>()
|
|
|
|
export function useAudioPlayer() {
|
|
const store = usePlayerStore()
|
|
const { trackAudioPlay, trackAudioComplete, trackAudioProgress } = useTracking()
|
|
|
|
function getAudio(): HTMLAudioElement {
|
|
if (!audio) {
|
|
audio = new Audio()
|
|
audio.preload = 'metadata'
|
|
audio.volume = store.volume
|
|
|
|
audio.addEventListener('loadedmetadata', () => {
|
|
store.setDuration(audio!.duration)
|
|
})
|
|
|
|
audio.addEventListener('ended', () => {
|
|
if (store.currentSong) {
|
|
trackAudioComplete(store.currentSong.id, store.currentSong.title)
|
|
}
|
|
firedMilestones.clear()
|
|
const next = store.nextSong()
|
|
if (next) {
|
|
loadAndPlay(next)
|
|
}
|
|
})
|
|
|
|
audio.addEventListener('error', (e) => {
|
|
console.error('Audio error:', e)
|
|
store.pause()
|
|
})
|
|
}
|
|
return audio
|
|
}
|
|
|
|
function startTimeUpdate() {
|
|
if (animationFrameId) return
|
|
const update = () => {
|
|
if (audio && !audio.paused) {
|
|
store.setCurrentTime(audio.currentTime)
|
|
// Track progress milestones (25 / 50 / 75 %)
|
|
if (audio.duration > 0 && store.currentSong) {
|
|
const pct = (audio.currentTime / audio.duration) * 100
|
|
for (const milestone of [25, 50, 75] as const) {
|
|
if (pct >= milestone && !firedMilestones.has(milestone)) {
|
|
firedMilestones.add(milestone)
|
|
trackAudioProgress(store.currentSong.id, milestone)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
animationFrameId = requestAnimationFrame(update)
|
|
}
|
|
animationFrameId = requestAnimationFrame(update)
|
|
}
|
|
|
|
function stopTimeUpdate() {
|
|
if (animationFrameId) {
|
|
cancelAnimationFrame(animationFrameId)
|
|
animationFrameId = null
|
|
}
|
|
}
|
|
|
|
async function loadAndPlay(song: Song) {
|
|
const el = getAudio()
|
|
store.setSong(song)
|
|
firedMilestones.clear()
|
|
|
|
// Try OGG first, fall back to MP3
|
|
const oggPath = song.file.replace(/\.mp3$/, '.ogg')
|
|
const canOgg = el.canPlayType('audio/ogg; codecs=vorbis')
|
|
|
|
el.src = canOgg ? oggPath : song.file
|
|
el.volume = store.volume
|
|
|
|
try {
|
|
await el.play()
|
|
store.play()
|
|
startTimeUpdate()
|
|
trackAudioPlay(song.id, song.title)
|
|
}
|
|
catch {
|
|
// If OGG failed, try MP3
|
|
if (el.src !== song.file) {
|
|
el.src = song.file
|
|
try {
|
|
await el.play()
|
|
store.play()
|
|
startTimeUpdate()
|
|
trackAudioPlay(song.id, song.title)
|
|
}
|
|
catch (err) {
|
|
console.error('Playback failed:', err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function pause() {
|
|
getAudio().pause()
|
|
store.pause()
|
|
stopTimeUpdate()
|
|
}
|
|
|
|
function resume() {
|
|
const el = getAudio()
|
|
if (el.src) {
|
|
el.play()
|
|
store.play()
|
|
startTimeUpdate()
|
|
}
|
|
}
|
|
|
|
function togglePlayPause() {
|
|
if (store.isPlaying) {
|
|
pause()
|
|
}
|
|
else {
|
|
resume()
|
|
}
|
|
}
|
|
|
|
function seek(time: number) {
|
|
const el = getAudio()
|
|
el.currentTime = time
|
|
store.setCurrentTime(time)
|
|
}
|
|
|
|
function setVolume(vol: number) {
|
|
store.setVolume(vol)
|
|
getAudio().volume = store.volume
|
|
}
|
|
|
|
function playNext() {
|
|
const song = store.nextSong()
|
|
if (song) loadAndPlay(song)
|
|
}
|
|
|
|
function playPrev() {
|
|
const song = store.prevSong()
|
|
if (song) {
|
|
loadAndPlay(song)
|
|
}
|
|
}
|
|
|
|
// Watch volume changes from store
|
|
watch(() => store.volume, (vol) => {
|
|
if (audio) audio.volume = vol
|
|
})
|
|
|
|
return {
|
|
loadAndPlay,
|
|
pause,
|
|
resume,
|
|
togglePlayPause,
|
|
seek,
|
|
setVolume,
|
|
playNext,
|
|
playPrev,
|
|
getAudio,
|
|
}
|
|
}
|