- Agrega @pinia/nuxt, idb y store central (stores/music.ts) - Cacheo manual desde menú contextual y borrado (TrackContextMenu) - Ícono verde para canciones cacheadas, sin auto-cache al reproducir - Toasts de feedback (stores/toast.ts, ToastContainer) - Fallback offline de listado a IndexedDB; fix MUSIC_DIR absoluto en preview/prod - Ajustes PWA: navigateFallback '/', devOptions, workbox condicional - Estilos y animación del context menu (tema light/dark, blur fuerte) - Correcciones de sintaxis y posicionamiento exacto al cursor
133 lines
3.4 KiB
TypeScript
133 lines
3.4 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { openDB, type IDBPDatabase } from 'idb'
|
|
|
|
type Track = {
|
|
name: string
|
|
duration?: number
|
|
}
|
|
|
|
type DBTrack = {
|
|
name: string
|
|
blob: Blob
|
|
type?: string
|
|
duration?: number
|
|
}
|
|
|
|
let dbPromise: Promise<IDBPDatabase> | null = null
|
|
|
|
async function getDB() {
|
|
if (!dbPromise) {
|
|
dbPromise = openDB('repodructor-music', 1, {
|
|
upgrade(db) {
|
|
if (!db.objectStoreNames.contains('tracks')) {
|
|
const store = db.createObjectStore('tracks', { keyPath: 'name' })
|
|
store.createIndex('name', 'name', { unique: true })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
return dbPromise
|
|
}
|
|
|
|
export const useMusicStore = defineStore('music', {
|
|
state: () => ({
|
|
tracks: [] as Track[],
|
|
cachedNames: new Set<string>(),
|
|
loading: false,
|
|
error: null as string | null
|
|
}),
|
|
getters: {
|
|
isCached: (state) => (name: string) => state.cachedNames.has(name)
|
|
},
|
|
actions: {
|
|
async fetchTracks() {
|
|
this.loading = true
|
|
this.error = null
|
|
try {
|
|
const response = await $fetch<{ tracks: Track[] }>('/api/music')
|
|
this.tracks = response.tracks || []
|
|
} catch (e: any) {
|
|
this.error = e?.message || 'Failed to load tracks'
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
|
|
async loadCachedNames() {
|
|
try {
|
|
const db = await getDB()
|
|
const tx = db.transaction('tracks', 'readonly')
|
|
const store = tx.objectStore('tracks')
|
|
const all = await store.getAllKeys()
|
|
this.cachedNames = new Set((all as string[]) || [])
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
},
|
|
|
|
async loadCachedTracks(): Promise<Track[]> {
|
|
try {
|
|
const db = await getDB()
|
|
const tx = db.transaction('tracks', 'readonly')
|
|
const store = tx.objectStore('tracks')
|
|
const all = (await store.getAll()) as DBTrack[]
|
|
const names = all.map(t => t.name)
|
|
this.cachedNames = new Set(names)
|
|
const list: Track[] = all.map(t => ({ name: t.name, duration: t.duration }))
|
|
return list
|
|
} catch (e) {
|
|
return []
|
|
}
|
|
},
|
|
|
|
async getCachedBlob(name: string): Promise<Blob | null> {
|
|
try {
|
|
const db = await getDB()
|
|
const entry = (await db.get('tracks', name)) as DBTrack | undefined
|
|
if (entry && entry.blob) {
|
|
this.cachedNames.add(name)
|
|
return entry.blob
|
|
}
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
return null
|
|
},
|
|
|
|
async saveTrackBlob(name: string, blob: Blob, duration?: number) {
|
|
try {
|
|
const db = await getDB()
|
|
const payload: DBTrack = { name, blob, type: blob.type, duration }
|
|
await db.put('tracks', payload)
|
|
this.cachedNames.add(name)
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
},
|
|
|
|
async deleteCachedTrack(name: string) {
|
|
try {
|
|
const db = await getDB()
|
|
await db.delete('tracks', name)
|
|
this.cachedNames.delete(name)
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
},
|
|
|
|
// Fetch from API and store in IndexedDB
|
|
async cacheByName(name: string, duration?: number): Promise<boolean> {
|
|
try {
|
|
const encodedName = encodeURIComponent(name)
|
|
const response = await fetch(`/api/music/${encodedName}`)
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
const blob = await response.blob()
|
|
await this.saveTrackBlob(name, blob, duration)
|
|
return true
|
|
} catch (e) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
})
|