feat(app): integrate Jamendo API and enhance UI components
- Added Jamendo API client ID to the environment configuration for music search. - Updated pnpm lock file to include new dependencies for enhanced functionality. - Integrated Plyr library for improved media playback experience. - Refactored ChatHeader, ChatInput, and ChatMessage components to utilize new styles and improve user interaction. - Enhanced CSS styles for path-glass elements to align with the new design system. Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Song } from '@aiui/core/types/content'
|
||||
import Plyr from 'plyr'
|
||||
import 'plyr/dist/plyr.css'
|
||||
|
||||
interface MusicSearchResult {
|
||||
source: string
|
||||
type: 'stream' | 'embed'
|
||||
url: string
|
||||
title?: string
|
||||
artist?: string
|
||||
}
|
||||
|
||||
const currentSong = ref<Song | null>(null)
|
||||
const playableSource = ref<MusicSearchResult | null>(null)
|
||||
const isPlaying = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const currentTime = ref(0)
|
||||
const duration = ref(0)
|
||||
|
||||
let plyrInstance: Plyr | null = null
|
||||
let containerEl: HTMLDivElement | null = null
|
||||
|
||||
export function usePlayer() {
|
||||
const hasTrack = computed(() => !!currentSong.value)
|
||||
const progress = computed(() => {
|
||||
if (duration.value <= 0) return 0
|
||||
return (currentTime.value / duration.value) * 100
|
||||
})
|
||||
|
||||
async function searchMusic(query: string, title?: string, artist?: string): Promise<MusicSearchResult | null> {
|
||||
try {
|
||||
const params = new URLSearchParams({ q: query })
|
||||
if (title) params.set('title', title)
|
||||
if (artist) params.set('artist', artist)
|
||||
const res = await fetch(`/api/music/search?${params}`)
|
||||
const data = (await res.json()) as MusicSearchResult & { error?: string }
|
||||
if (data.error || !data.url) {
|
||||
error.value = data.error ?? 'No playable source found'
|
||||
return null
|
||||
}
|
||||
return data as MusicSearchResult
|
||||
} catch (e) {
|
||||
error.value = 'Network error. Is the dev server running?'
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function setContainer(el: HTMLDivElement | null) {
|
||||
containerEl = el
|
||||
if (el && playableSource.value) {
|
||||
initPlayer(playableSource.value)
|
||||
}
|
||||
}
|
||||
|
||||
function initPlayer(result: MusicSearchResult) {
|
||||
if (!containerEl) return
|
||||
destroyPlayer()
|
||||
|
||||
if (result.type === 'stream') {
|
||||
const audio = document.createElement('audio')
|
||||
audio.src = result.url
|
||||
audio.crossOrigin = 'anonymous'
|
||||
containerEl.innerHTML = ''
|
||||
containerEl.appendChild(audio)
|
||||
plyrInstance = new Plyr(audio, {
|
||||
controls: [],
|
||||
autoplay: true,
|
||||
muted: false,
|
||||
})
|
||||
} else {
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.src = result.url
|
||||
iframe.style.width = '100%'
|
||||
iframe.style.height = '100%'
|
||||
iframe.style.border = 'none'
|
||||
containerEl.innerHTML = ''
|
||||
containerEl.appendChild(iframe)
|
||||
plyrInstance = null
|
||||
duration.value = 0
|
||||
currentTime.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
plyrInstance!.on('ready', () => {
|
||||
plyrInstance!.play().catch(() => { /* autoplay blocked */ })
|
||||
})
|
||||
plyrInstance!.on('timeupdate', () => {
|
||||
currentTime.value = plyrInstance!.currentTime ?? 0
|
||||
})
|
||||
plyrInstance!.on('loadedmetadata', () => {
|
||||
duration.value = plyrInstance!.duration ?? 0
|
||||
})
|
||||
plyrInstance!.on('ended', () => {
|
||||
isPlaying.value = false
|
||||
})
|
||||
plyrInstance!.on('playing', () => {
|
||||
isPlaying.value = true
|
||||
})
|
||||
plyrInstance!.on('pause', () => {
|
||||
isPlaying.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function destroyPlayer() {
|
||||
if (plyrInstance) {
|
||||
plyrInstance.destroy()
|
||||
plyrInstance = null
|
||||
}
|
||||
if (containerEl) {
|
||||
containerEl.innerHTML = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function play(song: Song) {
|
||||
error.value = null
|
||||
if (currentSong.value?.id === song.id && playableSource.value) {
|
||||
plyrInstance?.play()
|
||||
isPlaying.value = true
|
||||
return
|
||||
}
|
||||
isLoading.value = true
|
||||
const query = `${song.title} ${song.artist}`.trim()
|
||||
const result = await searchMusic(query, song.title, song.artist)
|
||||
isLoading.value = false
|
||||
currentSong.value = song
|
||||
if (!result) {
|
||||
if (!error.value) error.value = 'No playable source. Tries: Internet Archive, Jamendo, Odysee. Add JAMENDO_CLIENT_ID for more.'
|
||||
return
|
||||
}
|
||||
error.value = null
|
||||
playableSource.value = result
|
||||
if (containerEl) {
|
||||
initPlayer(result)
|
||||
}
|
||||
}
|
||||
|
||||
function pause() {
|
||||
plyrInstance?.pause()
|
||||
isPlaying.value = false
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (plyrInstance) {
|
||||
if (isPlaying.value) pause()
|
||||
else plyrInstance.play()
|
||||
}
|
||||
}
|
||||
|
||||
function seek(percent: number) {
|
||||
if (!plyrInstance || !duration.value) return
|
||||
const time = (percent / 100) * duration.value
|
||||
plyrInstance.currentTime = time
|
||||
currentTime.value = time
|
||||
}
|
||||
|
||||
function clear() {
|
||||
pause()
|
||||
destroyPlayer()
|
||||
currentSong.value = null
|
||||
playableSource.value = null
|
||||
currentTime.value = 0
|
||||
duration.value = 0
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
currentSong,
|
||||
playableSource,
|
||||
isPlaying,
|
||||
isLoading,
|
||||
error,
|
||||
currentTime,
|
||||
duration,
|
||||
hasTrack,
|
||||
progress,
|
||||
play,
|
||||
pause,
|
||||
toggle,
|
||||
seek,
|
||||
clear,
|
||||
setContainer,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user