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:
2026-08-02 20:31:57 +07:00
parent d04631c7d3
commit 827693dfc4
2 changed files with 196 additions and 86 deletions
+182 -71
View File
@@ -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 phc favorites + các pref khác. KHÔNG khôi phc thư mc/tree đã m:
// My Computer luôn phi hin th cây h thng 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 ni li FileSystemDirectoryHandle đ client browsing hot đng (không khôi phc 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 phc 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 cp con dưi root ca client
await browseComputerDir(rootEntry);
} else {
setComputerRoots(null);
setComputerMode('client');
}
// 2. Khôi phc thư mc ca phiên làm vic 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ư mc đó Favorited (không ghi đè các mc 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 cu li quyn đc (permission có th b thu hi 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ó quyn client yêu cu ngưi dùng chn thư mc đ hin th cây phía client
// Luôn bt buc hin Window Picker chn thư mc ca 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 hy (AbortError) hoc li khác rơi xung quét server
// User cancelled or error
}
}
// Auto-scan đĩa/thư mc h thng (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 tng đĩa đ hin th cây thư mc
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> &lt;Track Templates&gt;</div>
<div className="flex items-center gap-1 px-1 py-0.5 text-slate-700"><span className="w-3"></span> &lt;Project Directory&gt;</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>