FIX: Media Explorer panel hiển thị nội dung thư mục của máy client và thêm các link của các thư mục Favorited
This commit is contained in:
+182
-71
@@ -9075,6 +9075,24 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
try { return JSON.parse(localStorage.getItem('studio_media_explorer_favorites_v1') || '[]'); } catch (e) { return []; }
|
||||
}());
|
||||
const [favContext, setFavContext] = React.useState(null);
|
||||
const [favoritedExpanded, setFavoritedExpanded] = React.useState(true);
|
||||
const [colWidths, setColWidths] = React.useState({ file: 260, size: 100, type: 100 });
|
||||
const startColResize = (colKey, e) => {
|
||||
e.preventDefault();
|
||||
const startX = e.clientX;
|
||||
const startWidth = colWidths[colKey];
|
||||
const onMouseMove = (moveEvent) => {
|
||||
const deltaX = moveEvent.clientX - startX;
|
||||
const newWidth = Math.max(50, startWidth + deltaX);
|
||||
setColWidths(prev => ({ ...prev, [colKey]: newWidth }));
|
||||
};
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
const [synthInst, setSynthInst] = React.useState(function() {
|
||||
var saved = localStorage.getItem('studio_media_explorer_synth');
|
||||
return saved ? JSON.parse(saved) : null;
|
||||
@@ -9125,8 +9143,8 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
|
||||
// ── Session persistence: keep loaded folder/files/tree across panel toggles & reloads ──
|
||||
const SESSION_KEY = 'studio_media_explorer_session_v1';
|
||||
const saveClientRootHandle = () => {
|
||||
if (!clientRoot || !window.indexedDB) return;
|
||||
const saveClientRootHandle = (key = 'client_root', handle = clientRoot) => {
|
||||
if (!handle || !window.indexedDB) return;
|
||||
try {
|
||||
const req = indexedDB.open('sonicforge_media_explorer', 1);
|
||||
req.onupgradeneeded = (e) => {
|
||||
@@ -9136,11 +9154,11 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
req.onsuccess = () => {
|
||||
const db = req.result;
|
||||
const tx = db.transaction('root_handle', 'readwrite');
|
||||
tx.objectStore('root_handle').put(clientRoot, 'client_root');
|
||||
tx.objectStore('root_handle').put(handle, key);
|
||||
};
|
||||
} catch (e) {}
|
||||
};
|
||||
const loadClientRootHandle = () => {
|
||||
const loadClientRootHandle = (key = 'client_root') => {
|
||||
return new Promise((resolve) => {
|
||||
if (!window.indexedDB) { resolve(null); return; }
|
||||
try {
|
||||
@@ -9153,7 +9171,7 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
const db = req.result;
|
||||
try {
|
||||
const tx = db.transaction('root_handle', 'readonly');
|
||||
const g = tx.objectStore('root_handle').get('client_root');
|
||||
const g = tx.objectStore('root_handle').get(key);
|
||||
g.onsuccess = () => resolve(g.result || null);
|
||||
g.onerror = () => resolve(null);
|
||||
} catch (e2) { resolve(null); }
|
||||
@@ -9204,26 +9222,71 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
if (computerMode === 'client' && clientRoot && clientRoot.kind === 'directory') saveClientRootHandle();
|
||||
}, [saveSession, computerMode, clientRoot]);
|
||||
|
||||
const restoreSession = React.useCallback(() => {
|
||||
// Chỉ khôi phục favorites + các pref khác. KHÔNG khôi phục thư mục/tree đã mở:
|
||||
// My Computer luôn phải hiển thị cây hệ thống client.
|
||||
try {
|
||||
const raw = localStorage.getItem(SESSION_KEY);
|
||||
if (!raw) return;
|
||||
const snap = JSON.parse(raw);
|
||||
if (snap.folder) setFolder(snap.folder);
|
||||
if (snap.computerMode) setComputerMode(snap.computerMode);
|
||||
if (snap.clientRootName) setClientRoot({ name: snap.clientRootName });
|
||||
} catch (e) {}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
await restoreSession();
|
||||
// Chỉ nối lại FileSystemDirectoryHandle để client browsing hoạt động (không khôi phục folder cũ)
|
||||
let lastFolder = 'library';
|
||||
let lastComputerPath = 'favorited';
|
||||
try {
|
||||
const raw = localStorage.getItem(SESSION_KEY);
|
||||
if (raw) {
|
||||
const snap = JSON.parse(raw);
|
||||
if (snap.folder) lastFolder = snap.folder;
|
||||
if (snap.computerPath) lastComputerPath = snap.computerPath;
|
||||
setFolder(lastFolder);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 1. Khôi phục client-side root handle từ IndexedDB
|
||||
const savedHandle = await loadClientRootHandle();
|
||||
let restoredClientRoot = null;
|
||||
if (savedHandle && savedHandle.kind === 'directory') {
|
||||
setClientRoot(savedHandle);
|
||||
restoredClientRoot = savedHandle;
|
||||
setComputerMode('client');
|
||||
const rootEntry = { name: savedHandle.name, path: 'root', is_dir: true, handle: savedHandle };
|
||||
setComputerRoots([rootEntry]);
|
||||
// Quét đúng 1 cấp con dưới root của client
|
||||
await browseComputerDir(rootEntry);
|
||||
} else {
|
||||
setComputerRoots(null);
|
||||
setComputerMode('client');
|
||||
}
|
||||
|
||||
// 2. Khôi phục thư mục của phiên làm việc trước
|
||||
if (lastFolder === 'computer' && lastComputerPath && lastComputerPath !== 'my_computer' && lastComputerPath !== 'favorited') {
|
||||
if (restoredClientRoot) {
|
||||
if (lastComputerPath === 'root') {
|
||||
browseComputerDir({ name: restoredClientRoot.name, path: 'root', is_dir: true, handle: restoredClientRoot });
|
||||
} else if (lastComputerPath.startsWith('root/')) {
|
||||
const segments = lastComputerPath.split('/').slice(1);
|
||||
let curHandle = restoredClientRoot;
|
||||
let success = true;
|
||||
for (const seg of segments) {
|
||||
try {
|
||||
curHandle = await curHandle.getDirectoryHandle(seg);
|
||||
} catch (err) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
browseComputerDir({
|
||||
name: segments[segments.length - 1] || restoredClientRoot.name,
|
||||
path: lastComputerPath,
|
||||
is_dir: true,
|
||||
handle: curHandle
|
||||
});
|
||||
} else {
|
||||
setComputerPath('favorited');
|
||||
}
|
||||
} else {
|
||||
setComputerPath('favorited');
|
||||
}
|
||||
} else {
|
||||
setComputerPath('favorited');
|
||||
}
|
||||
} else {
|
||||
setComputerPath(lastComputerPath === 'my_computer' ? 'favorited' : (lastComputerPath || 'favorited'));
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -9244,11 +9307,14 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
const folderFiles = React.useMemo(() => {
|
||||
if (folder === 'library') return MEDIA_LIBRARY_SAMPLES;
|
||||
if (folder === 'computer') {
|
||||
if (computerPath === 'my_computer' || computerPath === 'favorited' || !computerPath) {
|
||||
return favorites || [];
|
||||
}
|
||||
const node = computerTree[computerPath] || { dirs: [] };
|
||||
return [...(node.dirs || []), ...computerFiles];
|
||||
}
|
||||
return userFiles.filter(f => (folder === 'uploads' ? (f.type || 'Upload') === 'Upload' : (f.type || 'Processed') === 'Processed'));
|
||||
}, [folder, userFiles, computerFiles, computerTree, computerPath]);
|
||||
}, [folder, userFiles, computerFiles, computerTree, computerPath, favorites]);
|
||||
|
||||
const visibleFiles = React.useMemo(() => {
|
||||
const q = filterText.toLowerCase().trim();
|
||||
@@ -9295,6 +9361,12 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
} catch (e) { setPeaks(null); }
|
||||
};
|
||||
|
||||
const openFavorited = () => {
|
||||
setFolder('computer');
|
||||
setComputerPath('favorited');
|
||||
setComputerFiles([]);
|
||||
};
|
||||
|
||||
const openMyComputer = async () => {
|
||||
setFolder('computer');
|
||||
const useClientRoot = async (rootHandle) => {
|
||||
@@ -9304,30 +9376,27 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
setComputerRoots([rootEntry]);
|
||||
setComputerPath('root');
|
||||
setComputerFiles([]);
|
||||
const browsed = await browseComputerDir(rootEntry);
|
||||
if (browsed && browsed.dirs) {
|
||||
browsed.dirs.slice(0, 10).forEach(d => browseComputerDir(d));
|
||||
}
|
||||
|
||||
// Lưu handle vào IndexedDB làm client_root và làm link Favorited
|
||||
const favKey = 'client:' + rootHandle.name;
|
||||
saveClientRootHandle('client_root', rootHandle);
|
||||
saveClientRootHandle(favKey, rootHandle);
|
||||
|
||||
// Tự động thêm link đến thư mục đó ở Favorited (không ghi đè các mục cũ)
|
||||
setFavorites(prev => {
|
||||
const exists = prev.some(f => f.path === favKey);
|
||||
if (exists) return prev;
|
||||
const next = [...prev, { path: favKey, name: rootHandle.name, is_dir: true }];
|
||||
try { localStorage.setItem('studio_media_explorer_favorites_v1', JSON.stringify(next)); } catch (e2) {}
|
||||
return next;
|
||||
});
|
||||
window.showToast && window.showToast('Đã thêm thư mục client vào Favorited: ' + rootHandle.name, 'info');
|
||||
|
||||
await browseComputerDir(rootEntry);
|
||||
return true;
|
||||
};
|
||||
// Ưu tiên client-side: root handle đã lưu (File System Access API) → tự động quét cây client
|
||||
let savedHandle = null;
|
||||
try { savedHandle = await loadClientRootHandle(); } catch (e) {}
|
||||
if (savedHandle && savedHandle.kind === 'directory') {
|
||||
// Yêu cầu lại quyền đọc (permission có thể bị thu hồi sau reload)
|
||||
let permitted = true;
|
||||
try {
|
||||
if (typeof savedHandle.queryPermission === 'function') {
|
||||
const st = await savedHandle.queryPermission({ mode: 'read' });
|
||||
if (st !== 'granted' && typeof savedHandle.requestPermission === 'function') {
|
||||
const r = await savedHandle.requestPermission({ mode: 'read' });
|
||||
permitted = r === 'granted';
|
||||
}
|
||||
}
|
||||
} catch (e) { permitted = false; }
|
||||
if (permitted) return useClientRoot(savedHandle);
|
||||
}
|
||||
// Chưa có quyền client → yêu cầu người dùng chọn thư mục để hiển thị cây phía client
|
||||
|
||||
// Luôn bắt buộc hiện Window Picker chọn thư mục của client-side khi click vào My Computer
|
||||
if (window.showDirectoryPicker) {
|
||||
try {
|
||||
const picked = await window.showDirectoryPicker({ mode: 'read' });
|
||||
@@ -9335,10 +9404,11 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
return useClientRoot(picked);
|
||||
}
|
||||
} catch (e) {
|
||||
// Người dùng hủy (AbortError) hoặc lỗi khác → rơi xuống quét server
|
||||
// User cancelled or error
|
||||
}
|
||||
}
|
||||
// Auto-scan ổ đĩa/thư mục hệ thống (server API = máy local)
|
||||
|
||||
// Fallback sang Server-side chỉ khi browser không hỗ trợ
|
||||
setComputerMode('server');
|
||||
setComputerRoots(null);
|
||||
try {
|
||||
@@ -9348,13 +9418,9 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
const roots = data.roots || [];
|
||||
setComputerRoots(roots.length ? roots : [{ path: '/', name: 'Root (/)', is_dir: true }]);
|
||||
if (!roots.length) window.showToast && window.showToast('Không tìm thấy ổ đĩa nào', 'warning');
|
||||
// Tự động quét: expand từng ổ đĩa để hiển thị cây thư mục
|
||||
const rootList = roots.length ? roots : [{ path: '/', name: 'Root (/)', is_dir: true }];
|
||||
rootList.slice(0, 10).forEach(async (root) => {
|
||||
const res = await browseComputerDir(root);
|
||||
if (res && res.dirs) {
|
||||
res.dirs.slice(0, 8).forEach(d => browseComputerDir(d));
|
||||
}
|
||||
await browseComputerDir(root);
|
||||
});
|
||||
} catch (e) {
|
||||
setComputerRoots([{ path: '/', name: 'Root (/)', is_dir: true }]);
|
||||
@@ -9449,7 +9515,7 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
try { localStorage.setItem('studio_media_explorer_favorites_v1', JSON.stringify(next)); } catch (e2) {}
|
||||
return next;
|
||||
});
|
||||
window.showToast && window.showToast('Đã ' + (isFavorite(entry) ? 'gỡ khỏi' : 'thêm vào') + ' Favorites: ' + name, 'info');
|
||||
window.showToast && window.showToast('Đã ' + (isFavorite(entry) ? 'gỡ khỏi' : 'thêm vào') + ' Favorited: ' + name, 'info');
|
||||
};
|
||||
const openFavorite = (fav) => {
|
||||
if (!fav) return;
|
||||
@@ -9459,6 +9525,22 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
const node = computerTree[fav.path];
|
||||
if (node && node.handle) {
|
||||
browseComputerDir({ ...entry, handle: node.handle });
|
||||
} else if (fav.path && fav.path.startsWith('client:')) {
|
||||
(async () => {
|
||||
let favHandle = null;
|
||||
try { favHandle = await loadClientRootHandle(fav.path); } catch (e) {}
|
||||
if (favHandle) {
|
||||
setClientRoot(favHandle);
|
||||
setComputerMode('client');
|
||||
const rootEntry = { name: favHandle.name, path: 'root', is_dir: true, handle: favHandle };
|
||||
setComputerRoots([rootEntry]);
|
||||
setComputerPath('root');
|
||||
setComputerFiles([]);
|
||||
await browseComputerDir(rootEntry);
|
||||
} else {
|
||||
window.showToast && window.showToast('Không thể khôi phục quyền truy cập thư mục này', 'error');
|
||||
}
|
||||
})();
|
||||
} else if (fav.path === 'root' && clientRoot) {
|
||||
browseComputerDir({ name: clientRoot.name, path: 'root', is_dir: true, handle: clientRoot });
|
||||
} else if (fav.path && fav.path.startsWith('root/') && clientRoot) {
|
||||
@@ -9948,14 +10030,21 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
<div className="space-y-0.5 font-sans">
|
||||
<div className="flex items-center gap-1 px-1 py-0.5 text-slate-700"><span className="w-3"></span> <Track Templates></div>
|
||||
<div className="flex items-center gap-1 px-1 py-0.5 text-slate-700"><span className="w-3"></span> <Project Directory></div>
|
||||
<div className={`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${folder === 'computer' ? 'bg-slate-300 text-slate-900 font-semibold' : 'hover:bg-slate-200 text-slate-800'}`} onClick={openMyComputer}>
|
||||
<div className={`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${folder === 'computer' && computerPath !== 'favorited' ? 'bg-slate-300 text-slate-900 font-semibold' : 'hover:bg-slate-200 text-slate-800'}`} onClick={openMyComputer}>
|
||||
<i className="fa-solid fa-computer text-[11px] text-slate-600"></i> My Computer
|
||||
</div>
|
||||
{favorites && favorites.length > 0 && (
|
||||
{folder === 'computer' && computerPath !== 'favorited' && computerRoots && (
|
||||
<div className="pl-3 space-y-0.5">
|
||||
{computerRoots.map(root => renderComputerNode(root, 0, true))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${folder === 'computer' && computerPath === 'favorited' ? 'bg-slate-300 text-slate-900 font-semibold' : 'hover:bg-slate-200 text-slate-800'}`} onClick={openFavorited}>
|
||||
<i className={`fa-solid ${favoritedExpanded ? 'fa-minus' : 'fa-plus'} text-[9px] text-slate-500 w-3 text-center shrink-0`} onClick={e => { e.stopPropagation(); setFavoritedExpanded(!favoritedExpanded); }}></i>
|
||||
<i className="fa-solid fa-star text-amber-500 text-[10px]"></i> Favorited
|
||||
</div>
|
||||
{favoritedExpanded && (
|
||||
<div className="pl-3 space-y-0.5">
|
||||
<div className="flex items-center gap-1 px-1 py-0.5 font-semibold text-amber-700 rounded-sm">
|
||||
<i className="fa-solid fa-star text-amber-500 text-[10px]"></i> Favorites
|
||||
</div>
|
||||
{favorites.map((fav, fi) => (
|
||||
<div key={fav.path + fi}
|
||||
className="flex items-center gap-1 px-1 py-0.5 cursor-pointer pl-4 rounded-sm hover:bg-amber-100 text-slate-800"
|
||||
@@ -9967,13 +10056,12 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
<i className="fa-solid fa-star text-amber-500 text-[9px] shrink-0"></i>
|
||||
</div>
|
||||
))}
|
||||
{favorites.length === 0 && (
|
||||
<div className="pl-4 py-0.5 text-slate-400 italic text-[11px]">No favorites</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{folder === 'computer' && computerRoots && (
|
||||
<div className="pl-3 space-y-0.5">
|
||||
{computerRoots.map(root => renderComputerNode(root, 0, true))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`flex items-center gap-1 px-1 py-0.5 cursor-pointer font-semibold rounded-sm ${folder === 'library' ? 'bg-slate-300 text-slate-900' : 'hover:bg-slate-200 text-slate-800'}`} onClick={() => setFolder('library')}>
|
||||
<i className="fa-solid fa-folder-open text-[#d9a752]"></i> Media Library
|
||||
</div>
|
||||
@@ -9985,8 +10073,6 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
<i className="fa-solid fa-folder text-[#d9a752]"></i> Processed
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 px-1 py-0.5 text-slate-700"><i className="fa-solid fa-plus text-[9px] text-slate-500"></i> Desktop</div>
|
||||
<div className="flex items-center gap-1 px-1 py-0.5 text-slate-700"><i className="fa-solid fa-plus text-[9px] text-slate-500"></i> My Documents</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1.5 cursor-ew-resize hover:bg-blue-500/40 active:bg-blue-500/60 transition-colors shrink-0"
|
||||
@@ -10002,7 +10088,7 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
<div className={`px-2 py-1 cursor-pointer hover:bg-blue-100 flex items-center gap-1.5 ${isFavorite(favContext) ? 'text-amber-700' : ''}`}
|
||||
onClick={() => { toggleFavorite(favContext); setFavContext(null); }}>
|
||||
<i className={`fa-solid ${isFavorite(favContext) ? 'fa-star text-amber-500' : 'fa-star text-slate-400'} text-[11px]`}></i>
|
||||
{isFavorite(favContext) ? 'Gỡ khỏi Favorites' : 'Thêm vào Favorites'}
|
||||
{isFavorite(favContext) ? 'Gỡ khỏi Favorited' : 'Thêm vào Favorited'}
|
||||
</div>
|
||||
<div className="px-2 py-1 cursor-pointer hover:bg-blue-100 flex items-center gap-1.5" onClick={() => { if (favContext) browseComputerDir(favContext); setFavContext(null); }}>
|
||||
<i className="fa-solid fa-folder-open text-[#d9a752] text-[11px]"></i> Mở thư mục
|
||||
@@ -10012,11 +10098,28 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
|
||||
{/* FILE LIST */}
|
||||
<div className="flex-1 bg-white border border-[#808080] m-1 overflow-y-auto relative">
|
||||
<table className="w-full text-xs text-left border-collapse">
|
||||
<table className="w-full text-xs text-left border-collapse" style={{ tableLayout: 'fixed' }}>
|
||||
<thead className="sticky top-0 bg-[#e0e0e0] border-b border-[#a0a0a0] text-slate-800 font-semibold select-none shadow-sm z-10">
|
||||
<tr>
|
||||
<th className="py-1 px-2 border-r border-[#b0b0b0]">File</th>
|
||||
{viewMode === 'details' && <th className="py-1 px-2 border-r border-[#b0b0b0]">Size</th>}
|
||||
<th className="py-1 px-2 border-r border-[#b0b0b0] relative" style={viewMode === 'details' ? { width: colWidths.file } : undefined}>
|
||||
<div className="truncate pr-2">File</div>
|
||||
<div className="absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-slate-400"
|
||||
onMouseDown={e => startColResize('file', e)}></div>
|
||||
</th>
|
||||
{viewMode === 'details' && (
|
||||
<>
|
||||
<th className="py-1 px-2 border-r border-[#b0b0b0] relative" style={{ width: colWidths.size }}>
|
||||
<div className="truncate pr-2">Size</div>
|
||||
<div className="absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-slate-400"
|
||||
onMouseDown={e => startColResize('size', e)}></div>
|
||||
</th>
|
||||
<th className="py-1 px-2 border-r border-[#b0b0b0] relative" style={{ width: colWidths.type }}>
|
||||
<div className="truncate pr-2">Type</div>
|
||||
<div className="absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-slate-400"
|
||||
onMouseDown={e => startColResize('type', e)}></div>
|
||||
</th>
|
||||
</>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="font-sans text-slate-800">
|
||||
@@ -10036,14 +10139,22 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
onDragEnd={() => { window.__mediaExplorerDragFile = null; }}
|
||||
className={`cursor-pointer hover:bg-blue-100 ${isSel ? 'file-row-selected' : ''}`}
|
||||
onClick={() => f.is_dir ? browseComputerDir(f) : handleSelect(f)}
|
||||
onDoubleClick={() => f.is_dir && browseComputerDir(f)}>
|
||||
<td className="py-1 px-2"><i className={`fa-solid ${icon} mr-2`}></i>{f.name || f.original_name}</td>
|
||||
{viewMode === 'details' && <td className="py-1 px-2">{f.size_mb != null ? f.size_mb.toFixed(2) + ' MB' : (isMidi ? (f.tpqn || 'MIDI') + ' TPQN' : '-')}</td>}
|
||||
onDoubleClick={() => f.is_dir && browseComputerDir(f)}
|
||||
onContextMenu={e => {
|
||||
if (f.is_dir) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setFavContext(Object.assign({}, f, { x: e.clientX, y: e.clientY }));
|
||||
}
|
||||
}}>
|
||||
<td className="py-1 px-2 truncate"><i className={`fa-solid ${icon} mr-2`}></i>{f.name || f.original_name}</td>
|
||||
{viewMode === 'details' && <td className="py-1 px-2 truncate">{f.size_mb != null ? f.size_mb.toFixed(2) + ' MB' : (isMidi ? (f.tpqn || 'MIDI') + ' TPQN' : '-')}</td>}
|
||||
{viewMode === 'details' && <td className="py-1 px-2 truncate">{f.is_dir ? 'Folder' : (isMidi ? 'MIDI' : (f.kind === 'audio' ? 'Audio' : 'File'))}</td>}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{visibleFiles.length === 0 && (
|
||||
<tr><td className="py-3 px-2 text-slate-400 italic" colSpan={viewMode === 'details' ? 2 : 1}>No files</td></tr>
|
||||
<tr><td className="py-3 px-2 text-slate-400 italic" colSpan={viewMode === 'details' ? 3 : 1}>No files</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user