meatloaf-config/src/app/components/MediaEntry.tsx

104 lines
6.0 KiB
TypeScript

import {
BookOpen,
Braces,
CassetteTape,
ChevronRight,
Code2,
Cpu,
Disc,
File,
FileText,
FileType,
Folder,
HardDrive,
Image as ImageIcon,
MoreVertical,
Music,
Package,
Save,
SlidersHorizontal,
Terminal,
} from 'lucide-react';
import { humanFileSize, type EntryInfo } from '../webdav';
// ─── Extension sets ───────────────────────────────────────────────────────────
export const TEXT_EXTS = new Set(['txt', 'cfg', 'ini', 'seq', 'log', 'csv', 'lst']);
export const DOC_EXTS = new Set(['doc', 'docx', 'odt', 'rtf', 'pdf', 'pages', 'tex', 'xls', 'xlsx', 'ods', 'ppt', 'pptx', 'odp']);
export const CODE_EXTS = new Set(['asm', 'bas', 's', 'js', 'ts', 'jsx', 'tsx', 'css', 'scss', 'py', 'c', 'cpp', 'h', 'hpp', 'lua', 'sh', 'bash', 'php', 'rb', 'rs', 'go', 'java', 'cs', 'kt', 'sql', 'pl']);
export const MD_EXTS = new Set(['md', 'markdown']);
export const JSON_EXTS = new Set(['json', 'webmanifest']);
export const XML_EXTS = new Set(['xml', 'html', 'htm', 'rss', 'atom', 'xsl']);
export const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'ico']);
export const AUDIO_EXTS = new Set(['sid', 'psid', 'rsid', 'mus', 'vgm']);
export const ROM_EXTS = new Set(['bin', 'rom', 'crt']);
export const TAPE_EXTS = new Set(['tap', 'htap', 't64', 'tcrt']);
export const DISK_EXTS = new Set(['c64', 'd41', 'd64', 'd67', 'd71', 'd80', 'd81', 'd82', 'd88', 'f64', 'g41', 'g64', 'g71', 'g81', 'm2i', 'nbz', 'nib', 'p64', 'p71', 'p81', 'scp', 'x64']);
export const DISC_EXTS = new Set(['iso', 'img', 'cue']);
export const HD_EXTS = new Set(['d1m', 'd2m', 'd4m', 'd90', 'dhd', 'hdd', 'bbt', 'd8b', 'dfi']);
export const ARCHIVE_EXTS = new Set(['zip', '7z', 'tar', 'gz', 'bz2', 'xz', 'rar', 'arj', 'lzh', 'ace', 'z', 'lha', 'cab', 'lbr', 'arc', 'ark', 'lnx']);
export const CONFIG_EXTS = new Set(['config']);
// ─── EntryIcon ────────────────────────────────────────────────────────────────
export function EntryIcon({ entry }: { entry: EntryInfo }) {
if (entry.type === 'folder') return <Folder className="w-5 h-5 text-blue-500 flex-shrink-0" />;
const ext = entry.name.split('.').pop()?.toLowerCase() ?? '';
if (IMAGE_EXTS.has(ext)) return <ImageIcon className="w-5 h-5 text-purple-500 flex-shrink-0" />;
if (DISK_EXTS.has(ext)) return <Save className="w-5 h-5 text-amber-500 flex-shrink-0" />;
if (HD_EXTS.has(ext)) return <HardDrive className="w-5 h-5 text-orange-500 flex-shrink-0" />;
if (DISC_EXTS.has(ext)) return <Disc className="w-5 h-5 text-sky-500 flex-shrink-0" />;
if (TAPE_EXTS.has(ext)) return <CassetteTape className="w-5 h-5 text-rose-400 flex-shrink-0" />;
if (ROM_EXTS.has(ext)) return <Cpu className="w-5 h-5 text-red-500 flex-shrink-0" />;
if (AUDIO_EXTS.has(ext)) return <Music className="w-5 h-5 text-teal-500 flex-shrink-0" />;
if (ARCHIVE_EXTS.has(ext)) return <Package className="w-5 h-5 text-yellow-600 flex-shrink-0" />;
if (CONFIG_EXTS.has(ext)) return <SlidersHorizontal className="w-5 h-5 text-slate-400 flex-shrink-0" />;
if (JSON_EXTS.has(ext)) return <Braces className="w-5 h-5 text-yellow-500 flex-shrink-0" />;
if (XML_EXTS.has(ext)) return <Code2 className="w-5 h-5 text-cyan-500 flex-shrink-0" />;
if (MD_EXTS.has(ext)) return <BookOpen className="w-5 h-5 text-sky-400 flex-shrink-0" />;
if (DOC_EXTS.has(ext)) return <FileType className="w-5 h-5 text-blue-400 flex-shrink-0" />;
if (CODE_EXTS.has(ext)) return <Terminal className="w-5 h-5 text-green-600 flex-shrink-0" />;
if (TEXT_EXTS.has(ext)) return <FileText className="w-5 h-5 text-green-600 flex-shrink-0" />;
return <File className="w-5 h-5 text-neutral-400 flex-shrink-0" />;
}
// ─── MediaEntry ───────────────────────────────────────────────────────────────
export interface MediaEntryProps {
entry: EntryInfo;
onPrimaryClick: () => void;
onActionsClick: (e: React.MouseEvent) => void;
/** Optional leading slot — e.g. a checkbox in MediaManager. */
leftSlot?: React.ReactNode;
/** Replaces the filename text — e.g. an inline rename input. */
nameSlot?: React.ReactNode;
selected?: boolean;
className?: string;
}
export function MediaEntry({
entry, onPrimaryClick, onActionsClick, leftSlot, nameSlot, selected, className,
}: MediaEntryProps) {
return (
<div className={`pl-[14px] pr-4 py-3 flex items-center gap-3 border-b border-neutral-100 border-l-2 border-l-transparent transition-colors hover:bg-blue-50 hover:border-l-blue-400 ${selected ? 'bg-blue-50 border-l-blue-400' : ''} ${className ?? ''}`}>
{leftSlot}
<button className="flex-1 flex items-center gap-3 text-left min-w-0" onClick={onPrimaryClick}>
<EntryIcon entry={entry} />
<div className="min-w-0 flex-1">
{nameSlot ?? <div className="text-neutral-900 truncate text-sm">{entry.name}</div>}
{entry.type === 'file' && (
<div className="text-xs text-neutral-400 truncate">
{humanFileSize(entry.size)}
{entry.lastModified ? ` · ${entry.lastModified.toLocaleDateString()}` : ''}
</div>
)}
</div>
{entry.type === 'folder' && <ChevronRight className="w-4 h-4 text-neutral-400 flex-shrink-0" />}
</button>
<button onClick={onActionsClick} className="p-2 rounded hover:bg-neutral-200 flex-shrink-0" title="Actions">
<MoreVertical className="w-4 h-4" />
</button>
</div>
);
}