import { useState, useEffect, useCallback, useRef } from 'react'; import { X, Search, Folder, Grid, Plus, Tag, Trash2, RefreshCw, Upload, Edit3, Copy, ExternalLink, ChevronLeft } from 'lucide-react'; import { useAppStore } from '../store'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; interface LibItem { id: string; url: string; source_url: string; title: string; data_url: string; hash: string; width: number; height: number; colors: string[]; tags: string[]; created_at: number; } export const LibraryPanel = () => { const { isLibraryOpen, setIsLibraryOpen, setImages, pan, zoom } = useAppStore(); const [search, setSearch] = useState(''); const [activeTag, setActiveTag] = useState(null); const [libraryItems, setLibraryItems] = useState([]); const [isLoading, setIsLoading] = useState(false); const [allTags, setAllTags] = useState([]); const [isDragOver, setIsDragOver] = useState(false); const [editingItem, setEditingItem] = useState(null); const fileInputRef = useRef(null); const rebuildTags = (items: LibItem[]) => { const tags = new Set(); items.forEach(item => (item.tags || []).forEach(t => tags.add(t))); setAllTags(Array.from(tags).sort()); }; const loadLibrary = useCallback(() => { setIsLoading(true); invoke('library_items').then(items => { setLibraryItems(items); rebuildTags(items); setIsLoading(false); }).catch(() => setIsLoading(false)); }, []); useEffect(() => { if (isLibraryOpen) loadLibrary(); }, [isLibraryOpen, loadLibrary]); useEffect(() => { const unlisten = listen('board://image_added', loadLibrary); return () => { unlisten.then(fn => fn()); }; }, [loadLibrary]); useEffect(() => { const handler = () => loadLibrary(); window.addEventListener('muse:library-refresh', handler); return () => window.removeEventListener('muse:library-refresh', handler); }, [loadLibrary]); const addToCanvas = (item: LibItem) => { const w = Math.min(500, item.width || 300); const h = item.height ? w * (item.height / item.width) : w; setImages(prev => [...prev, { id: crypto.randomUUID(), url: item.data_url || item.url, sourceUrl: item.source_url || item.url, x: (-pan.x + window.innerWidth / 3) / zoom, y: (-pan.y + window.innerHeight / 3) / zoom, width: w, height: h, aspectRatio: w / h }]); }; const importFileAsDataUrl = (file: File) => { if (!file.type.startsWith('image/')) return; const reader = new FileReader(); reader.onload = async (ev) => { const dataUrl = ev.target?.result as string; if (!dataUrl) return; try { const item = await invoke('library_import_data_url', { dataUrl, title: file.name.replace(/\.[^/.]+$/, '') }); addToCanvas(item); loadLibrary(); } catch (err) { console.error('Import failed:', err); } }; reader.readAsDataURL(file); }; const handleBrowserDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); Array.from(e.dataTransfer.files).forEach(importFileAsDataUrl); }; const handleFileUpload = (e: React.ChangeEvent) => { if (e.target.files) Array.from(e.target.files).forEach(importFileAsDataUrl); e.target.value = ''; }; const handleDragStart = (e: React.DragEvent, item: LibItem) => { const payload = JSON.stringify({ id: item.id, data_url: item.data_url, width: item.width, height: item.height, title: item.title }); e.dataTransfer.setData('application/x-muse-library-item', payload); e.dataTransfer.setData('text/plain', payload); e.dataTransfer.effectAllowed = 'copy'; }; const handleDelete = (id: string) => invoke('library_remove_item', { id }).then(() => { const next = libraryItems.filter(i => i.id !== id); setLibraryItems(next); rebuildTags(next); if (editingItem?.id === id) setEditingItem(null); }); const updateItem = (updated: LibItem) => { const next = libraryItems.map(i => i.id === updated.id ? updated : i); setLibraryItems(next); rebuildTags(next); setEditingItem(updated); }; const filtered = libraryItems.filter(img => { if (activeTag && !(img.tags || []).includes(activeTag)) return false; if (!search) return true; const q = search.toLowerCase(); return (img.tags || []).some(t => t.toLowerCase().includes(q)) || (img.title || '').toLowerCase().includes(q) || (img.source_url || img.url || '').toLowerCase().includes(q); }); if (editingItem) return setEditingItem(null)} onUpdate={updateItem} onDelete={() => { handleDelete(editingItem.id); setEditingItem(null); }} onAddToCanvas={() => addToCanvas(editingItem)} isOpen={isLibraryOpen} onClosePanel={() => setIsLibraryOpen(false)} />; return (
{ e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; setIsDragOver(true); }} onDragLeave={() => setIsDragOver(false)} onDrop={handleBrowserDrop}> {isDragOver &&
Drop images to import
}
Asset Library ({libraryItems.length})
setSearch(e.target.value)} placeholder="Search by name, tag, or URL..." className="w-full bg-[#1C1C1E] text-[#E0E0E0] pl-9 pr-3 py-2 text-[13px] rounded-lg border border-[#3A3A3E] focus:border-[#0A84FF] outline-none placeholder:text-[#606060]" />
{allTags.length > 0 &&
{allTags.map(tag => )}
}
Images ({filtered.length})
fileInputRef.current?.click()} className="aspect-square bg-white/5 hover:bg-white/10 border border-dashed border-white/20 hover:border-[#0A84FF]/50 rounded-xl cursor-pointer flex flex-col items-center justify-center text-[#A0A0A0] hover:text-[#E0E0E0] transition-all group shadow-sm">
Import Filesor drag here
{filtered.map(img =>
handleDragStart(e, img)} onClick={() => addToCanvas(img)}>
{img.title || 'Reference'}{img.tags?.length > 0 &&
{img.tags.slice(0, 3).map(t => {t})}
}
{img.width}×{img.height}
{img.colors?.length > 0 &&
{img.colors.slice(0, 6).map((c, ci) =>
)}
}
)} {filtered.length === 0 && !isLoading &&

Library is empty

Drag files here, use Import, or capture from browser.

} {isLoading &&
Loading...
}
); }; function MetadataEditor({ item, onClose, onUpdate, onDelete, onAddToCanvas, isOpen, onClosePanel }: { item: LibItem; onClose: () => void; onUpdate: (item: LibItem) => void; onDelete: () => void; onAddToCanvas: () => void; isOpen: boolean; onClosePanel: () => void }) { const [title, setTitle] = useState(item.title); const [newTag, setNewTag] = useState(''); useEffect(() => { setTitle(item.title); }, [item.id]); const saveTitle = () => { if (title.trim() === item.title) return; invoke('library_update_metadata', { id: item.id, title: title.trim() || null, tags: null }).then(onUpdate).catch(() => {}); }; const addTag = () => { if (!newTag.trim()) return; invoke('library_add_tag', { id: item.id, tag: newTag.trim() }).then(updated => { onUpdate(updated); setNewTag(''); }).catch(() => {}); }; const removeTag = (tag: string) => invoke('library_remove_tag', { id: item.id, tag }).then(onUpdate).catch(() => {}); return
setTitle(e.target.value)} onBlur={saveTitle} onKeyDown={e => { if (e.key === 'Enter') saveTitle(); }} className="w-full bg-[#2A2A2E] text-[#E0E0E0] px-3 py-2 text-[13px] rounded-lg border border-[#3A3A3E] focus:border-[#0A84FF] outline-none" />
{item.width} × {item.height} px
{(item.tags || []).map(tag => {tag})}
setNewTag(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') addTag(); }} placeholder="Add tag..." className="flex-1 bg-[#2A2A2E] text-[#E0E0E0] px-3 py-1.5 text-[12px] rounded-lg border border-[#3A3A3E] focus:border-[#0A84FF] outline-none" />
{item.colors?.length > 0 &&
{item.colors.map((c, i) => )}
}{item.source_url &&
{item.source_url}
}
; }