fix: My Computer luôn hiển thị cây hệ thống client, bỏ restore folder cũ

This commit is contained in:
2026-08-02 18:55:14 +07:00
parent 5e3a283bd9
commit befc0d35fd
4 changed files with 13 additions and 34 deletions
+3 -29
View File
@@ -9205,6 +9205,8 @@ const MediaExplorerPanel = ({ height }) => {
}, [saveSession, computerMode, clientRoot]);
const restoreSession = React.useCallback(() => {
// Ch khôi phc favorites + các pref khác. KHÔNG khôi phc thư mc/tree đã m:
// My Computer luôn phi hin th cây h thng client.
try {
const raw = localStorage.getItem(SESSION_KEY);
if (!raw) return;
@@ -9212,44 +9214,16 @@ const MediaExplorerPanel = ({ height }) => {
if (snap.folder) setFolder(snap.folder);
if (snap.computerMode) setComputerMode(snap.computerMode);
if (snap.clientRootName) setClientRoot({ name: snap.clientRootName });
if (snap.computerRoots && snap.computerRoots.length) setComputerRoots(snap.computerRoots);
if (snap.computerPath) setComputerPath(snap.computerPath);
if (snap.computerFiles) setComputerFiles(snap.computerFiles);
if (snap.selected) setSelected(snap.selected);
if (snap.computerTree) {
const tree = {};
Object.keys(snap.computerTree).forEach(path => {
tree[path] = {
dirs: snap.computerTree[path].dirs || [],
expanded: snap.computerTree[path].expanded
};
});
setComputerTree(tree);
}
} catch (e) {}
}, []);
React.useEffect(() => {
(async () => {
await restoreSession();
// Restore the FileSystemDirectoryHandle so client-mode browsing still works
// Ch ni li FileSystemDirectoryHandle đ client browsing hot đng (không khôi phc folder cũ)
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
+4 -4
View File
@@ -236,10 +236,10 @@ const MEDIA_LIBRARY_SAMPLES=[{name:"MIDI_Loop_01.mid",events:95,lengthQn:16,time
// stale closure) always draws the currently selected file, not the old one.
const selectedRef=React.useRef(null);const peaksRef=React.useRef(null);const audioBufferRef=React.useRef(null);const audioDurationRef=React.useRef(0);const midiNotesRef=React.useRef(null);const midiTotalRef=React.useRef(4);const midiBarsRef=React.useRef(1);const midiTotalBeatsRef=React.useRef(16);const midiFileBpmRef=React.useRef(120);const isPlayingRef=React.useRef(false);const isPausedRef=React.useRef(false);const folderRef=React.useRef('library');const tempoRef=React.useRef(120);const currentTimeRef=React.useRef(0);selectedRef.current=selected;peaksRef.current=peaks;audioBufferRef.current=audioBuffer;audioDurationRef.current=audioDuration;midiNotesRef.current=midiNotes;midiTotalRef.current=midiTotal;midiBarsRef.current=midiBars;midiTotalBeatsRef.current=midiTotalBeats;midiFileBpmRef.current=midiFileBpm;isPlayingRef.current=isPlaying;isPausedRef.current=isPaused;folderRef.current=folder;tempoRef.current=tempo;currentTimeRef.current=currentTime;const[computerRoots,setComputerRoots]=React.useState(null);const[computerTree,setComputerTree]=React.useState({});const[computerPath,setComputerPath]=React.useState(null);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;}());const[synthOpen,setSynthOpen]=React.useState(false);const[synthList,setSynthList]=React.useState(null);const[synthLoading,setSynthLoading]=React.useState(false);const synthListRef=React.useRef(null);synthListRef.current=synthList;const synthInstRef=React.useRef(null);synthInstRef.current=synthInst;const[treeWidth,setTreeWidth]=React.useState(function(){var saved=localStorage.getItem('studio_media_explorer_tree_width');return saved?parseInt(saved):176;}());const treeWidthRef=React.useRef(176);treeWidthRef.current=treeWidth;const startTreeResize=e=>{e.preventDefault();const startX=e.clientX;const startW=treeWidthRef.current;const onMove=ev=>{const newW=Math.max(110,Math.min(420,startW+(ev.clientX-startX)));treeWidthRef.current=newW;setTreeWidth(newW);};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);localStorage.setItem('studio_media_explorer_tree_width',treeWidthRef.current.toString());};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};const canvasRef=React.useRef(null);const playStateRef=React.useRef(null);const rafRef=React.useRef(null);const loopTimerRef=React.useRef(null);const selectTokenRef=React.useRef(0);React.useEffect(()=>{if(window.SonicAPI&&window.SonicAPI.listMyFiles){window.SonicAPI.listMyFiles([]).then(data=>setUserFiles(data||[])).catch(()=>{});}return()=>{stopMediaPlayback();};// eslint-disable-next-line react-hooks/exhaustive-deps
},[]);// ── Session persistence: keep loaded folder/files/tree across panel toggles & reloads ──
const SESSION_KEY='studio_media_explorer_session_v1';const saveClientRootHandle=()=>{if(!clientRoot||!window.indexedDB)return;try{const req=indexedDB.open('sonicforge_media_explorer',1);req.onupgradeneeded=e=>{const db=e.target.result;if(!db.objectStoreNames.contains('root_handle'))db.createObjectStore('root_handle');};req.onsuccess=()=>{const db=req.result;const tx=db.transaction('root_handle','readwrite');tx.objectStore('root_handle').put(clientRoot,'client_root');};}catch(e){}};const loadClientRootHandle=()=>{return new Promise(resolve=>{if(!window.indexedDB){resolve(null);return;}try{const req=indexedDB.open('sonicforge_media_explorer',1);req.onupgradeneeded=e=>{const db=e.target.result;if(!db.objectStoreNames.contains('root_handle'))db.createObjectStore('root_handle');};req.onsuccess=()=>{const db=req.result;try{const tx=db.transaction('root_handle','readonly');const g=tx.objectStore('root_handle').get('client_root');g.onsuccess=()=>resolve(g.result||null);g.onerror=()=>resolve(null);}catch(e2){resolve(null);}};req.onerror=()=>resolve(null);}catch(e){resolve(null);}});};const saveSession=React.useCallback(()=>{try{const stripHandle=o=>{if(Array.isArray(o))return o.map(stripHandle);if(o&&typeof o==='object'){const out={};for(const k of Object.keys(o)){if(k==='handle')continue;out[k]=stripHandle(o[k]);}return out;}return o;};const treeSnapshot={};Object.keys(computerTree).forEach(path=>{const node=computerTree[path];treeSnapshot[path]={dirs:stripHandle(node?node.dirs:[]),expanded:!!(node&&node.expanded)};});const snap={folder,computerMode,computerPath,clientRootName:clientRoot?clientRoot.name:null,computerRoots:stripHandle(computerRoots||[]),computerTree:treeSnapshot,computerFiles:stripHandle(computerFiles||[]),selected:selected?stripHandle({name:selected.name,path:selected.path,kind:selected.kind,is_dir:selected.is_dir,size_mb:selected.size_mb}):null,savedAt:Date.now()};localStorage.setItem(SESSION_KEY,JSON.stringify(snap));}catch(e){}},[folder,computerMode,computerPath,clientRoot,computerRoots,computerTree,computerFiles,selected]);React.useEffect(()=>{saveSession();if(computerMode==='client'&&clientRoot&&clientRoot.kind==='directory')saveClientRootHandle();},[saveSession,computerMode,clientRoot]);const restoreSession=React.useCallback(()=>{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});if(snap.computerRoots&&snap.computerRoots.length)setComputerRoots(snap.computerRoots);if(snap.computerPath)setComputerPath(snap.computerPath);if(snap.computerFiles)setComputerFiles(snap.computerFiles);if(snap.selected)setSelected(snap.selected);if(snap.computerTree){const tree={};Object.keys(snap.computerTree).forEach(path=>{tree[path]={dirs:snap.computerTree[path].dirs||[],expanded:snap.computerTree[path].expanded};});setComputerTree(tree);}}catch(e){}},[]);React.useEffect(()=>{(async()=>{await restoreSession();// Restore the FileSystemDirectoryHandle so client-mode browsing still works
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 SESSION_KEY='studio_media_explorer_session_v1';const saveClientRootHandle=()=>{if(!clientRoot||!window.indexedDB)return;try{const req=indexedDB.open('sonicforge_media_explorer',1);req.onupgradeneeded=e=>{const db=e.target.result;if(!db.objectStoreNames.contains('root_handle'))db.createObjectStore('root_handle');};req.onsuccess=()=>{const db=req.result;const tx=db.transaction('root_handle','readwrite');tx.objectStore('root_handle').put(clientRoot,'client_root');};}catch(e){}};const loadClientRootHandle=()=>{return new Promise(resolve=>{if(!window.indexedDB){resolve(null);return;}try{const req=indexedDB.open('sonicforge_media_explorer',1);req.onupgradeneeded=e=>{const db=e.target.result;if(!db.objectStoreNames.contains('root_handle'))db.createObjectStore('root_handle');};req.onsuccess=()=>{const db=req.result;try{const tx=db.transaction('root_handle','readonly');const g=tx.objectStore('root_handle').get('client_root');g.onsuccess=()=>resolve(g.result||null);g.onerror=()=>resolve(null);}catch(e2){resolve(null);}};req.onerror=()=>resolve(null);}catch(e){resolve(null);}});};const saveSession=React.useCallback(()=>{try{const stripHandle=o=>{if(Array.isArray(o))return o.map(stripHandle);if(o&&typeof o==='object'){const out={};for(const k of Object.keys(o)){if(k==='handle')continue;out[k]=stripHandle(o[k]);}return out;}return o;};const treeSnapshot={};Object.keys(computerTree).forEach(path=>{const node=computerTree[path];treeSnapshot[path]={dirs:stripHandle(node?node.dirs:[]),expanded:!!(node&&node.expanded)};});const snap={folder,computerMode,computerPath,clientRootName:clientRoot?clientRoot.name:null,computerRoots:stripHandle(computerRoots||[]),computerTree:treeSnapshot,computerFiles:stripHandle(computerFiles||[]),selected:selected?stripHandle({name:selected.name,path:selected.path,kind:selected.kind,is_dir:selected.is_dir,size_mb:selected.size_mb}):null,savedAt:Date.now()};localStorage.setItem(SESSION_KEY,JSON.stringify(snap));}catch(e){}},[folder,computerMode,computerPath,clientRoot,computerRoots,computerTree,computerFiles,selected]);React.useEffect(()=>{saveSession();if(computerMode==='client'&&clientRoot&&clientRoot.kind==='directory')saveClientRootHandle();},[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(()=>{(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;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