fix: My Computer client-side tự động quét cây không mở picker
This commit is contained in:
+16
-9
@@ -9353,6 +9353,22 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
}
|
||||
}
|
||||
} 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) {}
|
||||
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);
|
||||
@@ -9371,15 +9387,6 @@ const MediaExplorerPanel = ({ height }) => {
|
||||
res.dirs.slice(0, 8).forEach(d => browseComputerDir(d));
|
||||
}
|
||||
});
|
||||
// Nếu có root handle đã lưu (File System Access API), cũng tự động quét client tree
|
||||
const savedHandle = await loadClientRootHandle();
|
||||
if (savedHandle && savedHandle.kind === 'directory' && !computerRoots) {
|
||||
setClientRoot(savedHandle);
|
||||
setComputerRoots([{ name: savedHandle.name, path: 'root', is_dir: true, handle: savedHandle }]);
|
||||
setComputerMode('client');
|
||||
const browsed = await browseComputerDir({ name: savedHandle.name, path: 'root', is_dir: true, handle: savedHandle });
|
||||
if (browsed && browsed.dirs) browsed.dirs.slice(0, 10).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');
|
||||
|
||||
@@ -242,10 +242,10 @@ const storedTree=(()=>{try{return JSON.parse(localStorage.getItem(SESSION_KEY)||
|
||||
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){}// Auto-scan ổ đĩa/thư mục hệ thống (server API = máy local), không mở hộp thoại picker
|
||||
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){}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));}});// Nếu có root handle đã lưu (File System Access API), cũng tự động quét client tree
|
||||
const savedHandle=await loadClientRootHandle();if(savedHandle&&savedHandle.kind==='directory'&&!computerRoots){setClientRoot(savedHandle);setComputerRoots([{name:savedHandle.name,path:'root',is_dir:true,handle:savedHandle}]);setComputerMode('client');const browsed=await browseComputerDir({name:savedHandle.name,path:'root',is_dir:true,handle:savedHandle});if(browsed&&browsed.dirs)browsed.dirs.slice(0,10).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 ──
|
||||
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 ──
|
||||
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;if(parentHandle){const parentEntry=computerRoots&&computerRoots[0]&&parentHandle===clientRoot?computerRoots[0]:{name:parentHandle.name,path:computerPath.split('/').slice(0,-1).join('/')||'root',is_dir:true,handle:parentHandle};browseComputerDir({...parentEntry,parent:parentHandle===clientRoot?null:computerTree[parentEntry.path]?computerTree[parentEntry.path].parent:null});}return;}const isUnix=computerPath.startsWith('/');const parts=computerPath.split(/[\\/]/).filter(Boolean);parts.pop();if(isUnix){browseComputerDir(parts.length?'/'+parts.join('/'):'/');}else{// Windows: quay về root ổ đĩa nếu đã lên tới đỉnh
|
||||
|
||||
@@ -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=202608021965" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608021975" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
@@ -1154,3 +1154,8 @@
|
||||
- **Tóm tắt thay đổi:** (1) `openMyComputer` bỏ `showDirectoryPicker` khi click node My Computer — giờ tự động quét server API (`/api/v1/media/computer` liệt kê ổ đĩa máy local + `/browse` expand ổ đĩa → expand thư mục con cấp 1), cây hiển thị ngay không cần hộp thoại; vẫn restore session/handle IndexedDB nếu có. (2) Playhead trong `drawCanvas` (MIDI + audio): công thức `max(0, min(w, t*pxPerSec - offset))` — khi content rộng hơn frame, playhead chạy tới giữa rồi khóa ở giữa trong khi content scroll trái; khi offset đạt max (contentW-w) thì playhead tiếp tục chạy với tốc độ play tới cuối canvas.
|
||||
- **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: click My Computer → picker NOT called, DATA/HOME + Music/user auto-load; playhead math: big clip t=1→84, t=3→150 (center), t=5→150 locked, t=10→300 (end). Hard reload.
|
||||
|
||||
### [2026-08-02 19:75] Task: Fix My Computer client-side tự động quét cây
|
||||
- **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.
|
||||
|
||||
Reference in New Issue
Block a user