diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx
index b3af8b1..1f7d963 100644
--- a/app/static/js/app.jsx
+++ b/app/static/js/app.jsx
@@ -9025,6 +9025,7 @@ const MediaExplorerPanel = ({ height }) => {
const [currentTime, setCurrentTime] = React.useState(0);
const [peaks, setPeaks] = React.useState(null);
const [audioBuffer, setAudioBuffer] = React.useState(null);
+ const [audioDuration, setAudioDuration] = React.useState(0);
const [midiNotes, setMidiNotes] = React.useState(null);
const [midiTotal, setMidiTotal] = React.useState(4);
const [midiBars, setMidiBars] = React.useState(1);
@@ -9039,6 +9040,7 @@ const MediaExplorerPanel = ({ height }) => {
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);
@@ -9052,6 +9054,7 @@ const MediaExplorerPanel = ({ height }) => {
selectedRef.current = selected;
peaksRef.current = peaks;
audioBufferRef.current = audioBuffer;
+ audioDurationRef.current = audioDuration;
midiNotesRef.current = midiNotes;
midiTotalRef.current = midiTotal;
midiBarsRef.current = midiBars;
@@ -9257,7 +9260,7 @@ const MediaExplorerPanel = ({ height }) => {
}
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 || (audioBufferRef.current && matches ? audioBufferRef.current.duration : 0) || 0;
+ return f.duration || (matches && audioDurationRef.current ? audioDurationRef.current : 0) || (audioBufferRef.current && matches ? audioBufferRef.current.duration : 0) || 0;
};
const folderFiles = React.useMemo(() => {
@@ -9286,6 +9289,7 @@ const MediaExplorerPanel = ({ height }) => {
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));
@@ -9309,6 +9313,7 @@ const MediaExplorerPanel = ({ height }) => {
const data = await resp.json();
if (selectTokenRef.current !== token) return;
setPeaks(data.peaks || []);
+ if (data.duration) setAudioDuration(data.duration);
} catch (e) { setPeaks(null); }
};
@@ -9629,6 +9634,7 @@ const MediaExplorerPanel = ({ height }) => {
const decoded = await ctx.decodeAudioData(buf);
if (selectTokenRef.current !== (token || selectTokenRef.current)) return;
setAudioBuffer(decoded);
+ setAudioDuration(decoded.duration);
const src = ctx.createBufferSource();
src.buffer = decoded;
src.loop = isLooping;
@@ -9761,11 +9767,12 @@ const MediaExplorerPanel = ({ height }) => {
}
const mid = h / 2;
ctx.fillStyle = '#22c55e';
+ const barW = Math.max(1, contentW / pk.length);
for (let i = 0; i < pk.length; i++) {
const x = (i / pk.length) * contentW - offset;
if (x < -3 || x > w + 3) continue;
const ph = Math.max(2, pk[i] * (h / 2 - 4));
- ctx.fillRect(x, mid - ph, Math.max(1, w / pk.length), ph * 2);
+ ctx.fillRect(x, mid - ph, barW, ph * 2);
}
const audioPlayheadX = contentW > w ? Math.min(w / 2, t * pxPerSec) : Math.min(w, t * pxPerSec);
if (playing && dur > 0) {
@@ -9805,6 +9812,7 @@ const MediaExplorerPanel = ({ height }) => {
setCurrentTime(0);
setPeaks(null);
setAudioBuffer(null);
+ setAudioDuration(0);
setMidiNotes(null);
stopMediaPlayback();
if (f.kind === 'other') return;
@@ -9944,6 +9952,14 @@ const MediaExplorerPanel = ({ height }) => {
const icon = f.is_dir ? 'fa-folder text-[#d9a752]' : (isMidi ? 'fa-music text-purple-600' : (f.kind === 'audio' ? 'fa-file-audio text-emerald-600' : 'fa-file text-zinc-500'));
return (
{
+ if (f.is_dir) { e.preventDefault(); return; }
+ e.dataTransfer.setData('text/plain', f.name || f.original_name || '');
+ e.dataTransfer.effectAllowed = 'copy';
+ window.__mediaExplorerDragFile = f;
+ }}
+ onDragEnd={() => { window.__mediaExplorerDragFile = null; }}
className={`cursor-pointer hover:bg-blue-100 ${isSel ? 'file-row-selected' : ''}`}
onClick={() => f.is_dir ? browseComputerDir(f) : handleSelect(f)}
onDoubleClick={() => f.is_dir && browseComputerDir(f)}>
@@ -16871,6 +16887,32 @@ const App = () => {
window.parseMidiFile = parseMidiFile;
// ── Load File on Track (with server upload) ──
+ // ── Resolve Media Explorer drag file → real File (for timeline drop) ──
+ const resolveMediaExplorerDropFile = async () => {
+ const mef = window.__mediaExplorerDragFile;
+ if (!mef) return null;
+ try {
+ if (mef.handle && typeof mef.handle.getFile === 'function') {
+ const f = await mef.handle.getFile();
+ return new File([f], f.name || mef.name, { type: f.type || 'application/octet-stream' });
+ }
+ if (mef.file_id || mef.fileId) {
+ const fid = mef.file_id || mef.fileId;
+ const resp = await fetch(`${API_BASE_URL}/api/v1/audio/download/${fid}`);
+ const blob = await resp.blob();
+ return new File([blob], mef.name || mef.original_name || fid, { type: blob.type || 'audio/wav' });
+ }
+ if (mef.path) {
+ const resp = await fetch(`${API_BASE_URL}/api/v1/media/file?path=${encodeURIComponent(mef.path)}`);
+ const blob = await resp.blob();
+ return new File([blob], mef.name || mef.path.split(/[\\/]/).pop(), { type: blob.type || 'application/octet-stream' });
+ }
+ } catch (e) {
+ showToast('Không thể nạp file từ Media Explorer: ' + e.message, 'error');
+ }
+ return null;
+ };
+
const loadFileOnTrack = async (trackId, file) => {
if (!file) return;
var fileName = file.name || '';
@@ -21609,8 +21651,20 @@ const App = () => {
})), /*#__PURE__*/React.createElement("div", {
className: "flex-1 flex flex-col divide-y divide-[#141414] relative bg-[#111111] min-h-full",
onDragOver: e => e.preventDefault(),
- onDrop: e => {
+ onDrop: async e => {
e.preventDefault();
+ // Media Explorer drag (client FS handle / server file_id / local path)
+ if (window.__mediaExplorerDragFile) {
+ const f = await resolveMediaExplorerDropFile();
+ window.__mediaExplorerDragFile = null;
+ if (!f) return;
+ if (/\.mid$|\.midi$/i.test(f.name || '')) {
+ handleDropMidiToNewTracks(f);
+ } else if (selectedTrackId) {
+ loadFileOnTrack(selectedTrackId, f);
+ }
+ return;
+ }
const f = e.dataTransfer.files && e.dataTransfer.files[0];
if (!f) return;
if (/\.mid$|\.midi$/i.test(f.name || '')) {
@@ -21641,9 +21695,16 @@ const App = () => {
},
className: `shrink-0 relative border-b border-[#141414] hover:bg-zinc-850/5 transition-colors ${isSelected ? 'bg-zinc-800/10' : ''}`,
onDragOver: e => e.preventDefault(),
- onDrop: e => {
+ onDrop: async e => {
e.preventDefault();
e.stopPropagation();
+ // Media Explorer drag (client FS handle / server file_id / local path)
+ if (window.__mediaExplorerDragFile) {
+ const f = await resolveMediaExplorerDropFile();
+ window.__mediaExplorerDragFile = null;
+ if (f) loadFileOnTrack(track.id, f);
+ return;
+ }
const f = e.dataTransfer.files && e.dataTransfer.files[0];
if (!f) return;
loadFileOnTrack(track.id, f);
diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js
index 0b02de4..ecbbf99 100644
--- a/app/static/js/app.precompiled.js
+++ b/app/static/js/app.precompiled.js
@@ -232,15 +232,15 @@ for(let i=0;i{window.removeEventListener('resize',resizeAll);if(animFrameRef.current)cancelAnimationFrame(animFrameRef.current);};},[isOpen]);React.useEffect(()=>{if(!isOpen){stopAudioDemo();knobsInitializedRef.current=false;}else{setTimeout(()=>{try{window.lucide.createIcons();}catch(e){}},100);}},[isOpen]);if(!isOpen)return null;const switchModule=name=>setOzState(prev=>({...prev,activeModule:name}));const bandKnob=(param,min,max,val,unit,label,freq,color,filterType)=>/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-2 flex flex-col justify-between items-center rounded-lg"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between text-[10px] font-bold w-full",style:{color}},/*#__PURE__*/React.createElement("span",null,label),/*#__PURE__*/React.createElement("span",{className:"text-[9px] font-mono text-slate-400"},freq)),/*#__PURE__*/React.createElement("div",{className:"my-1"},/*#__PURE__*/React.createElement(MasteringKnob,{param:param,min:min,max:max,value:ozState[param]!==undefined?ozState[param]:val,unit:unit,color:color,onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))})),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 font-mono text-center mt-1"},filterType));return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm",onClick:onClose},/*#__PURE__*/React.createElement("div",{id:"masteringModalBody",className:"flex flex-col bg-slate-950 text-slate-200 w-[95vw] h-[92vh] max-w-[1400px] rounded-2xl border border-slate-800/80 shadow-2xl overflow-hidden",onClick:e=>e.stopPropagation(),style:{fontFamily:"'Inter', sans-serif"}},/*#__PURE__*/React.createElement("header",{className:"h-12 bg-slate-900/90 border-b border-slate-800/80 flex items-center justify-between px-4 z-30 shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3"},/*#__PURE__*/React.createElement("div",{className:"w-7 h-7 rounded-lg bg-cyan-600 flex items-center justify-center font-bold text-white shadow-lg shadow-cyan-950"},/*#__PURE__*/React.createElement("i",{"data-lucide":"zap",className:"w-3.5 h-3.5"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("h1",{className:"text-xs font-bold tracking-wider text-white flex items-center gap-2"},"MASTERING SUITE ",/*#__PURE__*/React.createElement("span",{className:"text-[9px] bg-cyan-950 text-cyan-400 border border-cyan-800/80 px-1.5 py-0.5 rounded oz-font-mono"},"WEB MASTERING V10.5")))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3 bg-slate-950 px-3 py-1 rounded-lg border border-slate-800/80"},/*#__PURE__*/React.createElement("button",{onClick:startAudioDemo,className:"px-3 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-xs font-semibold flex items-center gap-1.5 transition-all shadow-md shadow-cyan-950"},/*#__PURE__*/React.createElement("i",{"data-lucide":"play",className:"w-3 h-3"})," ",/*#__PURE__*/React.createElement("span",null,"Play Reference")),/*#__PURE__*/React.createElement("button",{onClick:stopAudioDemo,className:"px-3 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs font-semibold flex items-center gap-1.5 transition-all"},/*#__PURE__*/React.createElement("i",{"data-lucide":"square",className:"w-3 h-3"})," ",/*#__PURE__*/React.createElement("span",null,"Stop")),/*#__PURE__*/React.createElement("div",{className:"h-4 w-[1px] bg-slate-800 mx-1"}),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 text-xs oz-font-mono"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400 text-[11px]"},"Preset:"),/*#__PURE__*/React.createElement("select",{className:"bg-slate-900 border border-slate-700 rounded px-2 py-0.5 text-xs text-cyan-300 outline-none focus:border-cyan-500"},/*#__PURE__*/React.createElement("option",{value:"adaptive"},"Adaptive Dynamic Master"),/*#__PURE__*/React.createElement("option",{value:"edm_club"},"EDM / Club Punch Maximizer"),/*#__PURE__*/React.createElement("option",{value:"wide_space"},"Cinematic Stereo Expansion"),/*#__PURE__*/React.createElement("option",{value:"transparent"},"Transparent High-Clarity Limiter")))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3 text-xs oz-font-mono"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400 text-[11px]"},"Target LUFS:"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-bold bg-slate-950 border border-cyan-900/60 px-2 py-0.5 rounded"},"-11.0 LUFS"),/*#__PURE__*/React.createElement("button",{onClick:()=>{getAudioContext();setMasterConnected(prev=>!prev);},className:`px-2 py-0.5 rounded text-[10px] font-bold border transition-colors ${masterConnected?'bg-emerald-700 border-emerald-500 text-white':'bg-slate-800 border-slate-700 text-slate-300 hover:bg-slate-700'}`},masterConnected?'Master ON':'Master OFF'),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-2 py-0.5 rounded text-[10px] font-bold border transition-colors bg-slate-800 border-slate-700 text-slate-300 hover:bg-[#c2410c] hover:border-red-600 hover:text-white"},"Đóng"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-500 hover:text-slate-300 ml-1"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"})))),/*#__PURE__*/React.createElement("div",{className:"h-16 bg-slate-950 border-b border-slate-800 px-4 flex items-center gap-2 overflow-x-auto shrink-0 oz-scrollbar"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-slate-500 uppercase tracking-widest mr-1 oz-font-mono shrink-0"},"CHAIN:"),/*#__PURE__*/React.createElement("div",{onClick:()=>switchModule('eq'),className:`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all ${ozState.activeModule==='eq'?'oz-card-active':'oz-card'}`},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();setOzState(prev=>({...prev,eqActive:!prev.eqActive}));},className:"w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold",style:{backgroundColor:ozState.eqActive?'#38bdf8':'#334155',color:ozState.eqActive?'#0f172a':'#94a3b8'}},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-slate-200"},"Dynamic EQ"),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-cyan-400 oz-font-mono"},"4-Band Peak"))),/*#__PURE__*/React.createElement("i",{"data-lucide":"activity",className:"w-3.5 h-3.5 text-slate-500"})),/*#__PURE__*/React.createElement("div",{onClick:()=>switchModule('imager'),className:`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all ${ozState.activeModule==='imager'?'oz-card-active':'oz-card'}`},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();setOzState(prev=>({...prev,imagerActive:!prev.imagerActive}));},className:"w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold",style:{backgroundColor:ozState.imagerActive?'#38bdf8':'#334155',color:ozState.imagerActive?'#0f172a':'#94a3b8'}},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-slate-200"},"Imager"),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-400 oz-font-mono"},"4-Band Width"))),/*#__PURE__*/React.createElement("i",{"data-lucide":"radio",className:"w-3.5 h-3.5 text-slate-500"})),/*#__PURE__*/React.createElement("div",{onClick:()=>switchModule('maximizer'),className:`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all ${ozState.activeModule==='maximizer'?'oz-card-active':'oz-card'}`},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();setOzState(prev=>({...prev,maximizerActive:!prev.maximizerActive}));},className:"w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold",style:{backgroundColor:ozState.maximizerActive?'#38bdf8':'#334155',color:ozState.maximizerActive?'#0f172a':'#94a3b8'}},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-slate-200"},"Maximizer"),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-400 oz-font-mono"},"IRC IV True Peak"))),/*#__PURE__*/React.createElement("i",{"data-lucide":"gauge",className:"w-3.5 h-3.5 text-slate-500"})),/*#__PURE__*/React.createElement("div",{className:"w-24 h-12 rounded-lg border border-dashed border-slate-800 flex items-center justify-center text-slate-600 hover:text-slate-400 hover:border-slate-600 cursor-pointer transition-all shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("main",{className:"flex-1 flex overflow-hidden min-h-0"},/*#__PURE__*/React.createElement("div",{className:"flex-1 flex flex-col bg-slate-950 relative overflow-y-auto oz-scrollbar"},/*#__PURE__*/React.createElement("div",{className:"h-9 bg-slate-900/60 border-b border-slate-800/80 px-4 flex items-center justify-between text-xs oz-font-mono shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400 flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"headphones",className:"w-3 h-3 text-cyan-400"})," Delta Listen"),/*#__PURE__*/React.createElement("select",{className:"bg-slate-950 border border-slate-800 rounded px-2 py-0.5 text-[11px] text-slate-300 outline-none"},/*#__PURE__*/React.createElement("option",{value:"irc4"},"IRC IV - Classic"),/*#__PURE__*/React.createElement("option",{value:"irc3"},"IRC III - Balanced"),/*#__PURE__*/React.createElement("option",{value:"irc2"},"IRC II - Crisp"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 text-[11px]"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Learn Input Gain:"),/*#__PURE__*/React.createElement("button",{className:"bg-cyan-950 text-cyan-300 border border-cyan-800/80 px-2 py-0.5 rounded font-bold hover:bg-cyan-900 transition-colors"},"-11.0 LUFS"))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='eq'?'':'hidden'}`},/*#__PURE__*/React.createElement("div",{className:"relative w-full h-64 bg-slate-950 border border-slate-800/90 rounded-xl overflow-hidden shadow-inner cursor-crosshair"},/*#__PURE__*/React.createElement("canvas",{ref:eqCanvasRef,className:"w-full h-full block"})),/*#__PURE__*/React.createElement("div",{className:"oz-panel p-3 rounded-xl grid grid-cols-4 gap-3"},bandKnob('eqLowGain',-12,12,1.5,'dB','BAND 1 (LOW)','100 Hz','#22d3ee','Shelf Filter'),bandKnob('eqMid1Gain',-12,12,-1.0,'dB','BAND 2 (MID LOW)','822 Hz','#fbbf24','Dynamic Bell (Q: 0.7)'),bandKnob('eqMid2Gain',-12,12,2.0,'dB','BAND 3 (MID HIGH)','3.2 kHz','#a855f7','Dynamic Bell (Q: 1.2)'),bandKnob('eqHighGain',-12,12,1.8,'dB','BAND 4 (HIGH)','10 kHz','#34d399','High Shelf'))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='imager'?'':'hidden'}`},/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-12 gap-4 flex-1"},/*#__PURE__*/React.createElement("div",{className:"col-span-8 bg-slate-950 border border-slate-800/90 rounded-xl p-3 flex flex-col relative shadow-inner"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-cyan-400 uppercase oz-font-mono mb-2"},/*#__PURE__*/React.createElement("i",{"data-lucide":"radio",className:"w-3.5 h-3.5"})," Polar Vectorscope & Correlation Meter"),/*#__PURE__*/React.createElement("div",{className:"flex-1 relative w-full h-56 flex items-center justify-center"},/*#__PURE__*/React.createElement("canvas",{ref:imagerCanvasRef,className:"w-full h-full block"}))),/*#__PURE__*/React.createElement("div",{className:"col-span-4 oz-panel p-3 rounded-xl flex flex-col justify-between"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-slate-300 uppercase oz-font-mono"},"4-Band Stereo Width"),/*#__PURE__*/React.createElement("div",{className:"space-y-3 my-auto"},[{id:'w1',label:'Band 1 (0-100Hz)',color:'#22d3ee',val:ozState.w1},{id:'w2',label:'Band 2 (100-1kHz)',color:'#fbbf24',val:ozState.w2},{id:'w3',label:'Band 3 (1k-6kHz)',color:'#a855f7',val:ozState.w3},{id:'w4',label:'Band 4 (6k-20kHz)',color:'#34d399',val:ozState.w4}].map(b=>/*#__PURE__*/React.createElement("div",{key:b.id},/*#__PURE__*/React.createElement("div",{className:"flex justify-between text-[11px] font-mono mb-1"},/*#__PURE__*/React.createElement("span",{style:{color:b.color,fontWeight:700}},b.label),/*#__PURE__*/React.createElement("span",{id:b.id+'Val'},b.val,"%")),/*#__PURE__*/React.createElement("input",{type:"range",min:"-100",max:"100",value:b.val,onChange:e=>setOzState(prev=>({...prev,[b.id]:parseInt(e.target.value)})),className:"w-full h-1 cursor-pointer",style:{accentColor:b.color}}))))))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='maximizer'?'':'hidden'}`},/*#__PURE__*/React.createElement("div",{className:"oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center"},/*#__PURE__*/React.createElement("div",{className:"col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-slate-400 uppercase oz-font-mono mb-3"},"Maximizer Gain Boost"),/*#__PURE__*/React.createElement("div",{className:"my-2"},/*#__PURE__*/React.createElement(MasteringKnob,{param:"maxGain",min:0,max:12,value:ozState.maxGain,unit:"dB",color:"#22d3ee",size:"large",onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))})),/*#__PURE__*/React.createElement("div",{className:"w-full mt-4 bg-slate-950 p-2 rounded-lg border border-slate-800 text-xs oz-font-mono flex flex-col gap-1.5"},/*#__PURE__*/React.createElement("div",{className:"flex justify-between items-center w-full"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Ceiling Level:"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-bold"},ozState.ceiling.toFixed(2)," dB")),/*#__PURE__*/React.createElement("input",{type:"range",min:"-12",max:"0",step:"0.1",value:ozState.ceiling,onChange:e=>setOzState(prev=>({...prev,ceiling:parseFloat(e.target.value)})),className:"w-full h-1 cursor-pointer accent-cyan-400"}))),/*#__PURE__*/React.createElement("div",{className:"col-span-8 grid grid-cols-3 gap-4"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full"},/*#__PURE__*/React.createElement(MasteringKnob,{param:"maxUpward",min:0,max:10,value:ozState.maxUpward,unit:"dB",label:"UPWARD COMPRESS",color:"#22d3ee",onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))})),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full"},/*#__PURE__*/React.createElement(MasteringKnob,{param:"maxSoftClip",min:0,max:100,value:ozState.maxSoftClip,unit:"%",label:"SOFT CLIPPER",color:"#fbbf24",onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))})),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full"},/*#__PURE__*/React.createElement(MasteringKnob,{param:"maxTransient",min:0,max:100,value:ozState.maxTransient,unit:"%",label:"TRANSIENT EMPHASIS",color:"#34d399",onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))}))))),/*#__PURE__*/React.createElement("div",{className:"border-t border-slate-800 bg-slate-900/60 p-3 flex flex-col shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between pb-1.5 border-b border-slate-800/80 mb-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-[11px] font-bold text-white uppercase tracking-wider"},"Wave Observer"),/*#__PURE__*/React.createElement("span",{className:"text-[9px] bg-cyan-950 text-cyan-400 border border-cyan-800/60 px-1.5 py-0.2 rounded font-mono"},"Real-time Oscilloscope")),/*#__PURE__*/React.createElement("div",{className:"flex gap-2"},['Scope','Settings','Help','About'].map(tab=>/*#__PURE__*/React.createElement("button",{key:tab,className:`px-2 py-0.5 rounded text-[10px] font-bold ${tab==='Scope'?'bg-cyan-950 text-cyan-300 border border-cyan-800/60':'text-slate-400 hover:text-slate-200'}`},tab)))),/*#__PURE__*/React.createElement("div",{className:"relative w-full h-32 bg-[#090d16] border border-slate-800/80 rounded-lg overflow-hidden mb-2"},/*#__PURE__*/React.createElement("canvas",{ref:woCanvasRef,className:"w-full h-full block"})),/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between gap-4 text-[10px] oz-font-mono text-slate-400 mt-1 select-none"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 bg-slate-950 px-2.5 py-1.5 rounded border border-slate-800/80 shrink-0"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-slate-300"},"Input"),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1 w-20"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-[8px] text-slate-500 w-2"},"L"),/*#__PURE__*/React.createElement("div",{className:"w-16 h-1.5 bg-slate-900 rounded-sm overflow-hidden flex items-center"},/*#__PURE__*/React.createElement("div",{ref:woLeftMeterRef,className:"h-full bg-cyan-400 transition-all duration-75",style:{width:'0%'}}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-[8px] text-slate-500 w-2"},"R"),/*#__PURE__*/React.createElement("div",{className:"w-16 h-1.5 bg-slate-900 rounded-sm overflow-hidden flex items-center"},/*#__PURE__*/React.createElement("div",{ref:woRightMeterRef,className:"h-full bg-teal-500 transition-all duration-75",style:{width:'0%'}}))))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-4 bg-slate-950 px-3 py-1 rounded border border-slate-800/80 flex-1 justify-around"},/*#__PURE__*/React.createElement("span",{className:"font-bold text-slate-300 uppercase tracking-widest text-[9px]"},"Scope"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",null,"Channel"),/*#__PURE__*/React.createElement("select",{value:woChannel,onChange:e=>setWoChannel(e.target.value),className:"bg-slate-900 border border-slate-700 rounded px-1.5 py-0.5 text-[9px] text-slate-300 outline-none"},/*#__PURE__*/React.createElement("option",{value:"stereo"},"Stereo"),/*#__PURE__*/React.createElement("option",{value:"left"},"Left Only"),/*#__PURE__*/React.createElement("option",{value:"right"},"Right Only"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",null,"Mode"),/*#__PURE__*/React.createElement("select",{value:woMode,onChange:e=>setWoMode(e.target.value),className:"bg-slate-900 border border-slate-700 rounded px-1.5 py-0.5 text-[9px] text-slate-300 outline-none"},/*#__PURE__*/React.createElement("option",{value:"waveform"},"Waveform"),/*#__PURE__*/React.createElement("option",{value:"envelope"},"Envelope"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",null,"Duration:"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-bold w-10 text-right"},woDuration.toFixed(3),"s"),/*#__PURE__*/React.createElement("input",{type:"range",min:"0.5",max:"5.0",step:"0.1",value:woDuration,onChange:e=>setWoDuration(parseFloat(e.target.value)),className:"w-16 h-1 cursor-pointer accent-cyan-400"})),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",null,"V.Zoom:"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-bold w-12 text-right"},woZoom.toFixed(1)," dB"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-12",max:"24",step:"0.5",value:woZoom,onChange:e=>setWoZoom(parseFloat(e.target.value)),className:"w-16 h-1 cursor-pointer accent-cyan-400"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>setWoPaused(!woPaused),className:`px-3 py-1 rounded font-bold border transition shadow-md shrink-0 flex items-center gap-1 ${woPaused?'bg-amber-800 hover:bg-amber-700 border-amber-500 text-amber-100':'bg-slate-800 hover:bg-slate-700 border-slate-700 text-slate-200'}`},woPaused?'Resume':'Pause')))),/*#__PURE__*/React.createElement("div",{className:"w-64 bg-slate-900 border-l border-slate-800/80 p-3 flex flex-col justify-between shadow-2xl oz-font-mono text-xs z-20 shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-800 pb-2 mb-2"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-white tracking-wider uppercase flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"sliders",className:"w-3 h-3 text-cyan-400"})," I/O METERS"),/*#__PURE__*/React.createElement("span",{className:"text-[9px] bg-cyan-950 border border-cyan-800 text-cyan-300 px-1 py-0.5 rounded"},"TRUE PEAK")),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2 text-[10px] text-center mb-2"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800/80 p-1.5 rounded"},/*#__PURE__*/React.createElement("div",{className:"text-slate-500 font-bold"},"IN PEAK"),/*#__PURE__*/React.createElement("div",{id:"inPeakText",className:"text-cyan-400 font-bold oz-font-mono"},"-inf dB")),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800/80 p-1.5 rounded"},/*#__PURE__*/React.createElement("div",{className:"text-slate-500 font-bold"},"OUT PEAK"),/*#__PURE__*/React.createElement("div",{id:"outPeakText",className:"text-emerald-400 font-bold oz-font-mono"},"-inf dB"))),/*#__PURE__*/React.createElement("div",{className:"flex-1 bg-slate-950 border border-slate-800/90 rounded-xl p-2 flex justify-around relative overflow-hidden my-1"},/*#__PURE__*/React.createElement("div",{className:"flex flex-col items-center h-full"},/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-slate-500 mb-1"},"IN"),/*#__PURE__*/React.createElement("div",{className:"flex-1 w-5 bg-slate-900 border border-slate-800 rounded relative overflow-hidden flex items-end"},/*#__PURE__*/React.createElement("canvas",{ref:inMeterCanvasRef,className:"w-full h-full block"}))),/*#__PURE__*/React.createElement("div",{className:"flex flex-col items-center h-full"},/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-slate-500 mb-1"},"OUT"),/*#__PURE__*/React.createElement("div",{className:"flex-1 w-6 bg-slate-900 border border-slate-800 rounded relative overflow-hidden flex items-end"},/*#__PURE__*/React.createElement("canvas",{ref:outMeterCanvasRef,className:"w-full h-full block"})))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-1.5 mt-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>setOzState(prev=>({...prev,isBypassed:!prev.isBypassed})),className:`py-1.5 rounded text-[10px] font-bold border transition-colors ${ozState.isBypassed?'bg-cyan-600 border-cyan-500 text-white':'bg-slate-800 hover:bg-slate-700 text-slate-200 border-slate-700'}`},"Bypass"),/*#__PURE__*/React.createElement("button",{className:"bg-slate-800 hover:bg-slate-700 text-slate-200 py-1.5 rounded text-[10px] font-bold border border-slate-700 transition-colors"},"Gain Match"),/*#__PURE__*/React.createElement("button",{className:"bg-slate-800 hover:bg-slate-700 text-slate-300 py-1.5 rounded text-[10px] border border-slate-700"},"Codec"),/*#__PURE__*/React.createElement("button",{className:"bg-slate-800 hover:bg-slate-700 text-slate-300 py-1.5 rounded text-[10px] border border-slate-700"},"Dither"))))));};// ── Media Explorer Panel (from md/51_MEDIA_EXPLORER.md) ──
-const MEDIA_LIBRARY_SAMPLES=[{name:"MIDI_Loop_01.mid",events:95,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_02_Bass.mid",events:48,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_03_Lead.mid",events:110,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_04.mid",events:76,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_05_Bass.mid",events:52,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_06.mid",events:88,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"}];const MediaExplorerPanel=({height})=>{const[userFiles,setUserFiles]=React.useState([]);const[folder,setFolder]=React.useState('library');const[selected,setSelected]=React.useState(null);const[filterText,setFilterText]=React.useState('');const[viewMode,setViewMode]=React.useState('details');const[isPlaying,setIsPlaying]=React.useState(false);const[isPaused,setIsPaused]=React.useState(false);const[isLooping,setIsLooping]=React.useState(false);const[autoPlay,setAutoPlay]=React.useState(true);const[pitch,setPitch]=React.useState(0.0);const[rate,setRate]=React.useState(1.0);const[volumeDb,setVolumeDb]=React.useState(0.0);const[currentTime,setCurrentTime]=React.useState(0);const[peaks,setPeaks]=React.useState(null);const[audioBuffer,setAudioBuffer]=React.useState(null);const[midiNotes,setMidiNotes]=React.useState(null);const[midiTotal,setMidiTotal]=React.useState(4);const[midiBars,setMidiBars]=React.useState(1);const[midiTotalBeats,setMidiTotalBeats]=React.useState(16);const[midiFileBpm,setMidiFileBpm]=React.useState(120);const[tempo,setTempo]=React.useState(function(){var saved=localStorage.getItem('studio_media_explorer_tempo');return saved?parseInt(saved):120;}());// Refs mirror latest state so drawCanvas (also called from rAF clock with a
+const MEDIA_LIBRARY_SAMPLES=[{name:"MIDI_Loop_01.mid",events:95,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_02_Bass.mid",events:48,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_03_Lead.mid",events:110,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_04.mid",events:76,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_05_Bass.mid",events:52,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_06.mid",events:88,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"}];const MediaExplorerPanel=({height})=>{const[userFiles,setUserFiles]=React.useState([]);const[folder,setFolder]=React.useState('library');const[selected,setSelected]=React.useState(null);const[filterText,setFilterText]=React.useState('');const[viewMode,setViewMode]=React.useState('details');const[isPlaying,setIsPlaying]=React.useState(false);const[isPaused,setIsPaused]=React.useState(false);const[isLooping,setIsLooping]=React.useState(false);const[autoPlay,setAutoPlay]=React.useState(true);const[pitch,setPitch]=React.useState(0.0);const[rate,setRate]=React.useState(1.0);const[volumeDb,setVolumeDb]=React.useState(0.0);const[currentTime,setCurrentTime]=React.useState(0);const[peaks,setPeaks]=React.useState(null);const[audioBuffer,setAudioBuffer]=React.useState(null);const[audioDuration,setAudioDuration]=React.useState(0);const[midiNotes,setMidiNotes]=React.useState(null);const[midiTotal,setMidiTotal]=React.useState(4);const[midiBars,setMidiBars]=React.useState(1);const[midiTotalBeats,setMidiTotalBeats]=React.useState(16);const[midiFileBpm,setMidiFileBpm]=React.useState(120);const[tempo,setTempo]=React.useState(function(){var saved=localStorage.getItem('studio_media_explorer_tempo');return saved?parseInt(saved):120;}());// Refs mirror latest state so drawCanvas (also called from rAF clock with a
// 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 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;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[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
+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[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 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||(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);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||[]);}catch(e){setPeaks(null);}};const openMyComputer=async()=>{setFolder('computer');// Restore last opened folder from session (click My Computer → show folder content directly)
+},[]);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');// 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){}if(window.showDirectoryPicker){try{const handle=await window.showDirectoryPicker({mode:'read'});setComputerMode('client');setClientRoot(handle);const rootEntry={name:handle.name,path:'root',is_dir:true,handle};setComputerRoots([rootEntry]);setComputerPath('root');setComputerFiles([]);setComputerTree({root:{handle,parent:null,dirs:[],expanded:true}});await browseComputerDir(rootEntry);return;}catch(e){if(e&&e.name==='AbortError')return;window.showToast&&window.showToast('Không thể mở thư mục: '+e.message,'error');return;}}// Fallback: server API (trình duyệt không hỗ trợ File System Access API)
setComputerMode('server');if(computerRoots)return;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');}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;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}}));}catch(e){window.showToast&&window.showToast('Không thể đọc thư mục: '+e.message,'error');}};const browseComputerDir=async entry=>{if(!entry)return;if(computerMode==='client'||entry.handle){if(entry.handle&&entry.handle.entries){const parentInfo=computerTree[entry.path];await browseClientDir({...entry,parent:parentInfo?parentInfo.parent:null});return;}}const path=entry.path||entry;if(!path)return;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}}));}catch(e){window.showToast&&window.showToast('Không thể mở thư mục: '+e.message,'error');}};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);}};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
@@ -251,14 +251,14 @@ if(!f||!window.SonicSF)return;try{const buf=await readLocalFileBuffer(f);if(!buf
if(selectTokenRef.current!==(token||selectTokenRef.current))return;const prog=curInst&&curInst.program!==undefined?curInst.program:undefined;const eng=curInst?{soundfont_id:sfId,soundfont_bank:bank,soundfont_program:prog!==undefined?prog:0}:undefined;const totalSec=midiResult[0].duration||4;const allNotes=[];midiResult.forEach(track=>{(track.notes||[]).forEach(note=>{allNotes.push(Object.assign({},note,{trackOffset:track.startTime||0}));});});const schedulePass=passStartTime=>{allNotes.forEach(note=>{const startSec=(note.start_beat||0)*secondsPerBeat+(note.trackOffset||0);const durMs=Math.max(80,(note.duration_beats||1)*secondsPerBeat*1000);window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,passStartTime+startSec,prog,null,0,eng);});};schedulePass(startWallTime);// Loop scheduling
if(loopTimerRef.current){clearInterval(loopTimerRef.current);loopTimerRef.current=null;}if(isLooping){loopTimerRef.current=setInterval(()=>{if(selectTokenRef.current!==(token||selectTokenRef.current)){clearInterval(loopTimerRef.current);loopTimerRef.current=null;return;}if(!isPlayingRef.current||isPausedRef.current)return;const passStart=ctx.currentTime+0.05;schedulePass(passStart);playStateRef.current=Object.assign({},playStateRef.current,{startedAt:passStart,fakeStart:passStart});},Math.max(200,totalSec*1000));}// Keep a fake clock so canvas playhead animates; loop uses playStateRef
playStateRef.current={source:null,ctx,startedAt:startWallTime,fakeStart:startWallTime,midiTotal:totalSec};setMidiNotes(allNotes);setMidiTotal(totalSec);setMidiBars(midiResult[0].bars||1);setMidiTotalBeats(midiResult[0].totalBeats||16);setMidiFileBpm(midiResult[0].bpm||120);setIsPlaying(true);setIsPaused(false);startCanvasClock();}catch(e){console.error('MIDI preview failed',e);}};const stopMediaPlayback=React.useCallback(()=>{if(playStateRef.current){try{if(playStateRef.current.source)playStateRef.current.source.stop();}catch(e){}try{if(playStateRef.current.source)playStateRef.current.source.disconnect();}catch(e){}playStateRef.current=null;}if(rafRef.current)cancelAnimationFrame(rafRef.current);rafRef.current=null;if(loopTimerRef.current){clearInterval(loopTimerRef.current);loopTimerRef.current=null;}if(window.SonicSF&&typeof window.SonicSF.stopAll==='function'){try{window.SonicSF.stopAll();}catch(e){}}setIsPlaying(false);setIsPaused(false);},[]);const playSelected=async(f,token)=>{if(!f)return;stopMediaPlayback();if(isMidiFile(f)){if(f.handle||f.path||f.file_id||f.fileId){// Real MIDI file: play through selected synth instrument
-await playMidiPreview(f,token);return;}playStateRef.current={source:null,ctx:null,fakeStart:performance.now()/1000};setIsPlaying(true);setIsPaused(false);startCanvasClock();return;}const fid=f.file_id||f.fileId;const buf=await readLocalFileBuffer(f);if(selectTokenRef.current!==(token||selectTokenRef.current))return;if(!buf)return;try{const ctx=getAudioContext();const decoded=await ctx.decodeAudioData(buf);if(selectTokenRef.current!==(token||selectTokenRef.current))return;setAudioBuffer(decoded);const src=ctx.createBufferSource();src.buffer=decoded;src.loop=isLooping;src.playbackRate.value=rate;const gain=ctx.createGain();const linear=volumeDb<=-50?0:Math.pow(10,volumeDb/20);gain.gain.value=linear;src.connect(gain);gain.connect(ctx.destination);src.start();const startedAt=ctx.currentTime;playStateRef.current={source:src,ctx,startedAt};setIsPlaying(true);setIsPaused(false);startCanvasClock();}catch(e){console.error('Preview failed',e);}};const togglePause=()=>{const st=playStateRef.current;if(!st)return;if(isPaused){if(st.ctx)st.ctx.resume();setIsPaused(false);}else{if(st.ctx)st.ctx.suspend();setIsPaused(true);}};const startCanvasClock=()=>{if(rafRef.current)cancelAnimationFrame(rafRef.current);const tick=()=>{rafRef.current=requestAnimationFrame(tick);let t=currentTimeRef.current;const st=playStateRef.current;if(st&&!isPausedRef.current){t=st.ctx?st.ctx.currentTime-st.startedAt:performance.now()/1000-st.fakeStart;}setCurrentTime(t);currentTimeRef.current=t;drawCanvas(t);};tick();};const drawCanvas=t=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const w=canvas.clientWidth,h=canvas.clientHeight;if(!w||!h)return;canvas.width=w*2;canvas.height=h*2;ctx.scale(2,2);ctx.clearRect(0,0,w,h);ctx.fillStyle='#181818';ctx.fillRect(0,0,w,h);// Read from refs so the rAF clock's stale closure still draws the latest file
+await playMidiPreview(f,token);return;}playStateRef.current={source:null,ctx:null,fakeStart:performance.now()/1000};setIsPlaying(true);setIsPaused(false);startCanvasClock();return;}const fid=f.file_id||f.fileId;const buf=await readLocalFileBuffer(f);if(selectTokenRef.current!==(token||selectTokenRef.current))return;if(!buf)return;try{const ctx=getAudioContext();const decoded=await ctx.decodeAudioData(buf);if(selectTokenRef.current!==(token||selectTokenRef.current))return;setAudioBuffer(decoded);setAudioDuration(decoded.duration);const src=ctx.createBufferSource();src.buffer=decoded;src.loop=isLooping;src.playbackRate.value=rate;const gain=ctx.createGain();const linear=volumeDb<=-50?0:Math.pow(10,volumeDb/20);gain.gain.value=linear;src.connect(gain);gain.connect(ctx.destination);src.start();const startedAt=ctx.currentTime;playStateRef.current={source:src,ctx,startedAt};setIsPlaying(true);setIsPaused(false);startCanvasClock();}catch(e){console.error('Preview failed',e);}};const togglePause=()=>{const st=playStateRef.current;if(!st)return;if(isPaused){if(st.ctx)st.ctx.resume();setIsPaused(false);}else{if(st.ctx)st.ctx.suspend();setIsPaused(true);}};const startCanvasClock=()=>{if(rafRef.current)cancelAnimationFrame(rafRef.current);const tick=()=>{rafRef.current=requestAnimationFrame(tick);let t=currentTimeRef.current;const st=playStateRef.current;if(st&&!isPausedRef.current){t=st.ctx?st.ctx.currentTime-st.startedAt:performance.now()/1000-st.fakeStart;}setCurrentTime(t);currentTimeRef.current=t;drawCanvas(t);};tick();};const drawCanvas=t=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const w=canvas.clientWidth,h=canvas.clientHeight;if(!w||!h)return;canvas.width=w*2;canvas.height=h*2;ctx.scale(2,2);ctx.clearRect(0,0,w,h);ctx.fillStyle='#181818';ctx.fillRect(0,0,w,h);// Read from refs so the rAF clock's stale closure still draws the latest file
const f=selectedRef.current;const curPeaks=peaksRef.current;const curAudioBuffer=audioBufferRef.current;const curMidiNotes=midiNotesRef.current;const curMidiTotal=midiTotalRef.current;const curMidiBars=midiBarsRef.current;const curMidiTotalBeats=midiTotalBeatsRef.current;const curTempo=tempoRef.current;const playing=isPlayingRef.current;if(!f){ctx.fillStyle='#555';ctx.font='12px JetBrains Mono, monospace';ctx.fillText('No file selected',10,h/2);return;}const dur=fileDuration(f);if(isMidiFile(f)){ctx.strokeStyle='#333';for(let y=0;y0?curMidiTotalBeats:Math.max(curMidiTotal*bpmV/60,4);const beats=Math.max(totalBeats,4);const contentW=Math.max(w,beats*pxPerBeat);const pxPerSec=pxPerBeat*bpmV/60;let offset=0;if(playing&&contentW>w&&dur>0){offset=Math.max(0,Math.min(contentW-w,t*pxPerSec-w/2));}// bar lines (every 4 beats) + beat labels
for(let b=0;b<=beats;b+=4){const x=b*pxPerBeat-offset;if(x<-10||x>w+10)continue;ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,h-14);ctx.stroke();}ctx.fillStyle='#9ca3af';if(curMidiNotes&&curMidiNotes.length){// Real piano-roll: rows = pitches (48..84), columns = beats
const pitchMin=48,pitchMax=84;const pitchRange=Math.max(1,pitchMax-pitchMin);curMidiNotes.forEach(n=>{const x=n.start_beat*pxPerBeat-offset;if(x<-30||x>w+30)return;const nw=Math.max(3,(n.duration_beats||1)*pxPerBeat);const y=h-14-8-(Math.min(pitchMax,Math.max(pitchMin,n.pitch))-pitchMin)/pitchRange*(h-30);ctx.fillRect(x,y,nw,5);});}else{const events=f.events||76;for(let i=0;iw?Math.min(w/2,t*pxPerSec):Math.min(w,t*pxPerSec);if(playing&&dur>0){ctx.fillStyle='#ef4444';ctx.fillRect(midiPlayheadX-1,0,2,h-14);}}else{const pk=curPeaks&&curPeaks.length>0?curPeaks:null;if(pk){const pxPerSec=50;const contentW=Math.max(w,dur*pxPerSec);let offset=0;if(playing&&contentW>w&&dur>0){offset=Math.max(0,Math.min(contentW-w,t*pxPerSec-w/2));}const mid=h/2;ctx.fillStyle='#22c55e';for(let i=0;iw+3)continue;const ph=Math.max(2,pk[i]*(h/2-4));ctx.fillRect(x,mid-ph,Math.max(1,w/pk.length),ph*2);}const audioPlayheadX=contentW>w?Math.min(w/2,t*pxPerSec):Math.min(w,t*pxPerSec);if(playing&&dur>0){ctx.fillStyle='#ef4444';ctx.fillRect(audioPlayheadX-1,0,2,h-14);}}else{ctx.fillStyle='#666';ctx.font='11px monospace';ctx.fillText('Waveform unavailable',10,h/2);}}// ruler
-ctx.fillStyle='#111';ctx.fillRect(0,h-14,w,14);ctx.fillStyle='#888';ctx.font='9px JetBrains Mono, monospace';const rulerBpm=curTempo||120;const rulerPxPerBeat=isMidiFile(f)?42:50*60/rulerBpm;const isRealMidi=isMidiFile(f)&&curMidiNotes&&curMidiNotes.length&&curMidiTotalBeats>0;const rulerTotalBeats=isRealMidi?Math.max(curMidiTotalBeats,4):Math.max(4,Math.ceil(dur*rulerBpm/60)||16);const rulerContentW=isRealMidi?Math.max(w,rulerTotalBeats*rulerPxPerBeat):Math.max(w,dur*rulerPxPerBeat*rulerBpm/60);const rulerPxPerSec=rulerPxPerBeat*rulerBpm/60;const rulerOffset=playing&&rulerContentW>w&&dur>0?Math.max(0,Math.min(rulerContentW-w,t*rulerPxPerSec-w/2)):0;for(let b=0;b<=rulerTotalBeats;b+=4){const x=b*rulerPxPerBeat-rulerOffset;if(x<-20||x>w+20)continue;ctx.fillText(String(Math.floor(b/4)),x+2,h-3);}};React.useEffect(()=>{drawCanvas(currentTime);},[peaks,audioBuffer,midiNotes,selected,folder,isPlaying]);React.useEffect(()=>()=>{if(rafRef.current)cancelAnimationFrame(rafRef.current);},[]);const handleSelect=f=>{if(!f||f.is_dir)return;selectTokenRef.current++;const token=selectTokenRef.current;setSelected(f);setCurrentTime(0);setPeaks(null);setAudioBuffer(null);setMidiNotes(null);stopMediaPlayback();if(f.kind==='other')return;if(autoPlay){playSelected(f,token);}if(!isMidiFile(f)&&(f.path||f.file_id||f.fileId))loadWaveform(f);};const renderComputerNode=(entry,depth,isRoot)=>{const nodePath=entry.path;const node=computerTree[nodePath];const expanded=node&&node.expanded;const dirs=node?node.dirs:[];const pad=12+depth*12;return/*#__PURE__*/React.createElement(React.Fragment,{key:nodePath},/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${computerPath===nodePath?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,style:{paddingLeft:pad},onClick:()=>browseComputerDir(entry),onDoubleClick:e=>{e.stopPropagation();toggleComputerDir(entry);}},/*#__PURE__*/React.createElement("i",{className:`fa-solid ${expanded?'fa-minus':'fa-plus'} text-[9px] text-slate-500 w-3 text-center shrink-0`,onClick:e=>{e.stopPropagation();toggleComputerDir(entry);}}),/*#__PURE__*/React.createElement("i",{className:`fa-solid ${isRoot?'fa-hard-drive text-[#6ea8dc]':'fa-folder text-[#d9a752]'} shrink-0`}),/*#__PURE__*/React.createElement("span",{className:"truncate"},entry.name)),expanded&&dirs.map(d=>renderComputerNode(d,depth+1,false)));};const toggleLoop=()=>{setIsLooping(prev=>{const next=!prev;if(playStateRef.current&&playStateRef.current.source)playStateRef.current.source.loop=next;if(next&&isMidiFile(selectedRef.current)){// Re-schedule loop for the currently previewing MIDI file
-const cur=selectedRef.current;if(cur&&(cur.handle||cur.path||cur.file_id||cur.fileId)){selectTokenRef.current++;const token=selectTokenRef.current;try{if(window.SonicSF&&typeof window.SonicSF.stopAll==='function')window.SonicSF.stopAll();}catch(e){}playMidiPreview(cur,token);}}return next;});};const toggleRate=dir=>setRate(prev=>Math.max(0.25,Math.min(4.0,Math.round((prev+dir*0.1)*100)/100)));const togglePitch=dir=>setPitch(prev=>Math.max(-24,Math.min(24,prev+dir*0.5)));const selIsMidi=isMidiFile(selected);const selDur=fileDuration(selected);const selBpm=selected&&selected.bpm||tempo;return/*#__PURE__*/React.createElement("div",{className:"flex flex-col w-full h-full text-slate-900 overflow-hidden select-none",style:{fontFamily:"'Inter', sans-serif"}},/*#__PURE__*/React.createElement("div",{className:"h-8 bg-[#d4d0c8] border-b border-[#808080] px-2 flex items-center justify-between text-xs shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 flex-1 mr-2"},/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]",title:"Back",onClick:()=>folder==='computer'?goComputerParent():setFolder('library')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-arrow-left"})),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]",title:"Forward",onClick:()=>setFolder('uploads')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-arrow-right"})),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]",title:"Up Directory",onClick:()=>folder==='computer'?goComputerParent():setFolder('library')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-arrow-up"})),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]",title:"Refresh",onClick:()=>{if(folder==='computer'){const node=computerTree[computerPath];if(node&&node.handle)browseComputerDir({name:computerPath.split('/').pop()||computerPath,path:computerPath,is_dir:true,handle:node.handle});else if(computerPath)browseComputerDir({name:computerPath.split(/[\\/]/).pop()||computerPath,path:computerPath,is_dir:true});}else if(window.SonicAPI&&window.SonicAPI.listMyFiles)window.SonicAPI.listMyFiles([]).then(d=>setUserFiles(d||[]));}},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-rotate-right"})),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex items-center bg-white border border-[#808080] h-5 px-1"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder-open text-[#d9a752] text-[11px] mr-1.5"}),/*#__PURE__*/React.createElement("span",{className:"flex-1 text-xs text-slate-800 truncate"},folder==='computer'?computerPath||'My Computer':folder==='library'?'Media Library':folder==='uploads'?'Uploads':'Processed'),/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-caret-down text-slate-600 text-[10px] ml-1"}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center bg-white border border-[#808080] h-5 px-1 w-40"},/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"Filter/Search...",value:filterText,onChange:e=>setFilterText(e.target.value),className:"w-full text-xs outline-none bg-transparent font-sans text-slate-800"}),/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-caret-down text-slate-600 text-[10px] ml-1"})),/*#__PURE__*/React.createElement("button",{className:"px-2 py-0.5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[11px] flex items-center gap-1 font-semibold",onClick:()=>setViewMode(viewMode==='details'?'list':'details')},/*#__PURE__*/React.createElement("span",null,viewMode==='details'?'Details':'List')," ",/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-caret-down text-[9px]"})))),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0"},/*#__PURE__*/React.createElement("div",{className:"flex bg-white border border-[#808080] m-1 mr-0 text-xs select-none shrink-0 overflow-hidden"},/*#__PURE__*/React.createElement("div",{className:"overflow-y-auto p-1",style:{width:treeWidth+'px',minWidth:treeWidth+'px',maxWidth:treeWidth+'px'}},/*#__PURE__*/React.createElement("div",{className:"space-y-0.5 font-sans"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 px-1 py-0.5 text-slate-700"},/*#__PURE__*/React.createElement("span",{className:"w-3"}),"