feat: Add album artwork support
- Backend: artwork_filename column on clips table (auto-migration)
- Backend: PATCH /api/clips/{id}/artwork (upload image, 5MB limit)
- Backend: GET /api/clips/{id}/artwork (serve image with correct content-type)
- Backend: DELETE /api/clips/{id}/artwork (remove artwork file + null out DB)
- Backend: DELETE /api/clips/{id} now cascade-removes artwork file
- Frontend: Clip interface includes artwork_filename
- Frontend: Gallery ribbons show artwork thumbnail with hover overlay (play + upload)
- Frontend: Gallery tiles show artwork with hover overlay (play + upload)
- Frontend: DetailPanel shows full artwork with upload/change/remove controls
- Frontend: App.tsx handleUploadArtwork + handleRemoveArtwork handlers
- Storage: /data/artwork/ directory for image files
This commit is contained in:
1 parent
23672387e5
commit
8ba7133269
10 files changed
+1030
-59
No files matched your search
@@ -2,13 +2,18 @@ import { useState, useEffect } from 'react'
|
||||
import Generator from './components/Generator'
|
||||
import Gallery from './components/Gallery'
|
||||
import Player from './components/Player'
|
||||
import DetailPanel from './components/DetailPanel'
|
||||
|
||||
interface Clip {
|
||||
id: string
|
||||
title?: string
|
||||
prompt: string
|
||||
model: string
|
||||
duration_seconds: number
|
||||
status: string
|
||||
error_message?: string
|
||||
file_size_bytes?: number
|
||||
artwork_filename?: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -16,6 +21,7 @@ function App() {
|
||||
const [clips, setClips] = useState<Clip[]>([])
|
||||
const [currentClip, setCurrentClip] = useState<Clip | null>(null)
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [selectedClip, setSelectedClip] = useState<Clip | null>(null)
|
||||
|
||||
const fetchClips = async () => {
|
||||
try {
|
||||
@@ -51,6 +57,66 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleRename = async (id: string, title: string) => {
|
||||
try {
|
||||
await fetch(`/api/clips/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title }),
|
||||
})
|
||||
setClips(prev => prev.map(c => c.id === id ? { ...c, title } : c))
|
||||
if (currentClip?.id === id) {
|
||||
setCurrentClip({ ...currentClip, title })
|
||||
}
|
||||
if (selectedClip?.id === id) {
|
||||
setSelectedClip({ ...selectedClip, title })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to rename clip:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadArtwork = async (id: string, file: File) => {
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const res = await fetch(`/api/clips/${id}/artwork`, {
|
||||
method: 'PATCH',
|
||||
body: formData,
|
||||
})
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
console.error('Artwork upload failed:', err.detail)
|
||||
return
|
||||
}
|
||||
const updated = await res.json()
|
||||
setClips(prev => prev.map(c => c.id === id ? { ...c, artwork_filename: updated.artwork_filename } : c))
|
||||
if (currentClip?.id === id) {
|
||||
setCurrentClip({ ...currentClip, artwork_filename: updated.artwork_filename })
|
||||
}
|
||||
if (selectedClip?.id === id) {
|
||||
setSelectedClip({ ...selectedClip, artwork_filename: updated.artwork_filename })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to upload artwork:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveArtwork = async (id: string) => {
|
||||
try {
|
||||
await fetch(`/api/clips/${id}/artwork`, { method: 'DELETE' })
|
||||
setClips(prev => prev.map(c => c.id === id ? { ...c, artwork_filename: null } : c))
|
||||
if (currentClip?.id === id) {
|
||||
setCurrentClip({ ...currentClip, artwork_filename: null })
|
||||
}
|
||||
if (selectedClip?.id === id) {
|
||||
setSelectedClip({ ...selectedClip, artwork_filename: null })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to remove artwork:', err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-lyria-dark">
|
||||
<header className="border-b border-white/10 px-6 py-4">
|
||||
@@ -72,6 +138,9 @@ function App() {
|
||||
clips={clips}
|
||||
onPlay={handlePlay}
|
||||
onDelete={handleDelete}
|
||||
onRename={handleRename}
|
||||
onSelect={(clip) => setSelectedClip(clip)}
|
||||
onUploadArtwork={handleUploadArtwork}
|
||||
currentClipId={currentClip?.id}
|
||||
/>
|
||||
</div>
|
||||
@@ -88,6 +157,17 @@ function App() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedClip && (
|
||||
<DetailPanel
|
||||
clip={selectedClip}
|
||||
onClose={() => setSelectedClip(null)}
|
||||
onRename={handleRename}
|
||||
onDelete={handleDelete}
|
||||
onUploadArtwork={handleUploadArtwork}
|
||||
onRemoveArtwork={handleRemoveArtwork}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user