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 []; }
|
try { return JSON.parse(localStorage.getItem('studio_media_explorer_favorites_v1') || '[]'); } catch (e) { return []; }
|
||||||
}());
|
}());
|
||||||
const [favContext, setFavContext] = React.useState(null);
|
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() {
|
const [synthInst, setSynthInst] = React.useState(function() {
|
||||||
var saved = localStorage.getItem('studio_media_explorer_synth');
|
var saved = localStorage.getItem('studio_media_explorer_synth');
|
||||||
return saved ? JSON.parse(saved) : null;
|
return saved ? JSON.parse(saved) : null;
|
||||||
@@ -9125,8 +9143,8 @@ const MediaExplorerPanel = ({ height }) => {
|
|||||||
|
|
||||||
// ── Session persistence: keep loaded folder/files/tree across panel toggles & reloads ──
|
// ── Session persistence: keep loaded folder/files/tree across panel toggles & reloads ──
|
||||||
const SESSION_KEY = 'studio_media_explorer_session_v1';
|
const SESSION_KEY = 'studio_media_explorer_session_v1';
|
||||||
const saveClientRootHandle = () => {
|
const saveClientRootHandle = (key = 'client_root', handle = clientRoot) => {
|
||||||
if (!clientRoot || !window.indexedDB) return;
|
if (!handle || !window.indexedDB) return;
|
||||||
try {
|
try {
|
||||||
const req = indexedDB.open('sonicforge_media_explorer', 1);
|
const req = indexedDB.open('sonicforge_media_explorer', 1);
|
||||||
req.onupgradeneeded = (e) => {
|
req.onupgradeneeded = (e) => {
|
||||||
@@ -9136,11 +9154,11 @@ const MediaExplorerPanel = ({ height }) => {
|
|||||||
req.onsuccess = () => {
|
req.onsuccess = () => {
|
||||||
const db = req.result;
|
const db = req.result;
|
||||||
const tx = db.transaction('root_handle', 'readwrite');
|
const tx = db.transaction('root_handle', 'readwrite');
|
||||||
tx.objectStore('root_handle').put(clientRoot, 'client_root');
|
tx.objectStore('root_handle').put(handle, key);
|
||||||
};
|
};
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
};
|
};
|
||||||
const loadClientRootHandle = () => {
|
const loadClientRootHandle = (key = 'client_root') => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!window.indexedDB) { resolve(null); return; }
|
if (!window.indexedDB) { resolve(null); return; }
|
||||||
try {
|
try {
|
||||||
@@ -9153,7 +9171,7 @@ const MediaExplorerPanel = ({ height }) => {
|
|||||||
const db = req.result;
|
const db = req.result;
|
||||||
try {
|
try {
|
||||||
const tx = db.transaction('root_handle', 'readonly');
|
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.onsuccess = () => resolve(g.result || null);
|
||||||
g.onerror = () => resolve(null);
|
g.onerror = () => resolve(null);
|
||||||
} catch (e2) { resolve(null); }
|
} catch (e2) { resolve(null); }
|
||||||
@@ -9204,26 +9222,71 @@ const MediaExplorerPanel = ({ height }) => {
|
|||||||
if (computerMode === 'client' && clientRoot && clientRoot.kind === 'directory') saveClientRootHandle();
|
if (computerMode === 'client' && clientRoot && clientRoot.kind === 'directory') saveClientRootHandle();
|
||||||
}, [saveSession, computerMode, clientRoot]);
|
}, [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(() => {
|
React.useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
await restoreSession();
|
let lastFolder = 'library';
|
||||||
// Chỉ nối lại FileSystemDirectoryHandle để client browsing hoạt động (không khôi phục folder cũ)
|
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();
|
const savedHandle = await loadClientRootHandle();
|
||||||
|
let restoredClientRoot = null;
|
||||||
if (savedHandle && savedHandle.kind === 'directory') {
|
if (savedHandle && savedHandle.kind === 'directory') {
|
||||||
setClientRoot(savedHandle);
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -9244,11 +9307,14 @@ const MediaExplorerPanel = ({ height }) => {
|
|||||||
const folderFiles = React.useMemo(() => {
|
const folderFiles = React.useMemo(() => {
|
||||||
if (folder === 'library') return MEDIA_LIBRARY_SAMPLES;
|
if (folder === 'library') return MEDIA_LIBRARY_SAMPLES;
|
||||||
if (folder === 'computer') {
|
if (folder === 'computer') {
|
||||||
|
if (computerPath === 'my_computer' || computerPath === 'favorited' || !computerPath) {
|
||||||
|
return favorites || [];
|
||||||
|
}
|
||||||
const node = computerTree[computerPath] || { dirs: [] };
|
const node = computerTree[computerPath] || { dirs: [] };
|
||||||
return [...(node.dirs || []), ...computerFiles];
|
return [...(node.dirs || []), ...computerFiles];
|
||||||
}
|
}
|
||||||
return userFiles.filter(f => (folder === 'uploads' ? (f.type || 'Upload') === 'Upload' : (f.type || 'Processed') === 'Processed'));
|
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 visibleFiles = React.useMemo(() => {
|
||||||
const q = filterText.toLowerCase().trim();
|
const q = filterText.toLowerCase().trim();
|
||||||
@@ -9295,6 +9361,12 @@ const MediaExplorerPanel = ({ height }) => {
|
|||||||
} catch (e) { setPeaks(null); }
|
} catch (e) { setPeaks(null); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openFavorited = () => {
|
||||||
|
setFolder('computer');
|
||||||
|
setComputerPath('favorited');
|
||||||
|
setComputerFiles([]);
|
||||||
|
};
|
||||||
|
|
||||||
const openMyComputer = async () => {
|
const openMyComputer = async () => {
|
||||||
setFolder('computer');
|
setFolder('computer');
|
||||||
const useClientRoot = async (rootHandle) => {
|
const useClientRoot = async (rootHandle) => {
|
||||||
@@ -9304,30 +9376,27 @@ const MediaExplorerPanel = ({ height }) => {
|
|||||||
setComputerRoots([rootEntry]);
|
setComputerRoots([rootEntry]);
|
||||||
setComputerPath('root');
|
setComputerPath('root');
|
||||||
setComputerFiles([]);
|
setComputerFiles([]);
|
||||||
const browsed = await browseComputerDir(rootEntry);
|
|
||||||
if (browsed && browsed.dirs) {
|
// Lưu handle vào IndexedDB làm client_root và làm link Favorited
|
||||||
browsed.dirs.slice(0, 10).forEach(d => browseComputerDir(d));
|
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;
|
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;
|
// 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
|
||||||
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
|
|
||||||
if (window.showDirectoryPicker) {
|
if (window.showDirectoryPicker) {
|
||||||
try {
|
try {
|
||||||
const picked = await window.showDirectoryPicker({ mode: 'read' });
|
const picked = await window.showDirectoryPicker({ mode: 'read' });
|
||||||
@@ -9335,10 +9404,11 @@ const MediaExplorerPanel = ({ height }) => {
|
|||||||
return useClientRoot(picked);
|
return useClientRoot(picked);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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');
|
setComputerMode('server');
|
||||||
setComputerRoots(null);
|
setComputerRoots(null);
|
||||||
try {
|
try {
|
||||||
@@ -9348,13 +9418,9 @@ const MediaExplorerPanel = ({ height }) => {
|
|||||||
const roots = data.roots || [];
|
const roots = data.roots || [];
|
||||||
setComputerRoots(roots.length ? roots : [{ path: '/', name: 'Root (/)', is_dir: true }]);
|
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');
|
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 }];
|
const rootList = roots.length ? roots : [{ path: '/', name: 'Root (/)', is_dir: true }];
|
||||||
rootList.slice(0, 10).forEach(async (root) => {
|
rootList.slice(0, 10).forEach(async (root) => {
|
||||||
const res = await browseComputerDir(root);
|
await browseComputerDir(root);
|
||||||
if (res && res.dirs) {
|
|
||||||
res.dirs.slice(0, 8).forEach(d => browseComputerDir(d));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setComputerRoots([{ path: '/', name: 'Root (/)', is_dir: true }]);
|
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) {}
|
try { localStorage.setItem('studio_media_explorer_favorites_v1', JSON.stringify(next)); } catch (e2) {}
|
||||||
return next;
|
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) => {
|
const openFavorite = (fav) => {
|
||||||
if (!fav) return;
|
if (!fav) return;
|
||||||
@@ -9459,6 +9525,22 @@ const MediaExplorerPanel = ({ height }) => {
|
|||||||
const node = computerTree[fav.path];
|
const node = computerTree[fav.path];
|
||||||
if (node && node.handle) {
|
if (node && node.handle) {
|
||||||
browseComputerDir({ ...entry, handle: 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) {
|
} else if (fav.path === 'root' && clientRoot) {
|
||||||
browseComputerDir({ name: clientRoot.name, path: 'root', is_dir: true, handle: clientRoot });
|
browseComputerDir({ name: clientRoot.name, path: 'root', is_dir: true, handle: clientRoot });
|
||||||
} else if (fav.path && fav.path.startsWith('root/') && 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="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> <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 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
|
<i className="fa-solid fa-computer text-[11px] text-slate-600"></i> My Computer
|
||||||
</div>
|
</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="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) => (
|
{favorites.map((fav, fi) => (
|
||||||
<div key={fav.path + 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"
|
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>
|
<i className="fa-solid fa-star text-amber-500 text-[9px] shrink-0"></i>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{favorites.length === 0 && (
|
||||||
|
<div className="pl-4 py-0.5 text-slate-400 italic text-[11px]">No favorites</div>
|
||||||
|
)}
|
||||||
</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')}>
|
<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
|
<i className="fa-solid fa-folder-open text-[#d9a752]"></i> Media Library
|
||||||
</div>
|
</div>
|
||||||
@@ -9985,8 +10073,6 @@ const MediaExplorerPanel = ({ height }) => {
|
|||||||
<i className="fa-solid fa-folder text-[#d9a752]"></i> Processed
|
<i className="fa-solid fa-folder text-[#d9a752]"></i> Processed
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
<div className="w-1.5 cursor-ew-resize hover:bg-blue-500/40 active:bg-blue-500/60 transition-colors shrink-0"
|
<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' : ''}`}
|
<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); }}>
|
onClick={() => { toggleFavorite(favContext); setFavContext(null); }}>
|
||||||
<i className={`fa-solid ${isFavorite(favContext) ? 'fa-star text-amber-500' : 'fa-star text-slate-400'} text-[11px]`}></i>
|
<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>
|
||||||
<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); }}>
|
<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
|
<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 */}
|
{/* FILE LIST */}
|
||||||
<div className="flex-1 bg-white border border-[#808080] m-1 overflow-y-auto relative">
|
<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">
|
<thead className="sticky top-0 bg-[#e0e0e0] border-b border-[#a0a0a0] text-slate-800 font-semibold select-none shadow-sm z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="py-1 px-2 border-r border-[#b0b0b0]">File</th>
|
<th className="py-1 px-2 border-r border-[#b0b0b0] relative" style={viewMode === 'details' ? { width: colWidths.file } : undefined}>
|
||||||
{viewMode === 'details' && <th className="py-1 px-2 border-r border-[#b0b0b0]">Size</th>}
|
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="font-sans text-slate-800">
|
<tbody className="font-sans text-slate-800">
|
||||||
@@ -10036,14 +10139,22 @@ const MediaExplorerPanel = ({ height }) => {
|
|||||||
onDragEnd={() => { window.__mediaExplorerDragFile = null; }}
|
onDragEnd={() => { window.__mediaExplorerDragFile = null; }}
|
||||||
className={`cursor-pointer hover:bg-blue-100 ${isSel ? 'file-row-selected' : ''}`}
|
className={`cursor-pointer hover:bg-blue-100 ${isSel ? 'file-row-selected' : ''}`}
|
||||||
onClick={() => f.is_dir ? browseComputerDir(f) : handleSelect(f)}
|
onClick={() => f.is_dir ? browseComputerDir(f) : handleSelect(f)}
|
||||||
onDoubleClick={() => f.is_dir && browseComputerDir(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>
|
onContextMenu={e => {
|
||||||
{viewMode === 'details' && <td className="py-1 px-2">{f.size_mb != null ? f.size_mb.toFixed(2) + ' MB' : (isMidi ? (f.tpqn || 'MIDI') + ' TPQN' : '-')}</td>}
|
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>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{visibleFiles.length === 0 && (
|
{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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user