import React, { useState, useRef, useEffect, useCallback } from "react"; import { Film, Music, Upload, Search, X, Plus, Play, Pause, Tag as TagIcon, Trash2, ListMusic, MoreVertical, FolderPlus, Clock } from "lucide-react"; // ---------- design tokens ---------- const T = { bg: "#14120F", surface: "#1F1B16", surface2: "#262019", line: "#332E27", text: "#F1EAE0", textDim: "#9C9284", accent: "#E3A344", accentDim: "#8B6A2F", accentSoft: "#3A2E1C", }; function formatBytes(b) { if (!b) return "0 KB"; const units = ["B", "KB", "MB", "GB"]; let i = 0; while (b >= 1024 && i < units.length - 1) { b /= 1024; i++; } return `${b.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; } function formatDuration(s) { if (!s || !isFinite(s)) return "--:--"; const m = Math.floor(s / 60); const sec = Math.floor(s % 60); return `${m}:${sec.toString().padStart(2, "0")}`; } // deterministic pseudo-waveform bars from filename, purely decorative function waveformBars(name) { let seed = 0; for (let i = 0; i < name.length; i++) seed = (seed * 31 + name.charCodeAt(i)) % 997; const bars = []; for (let i = 0; i < 40; i++) { seed = (seed * 1103515245 + 12345) % 2147483648; bars.push(20 + (seed % 80)); } return bars; } function SprocketStrip() { return (
); } function VideoThumb({ item }) { const canvasRef = useRef(null); const videoRef = useRef(null); const [ready, setReady] = useState(false); const drawFrame = useCallback(() => { const v = videoRef.current, c = canvasRef.current; if (!v || !c) return; const ctx = c.getContext("2d"); c.width = 320; c.height = 180; try { ctx.drawImage(v, 0, 0, c.width, c.height); } catch (e) {} }, []); useEffect(() => { const v = videoRef.current; if (!v) return; const onLoaded = () => { v.currentTime = Math.min(1, (v.duration || 2) * 0.1); }; const onSeeked = () => { drawFrame(); setReady(true); }; v.addEventListener("loadedmetadata", onLoaded); v.addEventListener("seeked", onSeeked); return () => { v.removeEventListener("loadedmetadata", onLoaded); v.removeEventListener("seeked", onSeeked); }; }, [drawFrame]); const handleMove = (e) => { const v = videoRef.current; if (!v || !v.duration) return; const rect = e.currentTarget.getBoundingClientRect(); const frac = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width)); v.currentTime = frac * v.duration; }; const handleLeave = () => { const v = videoRef.current; if (v) v.currentTime = Math.min(1, (v.duration || 2) * 0.1); }; return (
); } function AudioThumb({ item }) { const bars = waveformBars(item.name); return (
{bars.map((h, i) => (
))}
); } function MediaCard({ item, playlists, onOpen, onAddTag, onRemoveTag, onDelete, onAddToPlaylist }) { const [tagInput, setTagInput] = useState(""); const [menuOpen, setMenuOpen] = useState(false); const [tagOpen, setTagOpen] = useState(false); return (
onOpen(item)}> {item.type === "video" ? : }

onOpen(item)} title={item.name} > {item.name.length > 42 ? item.name.slice(0, 40) + "…" : item.name}

{menuOpen && (
Add to playlist
{playlists.length === 0 && (
No playlists yet
)} {playlists.map((p) => ( ))}
)}
{item.type === "video" ? formatDuration(item.duration) : formatDuration(item.duration)} · {formatBytes(item.size)}
{item.tags.map((t) => ( {t} onRemoveTag(item.id, t)} /> ))} {tagOpen ? ( setTagInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && tagInput.trim()) { onAddTag(item.id, tagInput.trim()); setTagInput(""); setTagOpen(false); } if (e.key === "Escape") setTagOpen(false); }} onBlur={() => setTagOpen(false)} placeholder="tag + Enter" className="text-[11px] px-2 py-0.5 rounded-full outline-none w-24" style={{ backgroundColor: T.bg, color: T.text, border: `1px solid ${T.accentDim}`, fontFamily: "'IBM Plex Mono', monospace" }} /> ) : ( )}
); } function PlayerDrawer({ item, onClose, onAddTag, onRemoveTag }) { const [tagInput, setTagInput] = useState(""); if (!item) return null; return (
{item.type === "video" ? (

{item.name}

{item.type === "video" ? "VIDEO" : "AUDIO"} · {formatDuration(item.duration)} · {formatBytes(item.size)}
{item.tags.map((t) => ( {t} onRemoveTag(item.id, t)} /> ))} setTagInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && tagInput.trim()) { onAddTag(item.id, tagInput.trim()); setTagInput(""); } }} placeholder="add tag + Enter" className="text-[11px] px-2 py-0.5 rounded-full outline-none w-28" style={{ backgroundColor: T.bg, color: T.text, border: `1px solid ${T.accentDim}`, fontFamily: "'IBM Plex Mono', monospace" }} />
); } export default function MediaLibrary() { const [items, setItems] = useState([]); const [playlists, setPlaylists] = useState([]); const [search, setSearch] = useState(""); const [typeFilter, setTypeFilter] = useState("all"); const [tagFilter, setTagFilter] = useState(null); const [playlistFilter, setPlaylistFilter] = useState(null); const [selected, setSelected] = useState(null); const [dragging, setDragging] = useState(false); const [newPlaylistName, setNewPlaylistName] = useState(""); const [showNewPlaylist, setShowNewPlaylist] = useState(false); const fileInputRef = useRef(null); const dragCounter = useRef(0); const handleFiles = useCallback((fileList) => { Array.from(fileList).forEach((file) => { const isVideo = file.type.startsWith("video/"); const isAudio = file.type.startsWith("audio/"); if (!isVideo && !isAudio) return; const url = URL.createObjectURL(file); const id = `${file.name}-${file.size}-${Date.now()}-${Math.random().toString(36).slice(2)}`; const probe = document.createElement(isVideo ? "video" : "audio"); probe.preload = "metadata"; probe.src = url; probe.onloadedmetadata = () => { setItems((prev) => [ ...prev, { id, name: file.name, size: file.size, type: isVideo ? "video" : "audio", url, duration: probe.duration, tags: [], addedAt: Date.now(), }, ]); }; }); }, []); useEffect(() => { const onDragEnter = (e) => { e.preventDefault(); dragCounter.current++; setDragging(true); }; const onDragOver = (e) => e.preventDefault(); const onDragLeave = (e) => { e.preventDefault(); dragCounter.current--; if (dragCounter.current <= 0) { setDragging(false); dragCounter.current = 0; } }; const onDrop = (e) => { e.preventDefault(); dragCounter.current = 0; setDragging(false); if (e.dataTransfer.files?.length) handleFiles(e.dataTransfer.files); }; window.addEventListener("dragenter", onDragEnter); window.addEventListener("dragover", onDragOver); window.addEventListener("dragleave", onDragLeave); window.addEventListener("drop", onDrop); return () => { window.removeEventListener("dragenter", onDragEnter); window.removeEventListener("dragover", onDragOver); window.removeEventListener("dragleave", onDragLeave); window.removeEventListener("drop", onDrop); }; }, [handleFiles]); const addTag = (id, tag) => { setItems((prev) => prev.map((it) => it.id === id && !it.tags.includes(tag) ? { ...it, tags: [...it.tags, tag] } : it)); }; const removeTag = (id, tag) => { setItems((prev) => prev.map((it) => it.id === id ? { ...it, tags: it.tags.filter((t) => t !== tag) } : it)); }; const deleteItem = (id) => { setItems((prev) => prev.filter((it) => it.id !== id)); setPlaylists((prev) => prev.map((p) => ({ ...p, itemIds: p.itemIds.filter((i) => i !== id) }))); if (selected?.id === id) setSelected(null); }; const createPlaylist = () => { if (!newPlaylistName.trim()) return; setPlaylists((prev) => [...prev, { id: `pl-${Date.now()}`, name: newPlaylistName.trim(), itemIds: [] }]); setNewPlaylistName(""); setShowNewPlaylist(false); }; const addToPlaylist = (itemId, playlistId) => { setPlaylists((prev) => prev.map((p) => p.id === playlistId && !p.itemIds.includes(itemId) ? { ...p, itemIds: [...p.itemIds, itemId] } : p)); }; const allTags = Array.from(new Set(items.flatMap((it) => it.tags))); const filtered = items.filter((it) => { if (typeFilter !== "all" && it.type !== typeFilter) return false; if (tagFilter && !it.tags.includes(tagFilter)) return false; if (playlistFilter) { const p = playlists.find((pl) => pl.id === playlistFilter); if (!p || !p.itemIds.includes(it.id)) return false; } if (search.trim() && !it.name.toLowerCase().includes(search.toLowerCase())) return false; return true; }).sort((a, b) => b.addedAt - a.addedAt); return (
{/* drag overlay */} {dragging && (

Drop to add to your library

Video and audio files stay on this device

)} {/* header */}
Light Table
setSearch(e.target.value)} placeholder="Search your library" className="w-full pl-9 pr-3 py-2 rounded-md text-sm outline-none" style={{ backgroundColor: T.surface, color: T.text, border: `1px solid ${T.line}` }} />
{[["all", "All"], ["video", "Video"], ["audio", "Songs"]].map(([val, label]) => ( ))}