fix: My Computer hiển thị cây hệ thống thay vì Favorites session cũ

This commit is contained in:
2026-08-02 18:47:39 +07:00
parent 4a04bc04ef
commit 5e3a283bd9
4 changed files with 7 additions and 34 deletions
-30
View File
@@ -9323,36 +9323,6 @@ const MediaExplorerPanel = ({ height }) => {
const openMyComputer = async () => {
setFolder('computer');
// Restore last opened folder from session (click My Computer show folder content directly)
try {
const raw = localStorage.getItem(SESSION_KEY);
if (raw) {
const snap = JSON.parse(raw);
if (snap && snap.computerPath && snap.computerFiles) {
setComputerPath(snap.computerPath);
setComputerFiles(snap.computerFiles);
setComputerMode(snap.computerMode || 'server');
if (snap.computerTree) {
const tree = {};
Object.keys(snap.computerTree).forEach(p => { tree[p] = { dirs: snap.computerTree[p].dirs || [], expanded: snap.computerTree[p].expanded }; });
setComputerTree(tree);
}
if (snap.computerRoots && snap.computerRoots.length) setComputerRoots(snap.computerRoots);
if (snap.selected) setSelected(snap.selected);
// For client mode, re-attach real handles by walking from the stored root handle
if (snap.computerMode === 'client') {
const savedHandle = await loadClientRootHandle();
if (savedHandle && savedHandle.kind === 'directory') {
setClientRoot(savedHandle);
setComputerRoots([{ name: savedHandle.name, path: 'root', is_dir: true, handle: savedHandle }]);
setComputerMode('client');
browseComputerDir({ name: savedHandle.name, path: 'root', is_dir: true, handle: savedHandle });
}
}
return;
}
}
} catch (e) {}
// Ưu tiên client-side: nếu có root handle đã lưu (File System Access API) t đng quét cây client
let savedHandle = null;
try { savedHandle = await loadClientRootHandle(); } catch (e) {}
+1 -3
View File
@@ -240,9 +240,7 @@ const SESSION_KEY='studio_media_explorer_session_v1';const saveClientRootHandle=
const savedHandle=await loadClientRootHandle();if(savedHandle&&savedHandle.kind==='directory'){setClientRoot(savedHandle);const restoredPath=(()=>{try{return JSON.parse(localStorage.getItem(SESSION_KEY)||'{}').computerPath;}catch(e){return null;}})();if(restoredPath){setComputerRoots([{name:savedHandle.name,path:'root',is_dir:true,handle:savedHandle}]);// Re-browse current dir to re-attach real handles (metadata-only tree from localStorage)
const storedTree=(()=>{try{return JSON.parse(localStorage.getItem(SESSION_KEY)||'{}').computerTree;}catch(e){return null;}})();if(storedTree){const rebuilt={};Object.keys(storedTree).forEach(p=>{rebuilt[p]={dirs:storedTree[p].dirs||[],expanded:storedTree[p].expanded};});setComputerTree(rebuilt);}setComputerMode('client');// Rebuild handles for the current folder + ancestors by walking from root
browseComputerDir({name:savedHandle.name,path:'root',is_dir:true,handle:savedHandle});}}})();// eslint-disable-next-line react-hooks/exhaustive-deps
},[]);const isMidiFile=f=>f&&(f.kind==='midi'||/\.(mid|midi)$/i.test(f.name||f.original_name||''));const fileDuration=f=>{if(!f)return 0;if(isMidiFile(f)){if(midiTotalRef.current&&midiNotesRef.current&&midiNotesRef.current.length)return midiTotalRef.current;return(f.lengthQn||16)*60/(f.bpm||tempoRef.current||120);}const sel=selectedRef.current;const matches=sel&&(f.path&&f.path===sel.path||!f.path&&(f.file_id||f.fileId)===(sel.file_id||sel.fileId));return f.duration||(matches&&audioDurationRef.current?audioDurationRef.current:0)||(audioBufferRef.current&&matches?audioBufferRef.current.duration:0)||0;};const folderFiles=React.useMemo(()=>{if(folder==='library')return MEDIA_LIBRARY_SAMPLES;if(folder==='computer'){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]);const visibleFiles=React.useMemo(()=>{const q=filterText.toLowerCase().trim();if(!q)return folderFiles;return folderFiles.filter(f=>(f.name||f.original_name||'').toLowerCase().includes(q));},[folderFiles,filterText]);const loadWaveform=async f=>{const token=selectTokenRef.current;if(f&&(f.handle||f.path)){try{const buf=await readLocalFileBuffer(f);if(selectTokenRef.current!==token)return;if(!buf){setPeaks(null);return;}const ctx=getAudioContext();const decoded=await ctx.decodeAudioData(buf);if(selectTokenRef.current!==token)return;setAudioBuffer(decoded);setAudioDuration(decoded.duration);const data=decoded.getChannelData(0);const count=600;const step=Math.max(1,Math.floor(data.length/count));const pk=[];for(let i=0;i<data.length;i+=step){let m=0;for(let j=i;j<Math.min(i+step,data.length);j++){const v=Math.abs(data[j]);if(v>m)m=v;}pk.push(m);}setPeaks(pk);}catch(e){if(selectTokenRef.current===token)setPeaks(null);}return;}const fid=f.file_id||f.fileId;if(!fid){setPeaks(null);return;}try{const resp=await fetch(`${API_BASE_URL}/api/v1/audio/waveform/${fid}?num_peaks=600`);const data=await resp.json();if(selectTokenRef.current!==token)return;setPeaks(data.peaks||[]);if(data.duration)setAudioDuration(data.duration);}catch(e){setPeaks(null);}};const openMyComputer=async()=>{setFolder('computer');// Restore last opened folder from session (click My Computer → show folder content directly)
try{const raw=localStorage.getItem(SESSION_KEY);if(raw){const snap=JSON.parse(raw);if(snap&&snap.computerPath&&snap.computerFiles){setComputerPath(snap.computerPath);setComputerFiles(snap.computerFiles);setComputerMode(snap.computerMode||'server');if(snap.computerTree){const tree={};Object.keys(snap.computerTree).forEach(p=>{tree[p]={dirs:snap.computerTree[p].dirs||[],expanded:snap.computerTree[p].expanded};});setComputerTree(tree);}if(snap.computerRoots&&snap.computerRoots.length)setComputerRoots(snap.computerRoots);if(snap.selected)setSelected(snap.selected);// For client mode, re-attach real handles by walking from the stored root handle
if(snap.computerMode==='client'){const savedHandle=await loadClientRootHandle();if(savedHandle&&savedHandle.kind==='directory'){setClientRoot(savedHandle);setComputerRoots([{name:savedHandle.name,path:'root',is_dir:true,handle:savedHandle}]);setComputerMode('client');browseComputerDir({name:savedHandle.name,path:'root',is_dir:true,handle:savedHandle});}}return;}}}catch(e){}// Ưu tiên client-side: nếu có root handle đã lưu (File System Access API) → tự động quét cây client
},[]);const isMidiFile=f=>f&&(f.kind==='midi'||/\.(mid|midi)$/i.test(f.name||f.original_name||''));const fileDuration=f=>{if(!f)return 0;if(isMidiFile(f)){if(midiTotalRef.current&&midiNotesRef.current&&midiNotesRef.current.length)return midiTotalRef.current;return(f.lengthQn||16)*60/(f.bpm||tempoRef.current||120);}const sel=selectedRef.current;const matches=sel&&(f.path&&f.path===sel.path||!f.path&&(f.file_id||f.fileId)===(sel.file_id||sel.fileId));return f.duration||(matches&&audioDurationRef.current?audioDurationRef.current:0)||(audioBufferRef.current&&matches?audioBufferRef.current.duration:0)||0;};const folderFiles=React.useMemo(()=>{if(folder==='library')return MEDIA_LIBRARY_SAMPLES;if(folder==='computer'){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]);const visibleFiles=React.useMemo(()=>{const q=filterText.toLowerCase().trim();if(!q)return folderFiles;return folderFiles.filter(f=>(f.name||f.original_name||'').toLowerCase().includes(q));},[folderFiles,filterText]);const loadWaveform=async f=>{const token=selectTokenRef.current;if(f&&(f.handle||f.path)){try{const buf=await readLocalFileBuffer(f);if(selectTokenRef.current!==token)return;if(!buf){setPeaks(null);return;}const ctx=getAudioContext();const decoded=await ctx.decodeAudioData(buf);if(selectTokenRef.current!==token)return;setAudioBuffer(decoded);setAudioDuration(decoded.duration);const data=decoded.getChannelData(0);const count=600;const step=Math.max(1,Math.floor(data.length/count));const pk=[];for(let i=0;i<data.length;i+=step){let m=0;for(let j=i;j<Math.min(i+step,data.length);j++){const v=Math.abs(data[j]);if(v>m)m=v;}pk.push(m);}setPeaks(pk);}catch(e){if(selectTokenRef.current===token)setPeaks(null);}return;}const fid=f.file_id||f.fileId;if(!fid){setPeaks(null);return;}try{const resp=await fetch(`${API_BASE_URL}/api/v1/audio/waveform/${fid}?num_peaks=600`);const data=await resp.json();if(selectTokenRef.current!==token)return;setPeaks(data.peaks||[]);if(data.duration)setAudioDuration(data.duration);}catch(e){setPeaks(null);}};const openMyComputer=async()=>{setFolder('computer');// Ưu tiên client-side: nếu có 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'){setComputerMode('client');setClientRoot(savedHandle);const rootEntry={name:savedHandle.name,path:'root',is_dir:true,handle:savedHandle};setComputerRoots([rootEntry]);setComputerPath('root');setComputerFiles([]);const browsed=await browseComputerDir(rootEntry);if(browsed&&browsed.dirs){browsed.dirs.slice(0,10).forEach(d=>browseComputerDir(d));}return;}// Auto-scan ổ đĩa/thư mục hệ thống (server API = máy local), không mở hộp thoại picker
setComputerMode('server');setComputerRoots(null);try{const resp=await fetch(`${API_BASE_URL}/api/v1/media/computer`);if(!resp.ok)throw new Error('HTTP '+resp.status);const data=await resp.json();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));}});}catch(e){setComputerRoots([{path:'/',name:'Root (/)',is_dir:true}]);window.showToast&&window.showToast('Không thể truy cập My Computer: '+e.message,'error');}};const browseClientDir=async entry=>{const handle=entry&&entry.handle;if(!handle||!handle.entries)return null;const dirs=[];const files=[];const parentPath=entry.path;try{for await(const[name,h]of handle.entries()){if(name.startsWith('.'))continue;if(h.kind==='directory'){dirs.push({name,path:parentPath+'/'+name,is_dir:true,handle:h});}else{const ext=(name.split('.').pop()||'').toLowerCase();const kind=ext==='mid'||ext==='midi'?'midi':['wav','mp3','ogg','flac','aiff','aif','m4a','aac','opus'].includes(ext)?'audio':'other';let size=0;try{const f=await h.getFile();size=f.size;}catch(e2){}files.push({name,path:parentPath+'/'+name,is_dir:false,size_mb:size?+(size/1048576).toFixed(2):0,ext:'.'+ext,kind,handle:h});}}dirs.sort((a,b)=>a.name.localeCompare(b.name));files.sort((a,b)=>a.name.localeCompare(b.name));setComputerPath(parentPath);setComputerFiles(files);setSelected(null);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 null;if(computerMode==='client'||entry.handle){if(entry.handle&&entry.handle.entries){const parentInfo=computerTree[entry.path];return await browseClientDir({...entry,parent:parentInfo?parentInfo.parent:null});}}const path=entry.path||entry;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);const data=await resp.json();setComputerPath(data.path);setComputerFiles(data.files||[]);setSelected(null);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;}};const toggleComputerDir=async entry=>{if(!entry)return;const path=entry.path||entry;const node=computerTree[path];if(node&&node.expanded){setComputerTree(prev=>({...prev,[path]:{...prev[path],expanded:false}}));}else{await browseComputerDir(entry);}};// ── Favorites: thư mục yêu thích ──
+1 -1
View File
@@ -24,7 +24,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608021975" defer></script>
<script src="/static/js/app.precompiled.js?v=202608021985" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+5
View File
@@ -1159,3 +1159,8 @@
- **Tóm tắt thay đổi:** `openMyComputer` ưu tiên **client-side**: load root handle từ IndexedDB → tự động quét cây thư mục client (C:, Users, Program Files...) mà KHÔNG gọi `showDirectoryPicker` (đã gỡ hẳn). Fix bug cũ: `if (savedHandle && ... && !computerRoots)` luôn false vì `computerRoots` vừa được set bởi server API → client handle không bao giờ restore được → gây ra hộp thoại picker. Giờ: (1) nếu có client handle → auto-scan client tree + expand; (2) không có → fallback server API `/api/v1/media/computer` tự động quét ổ đĩa máy local. Mọi path đều không mở hộp thoại.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
- **Ghi chú/Test (nếu có):** Smoke jsdom: IndexedDB có client handle `C:` → click My Computer → picker NOT called, cây C:/Users/Program Files auto-load. Không handle → server API Root (/) auto-load, picker NOT called. Hard reload (browser cache cũ có thể vẫn hiện picker). Commit.
### [2026-08-02 19:85] Task: Fix My Computer hiển thị nhầm Favorites thay vì cây hệ thống
- **Tóm tắt thay đổi:** `openMyComputer` bỏ block restore session cũ (đọc `studio_media_explorer_session_v1` từ localStorage ghi đè `computerRoots`/`computerTree` bằng dữ liệu favorites/stale → khi click My Computer hiện thư mục favorite cũ thay vì cây client). Giờ click My Computer luôn: (1) load client root handle từ IndexedDB → auto-scan cây client thật (C:, Users, Program Files...) hoặc (2) fallback server API scan ổ đĩa local. Không còn ghi đè bằng session cũ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
- **Ghi chú/Test (nếu có):** Smoke jsdom: seed session cũ chứa `OLD_FAV` + favorites → click My Computer → picker NOT called, hiện C:/Users/Program Files (client tree), OLD_FAV không hiển thị. Hard reload.