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 (
{!ready && (
)}
{formatDuration(item.duration)}
);
}
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 */}
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]) => (
))}