feat: My Computer auto-load cây + Favorites thư mục
This commit is contained in:
+110
-13
@@ -9071,6 +9071,10 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
const [computerFiles, setComputerFiles] = React.useState([]);
|
||||
const [computerMode, setComputerMode] = React.useState('server');
|
||||
const [clientRoot, setClientRoot] = React.useState(null);
|
||||
const [favorites, setFavorites] = React.useState(function() {
|
||||
try { return JSON.parse(localStorage.getItem('studio_media_explorer_favorites_v1') || '[]'); } catch (e) { return []; }
|
||||
}());
|
||||
const [favContext, setFavContext] = React.useState(null);
|
||||
const [synthInst, setSynthInst] = React.useState(function() {
|
||||
var saved = localStorage.getItem('studio_media_explorer_synth');
|
||||
return saved ? JSON.parse(saved) : null;
|
||||
@@ -9359,7 +9363,11 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
setComputerPath('root');
|
||||
setComputerFiles([]);
|
||||
setComputerTree({ root: { handle, parent: null, dirs: [], expanded: true } });
|
||||
await browseComputerDir(rootEntry);
|
||||
const browsed = await browseComputerDir(rootEntry);
|
||||
// Tự động load cấu trúc: expand từng thư mục con cấp 1 để hiển thị cây
|
||||
if (browsed && browsed.dirs) {
|
||||
browsed.dirs.slice(0, 10).forEach(d => browseComputerDir(d));
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
if (e && e.name === 'AbortError') return;
|
||||
@@ -9386,7 +9394,7 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
|
||||
const browseClientDir = async (entry) => {
|
||||
const handle = entry && entry.handle;
|
||||
if (!handle || !handle.entries) return;
|
||||
if (!handle || !handle.entries) return null;
|
||||
const dirs = [];
|
||||
const files = [];
|
||||
const parentPath = entry.path;
|
||||
@@ -9411,22 +9419,23 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
setCurrentTime(0);
|
||||
stopMediaPlayback();
|
||||
setComputerTree(prev => ({ ...prev, [parentPath]: { handle, parent: entry.parent || null, dirs, expanded: true } }));
|
||||
return { dirs, files };
|
||||
} catch (e) {
|
||||
window.showToast && window.showToast('Không thể đọc thư mục: ' + e.message, 'error');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const browseComputerDir = async (entry) => {
|
||||
if (!entry) return;
|
||||
if (!entry) return null;
|
||||
if (computerMode === 'client' || entry.handle) {
|
||||
if (entry.handle && entry.handle.entries) {
|
||||
const parentInfo = computerTree[entry.path];
|
||||
await browseClientDir({ ...entry, parent: parentInfo ? parentInfo.parent : null });
|
||||
return;
|
||||
return await browseClientDir({ ...entry, parent: parentInfo ? parentInfo.parent : null });
|
||||
}
|
||||
}
|
||||
const path = entry.path || entry;
|
||||
if (!path) return;
|
||||
if (!path) return null;
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE_URL}/api/v1/media/browse?path=${encodeURIComponent(path)}`);
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
@@ -9437,8 +9446,10 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
setCurrentTime(0);
|
||||
stopMediaPlayback();
|
||||
setComputerTree(prev => ({ ...prev, [path]: { dirs: data.dirs || [], expanded: true } }));
|
||||
return { dirs: data.dirs || [], files: data.files || [] };
|
||||
} catch (e) {
|
||||
window.showToast && window.showToast('Không thể mở thư mục: ' + e.message, 'error');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9446,15 +9457,64 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
if (!entry) return;
|
||||
const path = entry.path || entry;
|
||||
const node = computerTree[path];
|
||||
if (node && node.expanded) {
|
||||
setComputerTree(prev => ({ ...prev, [path]: { ...prev[path], expanded: false } }));
|
||||
if (node && node.expanded) { setComputerTree(prev => ({ ...prev, [path]: { ...prev[path], expanded: false } }));
|
||||
} else {
|
||||
await browseComputerDir(entry);
|
||||
}
|
||||
};
|
||||
|
||||
const goComputerParent = () => {
|
||||
if (!computerPath) return;
|
||||
// ── Favorites: thư mục yêu thích ──
|
||||
const isFavorite = (entry) => {
|
||||
if (!entry) return false;
|
||||
return (favorites || []).some(f => f.path === (entry.path || entry));
|
||||
};
|
||||
const toggleFavorite = (entry, e) => {
|
||||
if (e && e.stopPropagation) e.stopPropagation();
|
||||
if (!entry) return;
|
||||
const path = entry.path || entry;
|
||||
const name = entry.name || path.split('/').pop() || path;
|
||||
setFavorites(prev => {
|
||||
const exists = prev.some(f => f.path === path);
|
||||
const next = exists ? prev.filter(f => f.path !== path) : [...prev, { path, name, is_dir: true }];
|
||||
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');
|
||||
};
|
||||
const openFavorite = (fav) => {
|
||||
if (!fav) return;
|
||||
setFolder('computer');
|
||||
const entry = { path: fav.path, name: fav.name, is_dir: true };
|
||||
// Cố gắng tìm handle trong cây đã load để mở trực tiếp
|
||||
const node = computerTree[fav.path];
|
||||
if (node && node.handle) {
|
||||
browseComputerDir({ ...entry, handle: node.handle });
|
||||
} 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) {
|
||||
// Duyệt lại từ root handle tới folder favorite
|
||||
const segments = fav.path.split('/').slice(1);
|
||||
let curHandle = clientRoot;
|
||||
const walk = async (idx) => {
|
||||
if (idx >= segments.length) {
|
||||
browseComputerDir({ name: segments[idx - 1] || clientRoot.name, path: fav.path, is_dir: true, handle: curHandle });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const child = await curHandle.getDirectoryHandle(segments[idx]);
|
||||
curHandle = child;
|
||||
walk(idx + 1);
|
||||
} catch (e) {
|
||||
window.showToast && window.showToast('Không mở được thư mục favorite', 'error');
|
||||
}
|
||||
};
|
||||
walk(0);
|
||||
} else if (fav.path) {
|
||||
browseComputerDir(entry);
|
||||
}
|
||||
};
|
||||
|
||||
const goComputerParent = () => { if (!computerPath) return;
|
||||
if (computerMode === 'client' || clientRoot) {
|
||||
const node = computerTree[computerPath];
|
||||
const parentHandle = node && node.parent;
|
||||
@@ -9831,15 +9891,18 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
const expanded = node && node.expanded;
|
||||
const dirs = node ? node.dirs : [];
|
||||
const pad = 12 + depth * 12;
|
||||
const fav = isFavorite(entry);
|
||||
return (
|
||||
<React.Fragment key={nodePath}>
|
||||
<div className={`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${computerPath === nodePath ? 'bg-slate-300 text-slate-900' : 'hover:bg-slate-200 text-slate-800'}`}
|
||||
style={{ paddingLeft: pad }}
|
||||
onClick={() => browseComputerDir(entry)}
|
||||
onDoubleClick={e => { e.stopPropagation(); toggleComputerDir(entry); }}>
|
||||
onDoubleClick={e => { e.stopPropagation(); toggleComputerDir(entry); }}
|
||||
onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setFavContext(Object.assign({}, entry, { x: e.clientX, y: e.clientY })); }}>
|
||||
<i className={`fa-solid ${expanded ? 'fa-minus' : 'fa-plus'} text-[9px] text-slate-500 w-3 text-center shrink-0`} onClick={e => { e.stopPropagation(); toggleComputerDir(entry); }}></i>
|
||||
<i className={`fa-solid ${isRoot ? 'fa-hard-drive text-[#6ea8dc]' : 'fa-folder text-[#d9a752]'} shrink-0`}></i>
|
||||
<span className="truncate">{entry.name}</span>
|
||||
<span className="truncate flex-1">{entry.name}</span>
|
||||
{fav && <i className="fa-solid fa-star text-amber-500 text-[9px] shrink-0"></i>}
|
||||
</div>
|
||||
{expanded && dirs.map(d => renderComputerNode(d, depth + 1, false))}
|
||||
</React.Fragment>
|
||||
@@ -9906,7 +9969,7 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
</div>
|
||||
|
||||
{/* 2. MIDDLE SPLIT VIEW */}
|
||||
<div className="flex-1 flex overflow-hidden min-h-0">
|
||||
<div className="flex-1 flex overflow-hidden min-h-0 relative">
|
||||
{/* DIRECTORY TREE */}
|
||||
<div className="flex bg-white border border-[#808080] m-1 mr-0 text-xs select-none shrink-0 overflow-hidden">
|
||||
<div className="overflow-y-auto p-1" style={{ width: treeWidth + 'px', minWidth: treeWidth + 'px', maxWidth: treeWidth + 'px' }}>
|
||||
@@ -9916,6 +9979,24 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
<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}>
|
||||
<i className="fa-solid fa-computer text-[11px] text-slate-600"></i> My Computer
|
||||
</div>
|
||||
{favorites && favorites.length > 0 && (
|
||||
<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"
|
||||
style={{ paddingLeft: 24 }}
|
||||
onClick={() => openFavorite(fav)}
|
||||
onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setFavContext(Object.assign({}, fav, { x: e.clientX, y: e.clientY })); }}>
|
||||
<i className="fa-solid fa-folder text-[#d9a752] shrink-0"></i>
|
||||
<span className="truncate flex-1">{fav.name}</span>
|
||||
<i className="fa-solid fa-star text-amber-500 text-[9px] shrink-0"></i>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{folder === 'computer' && computerRoots && (
|
||||
<div className="pl-3 space-y-0.5">
|
||||
{computerRoots.map(root => renderComputerNode(root, 0, true))}
|
||||
@@ -9941,6 +10022,22 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
title="Kéo để thay đổi chiều rộng"></div>
|
||||
</div>
|
||||
|
||||
{/* CONTEXT MENU (right-click folder → Favorite) */}
|
||||
{favContext && (
|
||||
<div className="fixed z-[100] bg-white border border-[#808080] shadow-lg rounded-sm text-xs font-sans text-slate-800 min-w-[180px]"
|
||||
style={{ left: favContext.x, top: favContext.y }}
|
||||
onMouseLeave={() => setFavContext(null)}>
|
||||
<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'}
|
||||
</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
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
|
||||
Reference in New Issue
Block a user