From 9be69912228c2f7726acdcbd6d75682964767347 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Sun, 2 Aug 2026 18:20:48 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20canvas=20audioclip=20hi=E1=BB=83n=20th?= =?UTF-8?q?=E1=BB=8B=20theo=20t=E1=BB=89=20l=E1=BB=87=20tempo,=20kh=C3=B4n?= =?UTF-8?q?g=20gi=C3=A3n=20khung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 17 +++++++++++------ app/static/js/app.precompiled.js | 7 +++++-- app/templates/index.html | 2 +- wiki.md | 5 +++++ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 1f7d963..2875f43 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -9759,8 +9759,11 @@ const MediaExplorerPanel = ({ height }) => { } else { const pk = curPeaks && curPeaks.length > 0 ? curPeaks : null; if (pk) { - const pxPerSec = 50; - const contentW = Math.max(w, dur * pxPerSec); + // Audio hiển thị theo tỉ lệ tempo (pxPerSec), không giãn đầy khung preview + const audioBpm = curTempo || 120; + const pxPerBeat = 42; + const pxPerSec = pxPerBeat * audioBpm / 60; + const contentW = Math.max(1, dur * pxPerSec); let offset = 0; if (playing && contentW > w && dur > 0) { offset = Math.max(0, Math.min(contentW - w, t * pxPerSec - w / 2)); @@ -9774,7 +9777,8 @@ const MediaExplorerPanel = ({ height }) => { const ph = Math.max(2, pk[i] * (h / 2 - 4)); ctx.fillRect(x, mid - ph, barW, ph * 2); } - const audioPlayheadX = contentW > w ? Math.min(w / 2, t * pxPerSec) : Math.min(w, t * pxPerSec); + // Playhead theo tỉ lệ tempo; khi content rộng hơn frame thì khóa giữa + scroll + const audioPlayheadX = contentW > w ? Math.min(w / 2, t * pxPerSec) : (t * pxPerSec); if (playing && dur > 0) { ctx.fillStyle = '#ef4444'; ctx.fillRect(audioPlayheadX - 1, 0, 2, h - 14); @@ -9788,10 +9792,11 @@ const MediaExplorerPanel = ({ height }) => { 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 rulerPxPerBeat = 42; 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); + // Tỉ lệ theo tempo (không giãn đầy khung) + const rulerContentW = Math.max(1, rulerTotalBeats * rulerPxPerBeat); 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) { @@ -9801,7 +9806,7 @@ const MediaExplorerPanel = ({ height }) => { } }; - React.useEffect(() => { drawCanvas(currentTime); }, [peaks, audioBuffer, midiNotes, selected, folder, isPlaying]); + React.useEffect(() => { drawCanvas(currentTime); }, [peaks, audioBuffer, audioDuration, midiNotes, selected, folder, isPlaying]); React.useEffect(() => () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); }, []); const handleSelect = (f) => { diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index ecbbf99..6e189dc 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -256,8 +256,11 @@ const f=selectedRef.current;const curPeaks=peaksRef.current;const curAudioBuffer const bpmV=curTempo||120;const pxPerBeat=42;const totalBeats=curMidiNotes&&curMidiNotes.length&&curMidiTotalBeats>0?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';const barW=Math.max(1,contentW/pk.length);for(let i=0;iw+3)continue;const ph=Math.max(2,pk[i]*(h/2-4));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){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);setAudioDuration(0);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 midiPlayheadX=contentW>w?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){// Audio hiển thị theo tỉ lệ tempo (pxPerSec), không giãn đầy khung preview +const audioBpm=curTempo||120;const pxPerBeat=42;const pxPerSec=pxPerBeat*audioBpm/60;const contentW=Math.max(1,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';const barW=Math.max(1,contentW/pk.length);for(let i=0;iw+3)continue;const ph=Math.max(2,pk[i]*(h/2-4));ctx.fillRect(x,mid-ph,barW,ph*2);}// Playhead theo tỉ lệ tempo; khi content rộng hơn frame thì khóa giữa + scroll +const audioPlayheadX=contentW>w?Math.min(w/2,t*pxPerSec):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=42;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);// Tỉ lệ theo tempo (không giãn đầy khung) +const rulerContentW=Math.max(1,rulerTotalBeats*rulerPxPerBeat);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,audioDuration,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);setAudioDuration(0);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"})," "),/*#__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"})," "),/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${folder==='computer'?'bg-slate-300 text-slate-900 font-semibold':'hover:bg-slate-200 text-slate-800'}`,onClick:openMyComputer},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-computer text-[11px] text-slate-600"})," My Computer"),folder==='computer'&&computerRoots&&/*#__PURE__*/React.createElement("div",{className:"pl-3 space-y-0.5"},computerRoots.map(root=>renderComputerNode(root,0,true))),/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer font-semibold rounded-sm ${folder==='library'?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,onClick:()=>setFolder('library')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder-open text-[#d9a752]"})," Media Library"),/*#__PURE__*/React.createElement("div",{className:"pl-3 space-y-0.5"},/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer pl-4 rounded-sm ${folder==='uploads'?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,onClick:()=>setFolder('uploads')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder text-[#d9a752]"})," Uploads"),/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer pl-4 rounded-sm ${folder==='processed'?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,onClick:()=>setFolder('processed')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder text-[#d9a752]"})," Processed")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 px-1 py-0.5 text-slate-700"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-plus text-[9px] text-slate-500"})," Desktop"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 px-1 py-0.5 text-slate-700"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-plus text-[9px] text-slate-500"})," My Documents"))),/*#__PURE__*/React.createElement("div",{className:"w-1.5 cursor-ew-resize hover:bg-blue-500/40 active:bg-blue-500/60 transition-colors shrink-0",onMouseDown:startTreeResize,title:"Kéo để thay đổi chiều rộng"})),/*#__PURE__*/React.createElement("div",{className:"flex-1 bg-white border border-[#808080] m-1 overflow-y-auto relative"},/*#__PURE__*/React.createElement("table",{className:"w-full text-xs text-left border-collapse"},/*#__PURE__*/React.createElement("thead",{className:"sticky top-0 bg-[#e0e0e0] border-b border-[#a0a0a0] text-slate-800 font-semibold select-none shadow-sm z-10"},/*#__PURE__*/React.createElement("tr",null,/*#__PURE__*/React.createElement("th",{className:"py-1 px-2 border-r border-[#b0b0b0]"},"File"),viewMode==='details'&&/*#__PURE__*/React.createElement("th",{className:"py-1 px-2 border-r border-[#b0b0b0]"},"Size"))),/*#__PURE__*/React.createElement("tbody",{className:"font-sans text-slate-800"},visibleFiles.map((f,i)=>{const isSel=selected&&(selected.name||selected.file_id)===(f.name||f.file_id);const isMidi=isMidiFile(f);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/*#__PURE__*/React.createElement("tr",{key:(f.path||f.file_id||f.name)+i,draggable:!f.is_dir,onDragStart:e=>{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)},/*#__PURE__*/React.createElement("td",{className:"py-1 px-2"},/*#__PURE__*/React.createElement("i",{className:`fa-solid ${icon} mr-2`}),f.name||f.original_name),viewMode==='details'&&/*#__PURE__*/React.createElement("td",{className:"py-1 px-2"},f.size_mb!=null?f.size_mb.toFixed(2)+' MB':isMidi?(f.tpqn||'MIDI')+' TPQN':'-'));}),visibleFiles.length===0&&/*#__PURE__*/React.createElement("tr",null,/*#__PURE__*/React.createElement("td",{className:"py-3 px-2 text-slate-400 italic",colSpan:viewMode==='details'?2:1},"No files")))))),/*#__PURE__*/React.createElement("div",{className:"h-[38%] min-h-[110px] bg-[#d4d0c8] border-t border-[#808080] p-1.5 flex flex-col justify-between text-xs shrink-0 select-none"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between gap-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{id:"btnStop",className:"w-6 h-6 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-slate-800",title:"Stop",onClick:stopMediaPlayback},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-square text-[10px]"})),/*#__PURE__*/React.createElement("button",{id:"btnPlay",className:"w-6 h-6 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-emerald-700 font-bold",title:"Play",onClick:()=>isPlaying?togglePause():playSelected(selected)},/*#__PURE__*/React.createElement("i",{className:`fa-solid ${isPlaying&&!isPaused?'fa-play':'fa-play'} text-xs`})),/*#__PURE__*/React.createElement("button",{id:"btnPause",className:"w-6 h-6 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-amber-700",title:"Pause",onClick:togglePause},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-pause text-xs"})),/*#__PURE__*/React.createElement("button",{id:"btnLoop",className:`w-6 h-6 border rounded-sm flex items-center justify-center text-xs ${isLooping?'bg-cyan-600 text-white border-cyan-700':'bg-[#e0e0e0] hover:bg-white text-slate-700 border-[#707070]'}`,title:"Loop / Repeat",onClick:toggleLoop},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-rotate-right"})),/*#__PURE__*/React.createElement("button",{id:"btnAutoPlay",className:`h-6 px-2 border rounded-sm font-bold text-[10px] flex items-center gap-1 ${autoPlay?'bg-gradient-to-r from-cyan-600 to-emerald-600 text-white border-slate-700':'bg-[#e0e0e0] text-slate-600 border-[#707070]'}`,onClick:()=>setAutoPlay(p=>!p)},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-bolt text-[9px]"})," ",/*#__PURE__*/React.createElement("span",null,"Auto-Play")),/*#__PURE__*/React.createElement("div",{className:"relative"},/*#__PURE__*/React.createElement("button",{id:"btnSynth",className:`h-6 px-2 border rounded-sm font-bold text-[10px] flex items-center gap-1 ${synthInst?'bg-gradient-to-r from-violet-600 to-fuchsia-600 text-white border-slate-700':'bg-[#e0e0e0] text-slate-600 border-[#707070]'}`,title:"Chọn instrument để preview MIDI",onClick:toggleSynthDropdown},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-music text-[9px]"})," ",/*#__PURE__*/React.createElement("span",null,"Synth",synthInst?': '+(synthInst.name||'?'):''),/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-caret-down text-[8px]"})),synthOpen&&/*#__PURE__*/React.createElement("div",{className:"absolute bottom-8 left-0 z-40 w-64 bg-white border border-[#808080] shadow-xl rounded-sm text-xs text-slate-800 font-sans max-h-72 overflow-y-auto"},/*#__PURE__*/React.createElement("div",{className:"sticky top-0 bg-[#e0e0e0] px-2 py-1 font-bold border-b border-[#a0a0a0] flex items-center justify-between"},/*#__PURE__*/React.createElement("span",null,"Select Instrument"),/*#__PURE__*/React.createElement("button",{onClick:()=>setSynthOpen(false),className:"text-slate-500 hover:text-slate-900"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-xmark"}))),synthLoading&&/*#__PURE__*/React.createElement("div",{className:"px-2 py-2 text-slate-400 italic"},"Loading..."),!synthLoading&&(!synthList||synthList.length===0)&&/*#__PURE__*/React.createElement("div",{className:"px-2 py-2 text-slate-400 italic"},"Không có SoundFont nào"),/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-2 py-1 cursor-pointer hover:bg-blue-100 ${!synthInst?'bg-slate-200':''}`,onClick:()=>selectSynthInst(null)},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-ban text-slate-400"})," None (mặc định)"),!synthLoading&&synthList&&synthList.map(group=>/*#__PURE__*/React.createElement("div",{key:group.sf.id||group.sf.name},/*#__PURE__*/React.createElement("div",{className:"px-2 py-1 bg-[#f0f0f0] font-semibold text-slate-600 border-t border-[#e0e0e0] truncate"},group.sf.display||group.sf.name||group.sf.id),(group.presets||[]).slice(0,200).map((p,pi)=>{const progId=p.id||p.name||'preset_'+pi;return/*#__PURE__*/React.createElement("div",{key:progId,className:`flex items-center gap-1 px-2 pl-4 py-0.5 cursor-pointer hover:bg-blue-100 truncate ${synthInst&&synthInst.program===p.program&&synthInst.sfId===(group.sf.id||group.sf.name)?'bg-slate-200':''}`,onClick:()=>selectSynthInst({sfId:group.sf.id,sfName:group.sf.display||group.sf.name||group.sf.id,bank:p.bank||0,program:p.program,name:p.name||'Program '+p.program})},p.bank===128?/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-drum text-slate-400"}):/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-music text-slate-400"})," ",p.name||'Program '+p.program);}))))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1",title:"Tempo preview MIDI"},/*#__PURE__*/React.createElement("span",{className:"font-mono text-[10px] text-slate-700"},"Tempo:"),/*#__PURE__*/React.createElement("button",{className:"px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]",onClick:()=>{const nt=Math.max(40,tempo-1);setTempo(nt);localStorage.setItem('studio_media_explorer_tempo',nt.toString());}},"-"),/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1 h-5 flex items-center w-11"},/*#__PURE__*/React.createElement("input",{type:"number",min:"40",max:"300",value:tempo,onChange:e=>{const v=Math.max(40,Math.min(300,parseInt(e.target.value)||120));setTempo(v);localStorage.setItem('studio_media_explorer_tempo',v.toString());},className:"w-full text-xs text-right outline-none bg-transparent"})),/*#__PURE__*/React.createElement("button",{className:"px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]",onClick:()=>{const nt=Math.min(300,tempo+1);setTempo(nt);localStorage.setItem('studio_media_explorer_tempo',nt.toString());}},"+"),/*#__PURE__*/React.createElement("span",{className:"text-slate-600 text-[10px]"},"BPM"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3 font-mono text-[11px]"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",null,"Pitch:"),/*#__PURE__*/React.createElement("button",{className:"px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]",onClick:()=>togglePitch(-0.5)},"-"),/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1 h-5 flex items-center w-14"},/*#__PURE__*/React.createElement("input",{type:"number",value:pitch.toFixed(1),step:"0.5",onChange:e=>setPitch(parseFloat(e.target.value)||0),className:"w-full text-xs text-right outline-none bg-transparent"})),/*#__PURE__*/React.createElement("button",{className:"px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]",onClick:()=>togglePitch(0.5)},"+")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",null,"Rate:"),/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1 h-5 flex items-center w-12"},/*#__PURE__*/React.createElement("input",{type:"number",value:rate.toFixed(2),step:"0.1",onChange:e=>setRate(Math.max(0.25,Math.min(4,parseFloat(e.target.value)||1))),className:"w-full text-xs text-right outline-none bg-transparent"})),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[9px]",onClick:()=>toggleRate(-1)},"-"),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[9px]",onClick:()=>toggleRate(1)},"+"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"font-sans text-slate-700"},"Volume:"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-60",max:"12",step:"0.5",value:volumeDb,onChange:e=>setVolumeDb(parseFloat(e.target.value)),className:"me-fader-slider w-24"}),/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1 h-5 flex items-center justify-center w-14 font-mono text-[11px]"},volumeDb<=-50?'-inf':volumeDb.toFixed(1)," dB")),/*#__PURE__*/React.createElement("div",{className:`px-2 py-0.5 border font-mono font-bold text-[10px] rounded-sm ${selIsMidi?'bg-purple-950 text-purple-300 border-purple-800':'bg-emerald-950 text-emerald-300 border-emerald-800'}`},selIsMidi?'MIDI':'Audio')),/*#__PURE__*/React.createElement("div",{className:"flex items-stretch gap-2 my-1 flex-1 min-h-0"},/*#__PURE__*/React.createElement("div",{className:"flex-1 bg-[#181818] border border-[#3a3a3a] relative overflow-hidden"},/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,className:"w-full h-full block cursor-pointer"})),/*#__PURE__*/React.createElement("div",{className:"w-48 bg-[#181818] border border-[#3a3a3a] p-1.5 font-mono text-[10px] text-slate-300 leading-tight overflow-y-auto shrink-0"},selected?selIsMidi&&!selected.path?/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",null,selected.events," MIDI events"),/*#__PURE__*/React.createElement("div",null,"Length: ",selected.lengthQn," quarter notes"),/*#__PURE__*/React.createElement("div",null,"Length: ",selected.time," (est)"),/*#__PURE__*/React.createElement("div",null,"Ticks per quarter note: ",selected.tpqn)):/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",null,"Size: ",selected.size_mb!=null?selected.size_mb.toFixed(2)+' MB':'-'),selIsMidi?/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",null,"Bars: ",midiBars||1),/*#__PURE__*/React.createElement("div",null,"Beats: ",Math.round(midiTotalBeats||16)),/*#__PURE__*/React.createElement("div",null,"BPM: ",midiFileBpm||120),/*#__PURE__*/React.createElement("div",null,"Duration: ",selDur.toFixed(2),"s")):/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",null,"Duration: ",selDur.toFixed(2),"s"),/*#__PURE__*/React.createElement("div",null,"Sample Rate: 44100 Hz"),/*#__PURE__*/React.createElement("div",null,"Type: ",selected.path?selected.kind==='other'?'Local File':'Local Audio':selected.type||'Audio'))):/*#__PURE__*/React.createElement("div",null,"No file selected"))),/*#__PURE__*/React.createElement("div",{className:"h-5 bg-[#c0c0c0] border-t border-white flex items-center justify-between text-[11px] font-mono px-1 shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3"},selIsMidi&&midiNotes&&midiNotes.length?/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1.5 text-slate-900 font-bold"},"Bar ",Math.max(1,Math.floor(currentTime/(4*60/(tempo||120)))+1)," / ",midiBars||1,/*#__PURE__*/React.createElement("span",{className:"text-slate-500 ml-1"},"| ",formatTime(currentTime)," / ",formatTime(selDur))):/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1.5 text-slate-900 font-bold"},formatTime(currentTime)," / ",formatTime(selDur))),/*#__PURE__*/React.createElement("div",{className:"text-slate-800 font-bold truncate max-w-[40%]"},selected?selected.name||selected.original_name:'No file selected'),/*#__PURE__*/React.createElement("div",{className:"text-slate-700"},selBpm," bpm x",rate.toFixed(2)))));};const App=()=>{// ── State Definitions ── const[tracks,setTracks]=useState([{id:'1',name:'Track 01',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#0f766e',markers:[],serverFileId:null,clips:[],sections:[],midiItems:[],isArmed:false,monitoringEnabled:true,inputSource:{deviceType:'NONE',deviceId:''}},{id:'2',name:'Track 02',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#1d4ed8',markers:[],serverFileId:null,clips:[],sections:[],midiItems:[],isArmed:false,monitoringEnabled:true,inputSource:{deviceType:'NONE',deviceId:''}}]);const[appWarningModal,setAppWarningModal]=useState(null);const[bpm,setBpm]=useState(localStorage.getItem('studio_bpm')||'120');const prevBpmRef=useRef(bpm);const[draggedClip,setDraggedClip]=useState(null);const[hoveredTrackId,setHoveredTrackId]=useState(null);// Recalculate item/section/selection durations when BPM changes useEffect(()=>{const oldSpb=prevBpmRef.current?60.0/parseFloat(prevBpmRef.current)*4:null;const bpmVal=parseFloat(bpm)||120;const secondsPerBar=60.0/bpmVal*4;// Recalculate range loop selection to maintain bar count (tempo mode only) diff --git a/app/templates/index.html b/app/templates/index.html index 7c9b531..028d676 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -24,7 +24,7 @@ - +