From 224fd54a5609bd57b6a97de1b01a0a1a78597e85 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Sun, 2 Aug 2026 18:59:35 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20My=20Computer=20hi=E1=BB=83n=20th?= =?UTF-8?q?=E1=BB=8B=20=E1=BB=95=20=C4=91=C4=A9a/th=C6=B0=20m=E1=BB=A5c,?= =?UTF-8?q?=20fallback=20khi=20client=20handle=20h=E1=BA=BFt=20quy?= =?UTF-8?q?=E1=BB=81n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 33 ++++++++++++++++++++++---------- app/static/js/app.precompiled.js | 3 ++- app/templates/index.html | 2 +- wiki.md | 5 +++++ 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 519f549..aa100a0 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -9301,17 +9301,30 @@ const MediaExplorerPanel = ({ height }) => { 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)); + // 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) { + 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.length) { + browsed.dirs.slice(0, 10).forEach(d => browseComputerDir(d)); + return; + } } - 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'); diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index f6cf662..c7ca4eb 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -241,7 +241,8 @@ const SESSION_KEY='studio_media_explorer_session_v1';const saveClientRootHandle= 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ỉ nối lại FileSystemDirectoryHandle để client browsing hoạt động (không khôi phục folder cũ) const savedHandle=await loadClientRootHandle();if(savedHandle&&savedHandle.kind==='directory'){setClientRoot(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;im)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 +let savedHandle=null;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){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.length){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 ── 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 diff --git a/app/templates/index.html b/app/templates/index.html index 44cae0e..e4290d8 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -24,7 +24,7 @@ - +