fix: single-item cross-track drag

Each track filters the item independently. Only target track
receives the inserted item. Removes duplicate state update.
This commit is contained in:
2026-07-29 11:02:03 +07:00
parent 8e8fc58082
commit 6c6a326b7d
3 changed files with 17 additions and 15 deletions
+7 -11
View File
@@ -12119,22 +12119,18 @@ const App = () => {
return { ...t, sections: resS, midiItems: resM, clips: resC };
});
}
const items = drag.itemType === 'section' ? (t.sections || []) : (t.midiItems || []);
const updatedItems = items.filter(it => it.id !== drag.itemId);
if (movedItem) {
updatedItems.push(drag.itemType === 'section'
return prev.map(function(tr) {
var its = drag.itemType === 'section' ? (tr.sections || []).filter(function(it) { return it.id !== drag.itemId; }) : (tr.midiItems || []).filter(function(it) { return it.id !== drag.itemId; });
if (tr.id === targetTrackId && movedItem) {
its.push(drag.itemType === 'section'
? { ...movedItem, start: newStart }
: { ...movedItem, startTime: newStart });
}
return prev.map(t => drag.itemType === 'section'
? { ...t, sections: updatedItems }
: { ...t, midiItems: updatedItems });
return drag.itemType === 'section' ? { ...tr, sections: its } : { ...tr, midiItems: its };
});
});
if (drag.trackId !== targetTrackId && typeof setDraggedSectionItem === 'function') {
setDraggedSectionItem(function(p) { return { ...p, trackId: targetTrackId }; });
}
if (drag.trackId !== targetTrackId) {
setDraggedSectionItem(prev => ({ ...prev, trackId: targetTrackId }));
setDraggedSectionItem(function(p) { return { ...p, trackId: targetTrackId }; });
}
};
const handleMouseUp = () => {
+1 -1
View File
@@ -345,7 +345,7 @@ const handleSectionItemResizeStart=(trackId,itemType,itemId,side,clickTime)=>{co
useEffect(()=>{const handleMouseMove=e=>{const drag=draggedSectionItemRef.current;if(!drag)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;autoScrollTimeline(e.clientX);const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);const beatSec=60.0/(parseInt(bpm)||120);const secondsPerBar=beatSec*4;const marginBar=maxDurationRef.current-secondsPerBar;const rawStart=Math.max(0,Math.min(time-drag.clickOffset,marginBar));const newStart=snapTime(rawStart,snapValueRef.current,bpmRef.current);const itemPx=newStart*zoom;const keepMargin=80;if(itemPx>scrollLeft+rect.width-keepMargin){wrapper.scrollLeft=itemPx-rect.width+keepMargin;setCanvasRedrawCount(n=>n+1);}else if(itemPx<scrollLeft+keepMargin){wrapper.scrollLeft=Math.max(0,itemPx-keepMargin);setCanvasRedrawCount(n=>n+1);}var targetTrackId=hoveredTrackIdRef.current||drag.trackId;updateActiveTracks(prev=>{// Build trackId->index map from live prev state
var trkIds=prev.map(function(tr){return tr.id;});var baseIdx=trkIds.indexOf(drag.trackId);if(baseIdx<0)baseIdx=0;var targetIdx=trkIds.indexOf(targetTrackId);if(targetIdx<0)targetIdx=prev.length-1;if(targetIdx<0)targetIdx=0;var crossOffset=targetIdx-baseIdx;let movedItem=null;for(let track of prev){const sec=(track.sections||[]).find(it=>it.id===drag.itemId);const mid=(track.midiItems||[]).find(it=>it.id===drag.itemId);if(sec||mid){movedItem=sec||mid;break;}}if(drag.multiIds){var dragOrigStart=drag.multiIds[drag.itemId]?drag.multiIds[drag.itemId].start:0;var delta=newStart-dragOrigStart;return prev.map(function(t){var resS=(t.sections||[]).slice();var resM=(t.midiItems||[]).slice();var resC=(t.clips||[]).slice();Object.keys(drag.multiIds).forEach(function(mid){var inf=drag.multiIds[mid];var newVal=inf.start+delta;// Use ORIGINAL trackId stored at drag start for track offset math
var origTid=inf.trackId;var origIdx=trkIds.indexOf(origTid);if(origIdx<0)origIdx=baseIdx;var itemTrgIdx=Math.max(0,origIdx+crossOffset);var itemTid=itemTrgIdx<prev.length?prev[itemTrgIdx].id:null;if(!itemTid)return;// Remove from source if leaving for another track
var isOnThisTrack=false;if(inf.type==='section')isOnThisTrack=(t.sections||[]).some(function(s){return s.id===mid;});else if(inf.type==='midiItem')isOnThisTrack=(t.midiItems||[]).some(function(mx){return mx.id===mid;});else if(inf.type==='clip')isOnThisTrack=(t.clips||[]).some(function(cx){return cx.id===mid||'default_'+t.id===mid;});if(isOnThisTrack&&t.id!==itemTid){if(inf.type==='section')resS=resS.filter(function(s){return s.id!==mid;});else if(inf.type==='midiItem')resM=resM.filter(function(mx){return mx.id!==mid;});else if(inf.type==='clip'){var c3='default_'+t.id;resC=resC.filter(function(cx){return cx.id!==mid&&cx.id!==c3;});}}if(t.id===itemTid){var srcItem=null;for(var pi2=0;pi2<prev.length;pi2++){var tr2=prev[pi2];if(inf.type==='section')srcItem=(tr2.sections||[]).find(function(s){return s.id===mid;});else if(inf.type==='midiItem')srcItem=(tr2.midiItems||[]).find(function(mx){return mx.id===mid;});else if(inf.type==='clip'){var c2=(tr2.clips||[]).find(function(cx){return cx.id===mid||'default_'+tr2.id===mid;});if(c2)srcItem=c2;}if(srcItem)break;}if(srcItem){if(inf.type==='section'){resS=resS.filter(function(s){return s.id!==mid;});resS.push({...srcItem,start:Math.max(0,newVal)});}else if(inf.type==='midiItem'){resM=resM.filter(function(mx){return mx.id!==mid;});resM.push({...srcItem,startTime:Math.max(0,newVal)});}else if(inf.type==='clip'){var c4='default_'+t.id;resC=resC.filter(function(cx){return cx.id!==mid&&cx.id!==c4;});resC.push({...srcItem,startTime:Math.max(0,newVal)});}}}});return{...t,sections:resS,midiItems:resM,clips:resC};});}const items=drag.itemType==='section'?t.sections||[]:t.midiItems||[];const updatedItems=items.filter(it=>it.id!==drag.itemId);if(movedItem){updatedItems.push(drag.itemType==='section'?{...movedItem,start:newStart}:{...movedItem,startTime:newStart});}return prev.map(t=>drag.itemType==='section'?{...t,sections:updatedItems}:{...t,midiItems:updatedItems});});if(drag.trackId!==targetTrackId&&typeof setDraggedSectionItem==='function'){setDraggedSectionItem(function(p){return{...p,trackId:targetTrackId};});}if(drag.trackId!==targetTrackId){setDraggedSectionItem(prev=>({...prev,trackId:targetTrackId}));}};const handleMouseUp=()=>{const drag=draggedSectionItemRef.current;if(!drag)return;setDraggedSectionItem(null);showToast(`Đã di chuyển ${drag.itemType==='section'?'section':'MIDI item'}.`,'success');};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Pending drag: Ctrl+click toggles selection; mousemove > threshold starts copy-drag ──
var isOnThisTrack=false;if(inf.type==='section')isOnThisTrack=(t.sections||[]).some(function(s){return s.id===mid;});else if(inf.type==='midiItem')isOnThisTrack=(t.midiItems||[]).some(function(mx){return mx.id===mid;});else if(inf.type==='clip')isOnThisTrack=(t.clips||[]).some(function(cx){return cx.id===mid||'default_'+t.id===mid;});if(isOnThisTrack&&t.id!==itemTid){if(inf.type==='section')resS=resS.filter(function(s){return s.id!==mid;});else if(inf.type==='midiItem')resM=resM.filter(function(mx){return mx.id!==mid;});else if(inf.type==='clip'){var c3='default_'+t.id;resC=resC.filter(function(cx){return cx.id!==mid&&cx.id!==c3;});}}if(t.id===itemTid){var srcItem=null;for(var pi2=0;pi2<prev.length;pi2++){var tr2=prev[pi2];if(inf.type==='section')srcItem=(tr2.sections||[]).find(function(s){return s.id===mid;});else if(inf.type==='midiItem')srcItem=(tr2.midiItems||[]).find(function(mx){return mx.id===mid;});else if(inf.type==='clip'){var c2=(tr2.clips||[]).find(function(cx){return cx.id===mid||'default_'+tr2.id===mid;});if(c2)srcItem=c2;}if(srcItem)break;}if(srcItem){if(inf.type==='section'){resS=resS.filter(function(s){return s.id!==mid;});resS.push({...srcItem,start:Math.max(0,newVal)});}else if(inf.type==='midiItem'){resM=resM.filter(function(mx){return mx.id!==mid;});resM.push({...srcItem,startTime:Math.max(0,newVal)});}else if(inf.type==='clip'){var c4='default_'+t.id;resC=resC.filter(function(cx){return cx.id!==mid&&cx.id!==c4;});resC.push({...srcItem,startTime:Math.max(0,newVal)});}}}});return{...t,sections:resS,midiItems:resM,clips:resC};});}return prev.map(function(tr){var its=drag.itemType==='section'?(tr.sections||[]).filter(function(it){return it.id!==drag.itemId;}):(tr.midiItems||[]).filter(function(it){return it.id!==drag.itemId;});if(tr.id===targetTrackId&&movedItem){its.push(drag.itemType==='section'?{...movedItem,start:newStart}:{...movedItem,startTime:newStart});}return drag.itemType==='section'?{...tr,sections:its}:{...tr,midiItems:its};});});if(drag.trackId!==targetTrackId){setDraggedSectionItem(function(p){return{...p,trackId:targetTrackId};});}};const handleMouseUp=()=>{const drag=draggedSectionItemRef.current;if(!drag)return;setDraggedSectionItem(null);showToast(`Đã di chuyển ${drag.itemType==='section'?'section':'MIDI item'}.`,'success');};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Pending drag: Ctrl+click toggles selection; mousemove > threshold starts copy-drag ──
useEffect(()=>{const handleMouseMove=e=>{var pd=pendingDragRef.current;if(!pd)return;var dx=e.clientX-pd.startX;if(Math.abs(dx)>5){var pdSnap=pendingDragRef.current;pendingDragRef.current=null;if(handleSectionItemDragStartRef.current)handleSectionItemDragStartRef.current(pdSnap.trackId,pdSnap.itemType,pdSnap.itemId,pdSnap.clickOffset,true,pdSnap.selectedIds);}};document.addEventListener('mousemove',handleMouseMove);var handleMouseUp=function(){pendingDragRef.current=null;};document.addEventListener('mouseup',handleMouseUp);return function(){document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);pendingDragRef.current=null;};},[]);// ── Document-level mousemove/mouseup for Section/MIDI item resize ──
useEffect(()=>{const handleMouseMove=e=>{const resize=resizedSectionItemRef.current;if(!resize)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;autoScrollTimeline(e.clientX);const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==resize.trackId)return t;const items=resize.itemType==='section'?[...(t.sections||[])]:[...(t.midiItems||[])];const idx=items.findIndex(it=>it.id===resize.itemId);if(idx===-1)return t;const item=items[idx];if(resize.side==='left'){const beatSec=60.0/(parseInt(bpm)||120);const newStart=Math.max(0,Math.min(time,resize.originalStart+resize.originalDuration-0.1));const end=resize.originalStart+resize.originalDuration;const newDuration=end-newStart;if(newDuration<0.1)return t;items[idx]=resize.itemType==='section'?{...item,start:newStart,duration:newDuration}:{...item,startTime:newStart,duration:newDuration};}else{const secondsPerBar=60.0/(parseInt(bpm)||120)*4;const marginBar=maxDurationRef.current-secondsPerBar;const clampedTime=Math.min(time,marginBar);const snappedDuration=snapValueRef.current!=='free'?snapTime(clampedTime-resize.originalStart,snapValueRef.current,bpm):clampedTime-resize.originalStart;const newDuration=Math.max(0.1,snappedDuration);items[idx]={...item,duration:newDuration};}return resize.itemType==='section'?{...t,sections:items}:{...t,midiItems:items};}));setCanvasRedrawCount(n=>n+1);const edgePx=(resize.side==='left'?Math.max(0,time):time)*zoom;const keepMargin=80;if(edgePx>scrollLeft+rect.width-keepMargin){wrapper.scrollLeft=edgePx-rect.width+keepMargin;setCanvasRedrawCount(n=>n+1);}else if(edgePx<scrollLeft+keepMargin){wrapper.scrollLeft=Math.max(0,edgePx-keepMargin);setCanvasRedrawCount(n=>n+1);}};const handleMouseUp=()=>{const resize=resizedSectionItemRef.current;if(!resize)return;setResizedSectionItem(null);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Sweep Select mousemove/mouseup ──
useEffect(()=>{const handleMouseMove=e=>{if(!isSweepingRef.current)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);setSweepSelect(prev=>{var updated=prev?{...prev,endTime:time}:null;sweepSelectRef.current=updated;return updated;});};const handleMouseUp=()=>{if(!isSweepingRef.current)return;isSweepingRef.current=false;sweepTrackIdRef.current=null;const sweep=sweepSelectRef.current;sweepSelectRef.current=null;if(sweep){const start=Math.min(sweep.startTime,sweep.endTime);const end=Math.max(sweep.startTime,sweep.endTime);// Small movement (no real drag) → deselect all
+6
View File
@@ -754,3 +754,9 @@
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
---
### [2026-07-29 11:04] Task: Fix single-item cross-track drag
- **Tóm tắt thay đổi:** Sửa single-item drag path: mỗi track filter `drag.itemId` riêng, chỉ insert vào `targetTrackId`. Xóa duplicate `setDraggedSectionItem`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
---