Compare commits
3 Commits
afc15134f5
...
c7048678b2
| Author | SHA1 | Date | |
|---|---|---|---|
| c7048678b2 | |||
| c287de5bad | |||
| 3d69970ea5 |
|
|
@ -6,7 +6,8 @@ import {
|
|||
BookOpen,
|
||||
Braces,
|
||||
Check,
|
||||
CheckSquare,
|
||||
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ClipboardPaste,
|
||||
|
|
@ -207,9 +208,7 @@ interface FolderManagementActions {
|
|||
onUpload: () => void;
|
||||
clipboard: Clipboard | null;
|
||||
onPaste: () => void;
|
||||
selectedCount: number;
|
||||
totalCount: number;
|
||||
onSelectAll: () => void;
|
||||
|
||||
isRoot: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -278,11 +277,6 @@ function ActionsModal({ entry, onClose, onOpen, onMount, onDownload, onDuplicate
|
|||
<span>{fm.clipboard.op === 'copy' ? 'Copy' : 'Move'} {fm.clipboard.paths.length} item{fm.clipboard.paths.length !== 1 ? 's' : ''} here</span>
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => { onClose(); fm.onSelectAll(); }}
|
||||
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
|
||||
<CheckSquare className="w-4 h-4 text-neutral-500" />
|
||||
<span>{fm.selectedCount === fm.totalCount && fm.totalCount > 0 ? 'Deselect All' : 'Select All'}</span>
|
||||
</button>
|
||||
{!fm.isRoot && <div className="border-t border-neutral-100" />}
|
||||
</>
|
||||
)}
|
||||
|
|
@ -435,6 +429,7 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
|
|||
const [actionEntry, setActionEntry] = useState<EntryInfo | null>(null);
|
||||
const [folderActionOpen, setFolderActionOpen] = useState(false);
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
const [showNewFile, setShowNewFile] = useState(false);
|
||||
const [newFileName, setNewFileName] = useState('');
|
||||
|
||||
|
|
@ -463,12 +458,28 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
|
|||
|
||||
// ── Directory loading ────────────────────────────────────────────────────
|
||||
|
||||
const [folderConfig, setFolderConfig] = useState<Record<string, string> | null>(null);
|
||||
|
||||
const load = useCallback(async (p: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSelected(new Set());
|
||||
setFolderConfig(null);
|
||||
try {
|
||||
setEntries(await listDirectory(p));
|
||||
const entries = await listDirectory(p);
|
||||
setEntries(entries);
|
||||
try {
|
||||
const blob = await getFileContents(joinPath(p, '.config'));
|
||||
const cfg: Record<string, string> = {};
|
||||
for (const line of (await blob.text()).split('\n')) {
|
||||
const t = line.trim();
|
||||
if (!t || t.startsWith('#')) continue;
|
||||
const eq = t.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
cfg[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
|
||||
}
|
||||
setFolderConfig(cfg);
|
||||
} catch { /* no .config or unreadable — folderConfig stays null */ }
|
||||
} catch (e: any) {
|
||||
setError(e?.message ?? 'Failed to load directory');
|
||||
setEntries([]);
|
||||
|
|
@ -486,30 +497,6 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
|
|||
useEffect(() => { localStorage.setItem('fileManager.sortKey', sortKey); }, [sortKey]);
|
||||
useEffect(() => { localStorage.setItem('fileManager.sortAsc', String(sortAsc)); }, [sortAsc]);
|
||||
|
||||
// ── Folder config (.config) ──────────────────────────────────────────────
|
||||
|
||||
const [folderConfig, setFolderConfig] = useState<Record<string, string> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setFolderConfig(null);
|
||||
getFileContents(joinPath(path, '.config'))
|
||||
.then(async blob => {
|
||||
if (cancelled) return;
|
||||
const cfg: Record<string, string> = {};
|
||||
for (const line of (await blob.text()).split('\n')) {
|
||||
const t = line.trim();
|
||||
if (!t || t.startsWith('#')) continue;
|
||||
const eq = t.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
cfg[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
|
||||
}
|
||||
setFolderConfig(cfg);
|
||||
})
|
||||
.catch(() => { if (!cancelled) setFolderConfig(null); });
|
||||
return () => { cancelled = true; };
|
||||
}, [path]);
|
||||
|
||||
const navigateTo = (p: string) => {
|
||||
let norm = normalizePath(p);
|
||||
if (rootPath && !norm.startsWith(rootPath)) norm = rootPath;
|
||||
|
|
@ -992,15 +979,6 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
|
|||
<button onClick={() => void load(path)} className="p-1.5 rounded hover:bg-neutral-100" title="Refresh">
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => { setShowNewFile(v => !v); setShowNewFolder(false); }} className="p-1.5 rounded hover:bg-neutral-100" title="New File">
|
||||
<FilePlus className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => { setShowNewFolder(v => !v); setShowNewFile(false); }} className="p-1.5 rounded hover:bg-neutral-100" title="New Folder">
|
||||
<FolderPlus className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => fileInputRef.current?.click()} className="p-1.5 rounded hover:bg-neutral-100" title="Upload">
|
||||
<Upload className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => setFolderActionOpen(true)} className="p-1.5 rounded hover:bg-neutral-100" title="Actions">
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</button>
|
||||
|
|
@ -1050,6 +1028,13 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
|
|||
<SlidersHorizontal className="w-3.5 h-3.5" strokeWidth={3} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowFilter(v => !v)}
|
||||
className={`p-1 rounded flex-shrink-0 ${showFilter ? 'text-blue-600' : 'text-neutral-400 hover:text-neutral-600'}`}
|
||||
title={showFilter ? 'Hide filter' : 'Filter & sort'}
|
||||
>
|
||||
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-150 ${showFilter ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
{(folderConfig?.['base_url']) && (
|
||||
<div className="text-xs text-neutral-400 mt-0.5 truncate">
|
||||
|
|
@ -1115,10 +1100,11 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* ── Filter + sort bar ── */}
|
||||
<div className="bg-neutral-50 border-b border-neutral-200 px-4 py-2 flex items-center gap-2 flex-shrink-0">
|
||||
{showFilter && <div className="bg-neutral-50 border-b border-neutral-200 px-4 py-2 flex items-center gap-2 flex-shrink-0">
|
||||
<div className="relative flex-1 min-w-0">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-neutral-400 pointer-events-none" />
|
||||
<input
|
||||
|
|
@ -1143,7 +1129,7 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
|
|||
{sortKey === k ? (sortAsc ? ' ↑' : ' ↓') : ''}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{/* ── Selection / clipboard bar ── */}
|
||||
{(selCount > 0 || clipboard) && (
|
||||
|
|
@ -1316,9 +1302,7 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
|
|||
onUpload: () => fileInputRef.current?.click(),
|
||||
clipboard,
|
||||
onPaste: () => void paste(),
|
||||
selectedCount: selected.size,
|
||||
totalCount: visible.length,
|
||||
onSelectAll: selectAll,
|
||||
|
||||
isRoot: path === (rootPath ?? '/'),
|
||||
} : undefined}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ export default function StatusPage({ config, setConfig, onOpenFileManager }: Sta
|
|||
// Mock image association (replace with real logic if available)
|
||||
const imageUrl = lastFile.endsWith('.d64') ? '/assets/floppy.png' : undefined;
|
||||
|
||||
const [useShiftFont, setUseShiftFont] = useState(false);
|
||||
|
||||
// Dialog/modal state for reset actions
|
||||
const [showResetModal, setShowResetModal] = useState<null | 'meatloaf' | 'host'>(null);
|
||||
const [resetStatus, setResetStatus] = useState('idle'); // 'idle' | 'in-progress' | 'done'
|
||||
|
|
@ -210,21 +212,19 @@ export default function StatusPage({ config, setConfig, onOpenFileManager }: Sta
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* New device info cards */}
|
||||
<div className="mb-4">
|
||||
<div className="bg-neutral-50 rounded-lg p-3 flex flex-col items-start justify-center w-full mb-2">
|
||||
<div className="text-xs text-neutral-500 mb-1">Last File Access</div>
|
||||
<div className="text-sm font-medium break-all w-full text-left">{lastFile}</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between gap-4 w-full">
|
||||
<div className="bg-neutral-50 rounded-lg p-3 flex-1 flex flex-col items-start justify-center">
|
||||
<div className="text-xs text-neutral-500 mb-1">Size</div>
|
||||
<div className="text-sm font-medium">{fileSize}</div>
|
||||
</div>
|
||||
<div className="bg-neutral-50 rounded-lg p-3 flex-1 flex flex-col items-end justify-center">
|
||||
<div className="text-xs text-neutral-500 mb-1">Transfer Speed</div>
|
||||
<div className="text-sm font-medium">{transferSpeed}</div>
|
||||
</div>
|
||||
{/* Last File Access + Size + Transfer Speed */}
|
||||
<div className="mb-4 bg-neutral-50 rounded-lg p-3">
|
||||
<div className="text-xs text-neutral-500 mb-1">Last File Access</div>
|
||||
<button
|
||||
onClick={() => setUseShiftFont(v => !v)}
|
||||
className="w-full text-left break-all text-xs font-medium mb-2 leading-snug"
|
||||
style={{ fontFamily: useShiftFont ? "'CbmShift'" : "'C64_Pro_Mono'" }}
|
||||
>
|
||||
{lastFile}
|
||||
</button>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-neutral-500">{fileSize}</span>
|
||||
<span className="text-xs text-neutral-500">{transferSpeed}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user