fix: rewrite drag handler with stable crossOffset + outTracks merge

This commit is contained in:
2026-07-29 11:13:31 +07:00
parent e40fdf15ad
commit d4262acb9f
3 changed files with 91 additions and 86 deletions
+79 -81
View File
@@ -12061,102 +12061,100 @@ const App = () => {
}
if (targetIdx < 0) targetIdx = mouseY < trackTop ? 0 : Math.max(0, allTrks.length - 1);
var targetTrackId = allTrks[targetIdx] ? allTrks[targetIdx].id : drag.trackId;
// Use ORIGINAL trackId (never changes during drag) for crossOffset math
// Use ORIGINAL trackId from drag start for crossOffset math (never changes)
var dragBaseTrackId = drag.multiIds && drag.multiIds[drag.itemId] ? drag.multiIds[drag.itemId].trackId : drag.trackId;
updateActiveTracks(prev => {
var trkIds = prev.map(function(tr) { return tr.id; });
var baseIdx = trkIds.indexOf(dragBaseTrackId);
if (baseIdx < 0) baseIdx = 0;
var tIdx = trkIds.indexOf(targetTrackId);
if (tIdx < 0) tIdx = prev.length - 1;
if (tIdx < 0) tIdx = 0;
var crossOffset = tIdx - 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;
// Ensure enough tracks exist for all displaced items
var maxTrg = 0;
Object.keys(drag.multiIds).forEach(function(mid) {
var inf = drag.multiIds[mid];
var origIdx = trkIds.indexOf(inf.trackId);
if (origIdx < 0) origIdx = baseIdx;
var trg = Math.max(0, origIdx + crossOffset);
if (trg > maxTrg) maxTrg = trg;
});
var tracksToAdd = [];
while (prev.length <= maxTrg) {
var newId = (prev.length + tracksToAdd.length + 1).toString();
var colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
tracksToAdd.push({ id: newId, name: 'Track ' + newId, buffer: null, startTime: 0, volumeDb: 0, pan: 0, muted: false, solo: false, color: colors[(prev.length + tracksToAdd.length) % colors.length], clips: [], midiItems: [], sections: [], markers: [], isArmed: false, height: 140 });
// Pre-compute crossOffset from current allTrks (stable per mousemove)
var baseIdx = allTrks.findIndex(function(tr) { return tr.id === dragBaseTrackId; });
if (baseIdx < 0) baseIdx = 0;
var trgIdx = allTrks.findIndex(function(tr) { return tr.id === targetTrackId; });
if (trgIdx < 0) trgIdx = allTrks.length - 1;
if (trgIdx < 0) trgIdx = 0;
var crossOffset = trgIdx - baseIdx;
// Special path for multi-select drag: handle items from multiple tracks
if (drag.multiIds) {
var dragOrigStart = drag.multiIds[drag.itemId] ? drag.multiIds[drag.itemId].start : 0;
var delta = newStart - dragOrigStart;
// Extend activeTracks if target track index exceeds track count
var outTracks = [];
Object.keys(drag.multiIds).forEach(function(mid) {
var inf = drag.multiIds[mid];
var oIdx = allTrks.findIndex(function(tr) { return tr.id === inf.trackId; });
if (oIdx < 0) oIdx = baseIdx;
var needIdx = oIdx + crossOffset;
if (needIdx >= allTrks.length) {
for (var ai = allTrks.length; ai <= needIdx; ai++) {
var exists = outTracks.some(function(ot) { return ot.id === (ai + 1).toString(); });
if (!exists) {
var colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
outTracks.push({ id: (ai + 1).toString(), name: 'Track ' + (ai + 1), buffer: null, startTime: 0, volumeDb: 0, pan: 0, muted: false, solo: false, color: colors[ai % colors.length], clips: [], midiItems: [], sections: [], markers: [], isArmed: false, height: 140 });
}
}
}
if (tracksToAdd.length > 0) prev = prev.concat(tracksToAdd);
return prev.map(function(t) {
});
updateActiveTracks(function(prev) {
var trkIds = prev.map(function(tr) { return tr.id; });
var merged = outTracks.length > 0 ? prev.concat(outTracks.filter(function(ot) { return !trkIds.includes(ot.id); })) : prev;
var allIds = merged.map(function(tr) { return tr.id; });
return merged.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;
var oIdx = allIds.indexOf(inf.trackId);
if (oIdx < 0) oIdx = baseIdx;
var needIdx = Math.max(0, oIdx + crossOffset);
var targetTid = needIdx < merged.length ? merged[needIdx].id : merged[merged.length - 1].id;
if (inf.type === 'section') {
resS = resS.filter(function(s) { return s.id !== mid; });
if (t.id === targetTid) {
var src = null;
for (var p = 0; p < prev.length; p++) { src = (prev[p].sections || []).find(function(s) { return s.id === mid; }); if (src) break; }
if (src) resS.push({ ...src, start: Math.max(0, newVal) });
}
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) });
}
} else if (inf.type === 'midiItem') {
resM = resM.filter(function(mx) { return mx.id !== mid; });
if (t.id === targetTid) {
var src = null;
for (var p = 0; p < prev.length; p++) { src = (prev[p].midiItems || []).find(function(mx) { return mx.id === mid; }); if (src) break; }
if (src) resM.push({ ...src, startTime: Math.max(0, newVal) });
}
} else if (inf.type === 'clip') {
var cid3 = 'default_' + t.id;
resC = resC.filter(function(cx) { return cx.id !== mid && cx.id !== cid3; });
if (t.id === targetTid) {
var src = null;
for (var p = 0; p < prev.length; p++) { var c2 = (prev[p].clips || []).find(function(cx) { return cx.id === mid || 'default_' + prev[p].id === mid; }); if (c2) { src = c2; break; } }
if (src) resC.push({ ...src, 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 };
});
});
} else {
// Single item drag path
updateActiveTracks(function(prev) {
let movedItem = null;
for (let track of prev) {
const sec = (track.sections || []).find(function(it) { return it.id === drag.itemId; });
const mid = (track.midiItems || []).find(function(it) { return it.id === drag.itemId; });
if (sec || mid) { movedItem = sec || mid; break; }
}
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 };
});
});
}
};
const handleMouseUp = () => {
+6 -5
View File
@@ -343,11 +343,12 @@ const handleSweepSelectStart=(trackId,startTime,startY)=>{isSweepingRef.current=
const handleSectionItemDragStart=(trackId,itemType,itemId,clickOffset,isDuplicate,pendingSelectedIds)=>{var curTracks=activeTracksRef.current||activeTracks;var multiIds=null;var selIds=pendingSelectedIds||selectedItemIds;if(selIds&&selIds.size>0&&selIds.has(itemId)){var selArr=Array.from(selIds);var originals={};curTracks.forEach(function(t){(t.sections||[]).forEach(function(s){if(selArr.indexOf(s.id)>=0)originals[s.id]={type:'section',start:s.start,trackId:t.id};});(t.midiItems||[]).forEach(function(m){if(selArr.indexOf(m.id)>=0)originals[m.id]={type:'midiItem',start:m.startTime,trackId:t.id};});(t.clips||[]).forEach(function(c){var cid=c.id==='default'?'default_'+t.id:c.id;if(selArr.indexOf(cid)>=0)originals[cid]={type:'clip',start:c.startTime,trackId:t.id};});});if(Object.keys(originals).length>0)multiIds=originals;}if(isDuplicate){var track=curTracks.find(function(t){return t.id===trackId;});if(!track)return;if(multiIds){var newOriginals={};updateActiveTracks(function(prev){return prev.map(function(t){var updatedSections=t.sections?t.sections.slice():[];var updatedMidi=t.midiItems?t.midiItems.slice():[];var updatedClips=t.clips?t.clips.slice():[];Object.keys(multiIds).forEach(function(oid){var info=multiIds[oid];if(info.type==='section'){var sec=(t.sections||[]).find(function(s){return s.id===oid;});if(sec){var rearrangeNewId='sec_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedSections.push({...sec,id:rearrangeNewId,name:sec.name+' (Copy)'});newOriginals[rearrangeNewId]={type:'section',start:sec.start};}}else if(info.type==='midiItem'){var mid=(t.midiItems||[]).find(function(m){return m.id===oid;});if(mid){var rearrangeNewId='midi_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedMidi.push({...mid,id:rearrangeNewId,name:mid.name+' (Copy)'});newOriginals[rearrangeNewId]={type:'midiItem',start:mid.startTime};}}else if(info.type==='clip'){var clip=(t.clips||[]).find(function(c){return c.id===oid||'default_'+t.id===oid;});if(clip){var rearrangeNewId='clip_dup_'+Date.now()+'_'+Math.random().toString(36).substr(2,5);updatedClips.push({...clip,id:rearrangeNewId,startTime:clip.startTime,name:clip.name+' (Copy)'});newOriginals[rearrangeNewId]={type:'clip',start:clip.startTime};}}});return{...t,sections:updatedSections,midiItems:updatedMidi,clips:updatedClips};});});var newItemId=Object.keys(newOriginals)[0]||itemId;setDraggedSectionItem({trackId,itemType,itemId:newItemId,clickOffset,multiIds:newOriginals});}else{var items=itemType==='section'?track.sections||[]:track.midiItems||[];var item=items.find(function(it){return it.id===itemId;});if(!item)return;var rearrangeNewId=itemType+'_dup_'+Date.now();var newItem={...item,id:rearrangeNewId,name:item.name+' (Copy)'};updateActiveTracks(function(prev){return prev.map(function(t){if(t.id!==trackId)return t;var updated=itemType==='section'?[...(t.sections||[]),newItem]:[...(t.midiItems||[]),newItem];return itemType==='section'?{...t,sections:updated}:{...t,midiItems:updated};});});setDraggedSectionItem({trackId,itemType,itemId:rearrangeNewId,clickOffset,isDuplicate:false});}return;}setDraggedSectionItem({trackId,itemType,itemId,clickOffset,multiIds});};handleSectionItemDragStartRef.current=handleSectionItemDragStart;// ── Section / MIDI Item Resize Start ──
const handleSectionItemResizeStart=(trackId,itemType,itemId,side,clickTime)=>{const curTracks=activeTracks;const track=curTracks.find(t=>t.id===trackId);if(!track)return;const items=itemType==='section'?track.sections:track.midiItems;const item=(items||[]).find(it=>it.id===itemId);if(!item)return;const start=itemType==='section'?item.start:item.startTime;setResizedSectionItem({trackId,itemType,itemId,side,originalStart:start,originalDuration:item.duration});};// ── Document-level mousemove/mouseup for Section/MIDI item drag ──
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);}// Compute target track from mouse Y position
var allTrks=activeTracksRef.current||[];var mouseY=e.clientY-rect.top;var trackTop=0;var targetIdx=-1;for(var ti=0;ti<allTrks.length;ti++){var tH=allTrks[ti].height||(allTrks[ti].isArmed?164:140);if(mouseY>=trackTop&&mouseY<trackTop+tH){targetIdx=ti;break;}trackTop+=tH;}if(targetIdx<0)targetIdx=mouseY<trackTop?0:Math.max(0,allTrks.length-1);var targetTrackId=allTrks[targetIdx]?allTrks[targetIdx].id:drag.trackId;// Use ORIGINAL trackId (never changes during drag) for crossOffset math
var dragBaseTrackId=drag.multiIds&&drag.multiIds[drag.itemId]?drag.multiIds[drag.itemId].trackId:drag.trackId;updateActiveTracks(prev=>{var trkIds=prev.map(function(tr){return tr.id;});var baseIdx=trkIds.indexOf(dragBaseTrackId);if(baseIdx<0)baseIdx=0;var tIdx=trkIds.indexOf(targetTrackId);if(tIdx<0)tIdx=prev.length-1;if(tIdx<0)tIdx=0;var crossOffset=tIdx-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;// Ensure enough tracks exist for all displaced items
var maxTrg=0;Object.keys(drag.multiIds).forEach(function(mid){var inf=drag.multiIds[mid];var origIdx=trkIds.indexOf(inf.trackId);if(origIdx<0)origIdx=baseIdx;var trg=Math.max(0,origIdx+crossOffset);if(trg>maxTrg)maxTrg=trg;});var tracksToAdd=[];while(prev.length<=maxTrg){var newId=(prev.length+tracksToAdd.length+1).toString();var colors=['#0f766e','#1d4ed8','#701a75','#a21caf','#b45309'];tracksToAdd.push({id:newId,name:'Track '+newId,buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:colors[(prev.length+tracksToAdd.length)%colors.length],clips:[],midiItems:[],sections:[],markers:[],isArmed:false,height:140});}if(tracksToAdd.length>0)prev=prev.concat(tracksToAdd);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};});}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};});});};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 allTrks=activeTracksRef.current||[];var mouseY=e.clientY-rect.top;var trackTop=0;var targetIdx=-1;for(var ti=0;ti<allTrks.length;ti++){var tH=allTrks[ti].height||(allTrks[ti].isArmed?164:140);if(mouseY>=trackTop&&mouseY<trackTop+tH){targetIdx=ti;break;}trackTop+=tH;}if(targetIdx<0)targetIdx=mouseY<trackTop?0:Math.max(0,allTrks.length-1);var targetTrackId=allTrks[targetIdx]?allTrks[targetIdx].id:drag.trackId;// Use ORIGINAL trackId from drag start for crossOffset math (never changes)
var dragBaseTrackId=drag.multiIds&&drag.multiIds[drag.itemId]?drag.multiIds[drag.itemId].trackId:drag.trackId;// Pre-compute crossOffset from current allTrks (stable per mousemove)
var baseIdx=allTrks.findIndex(function(tr){return tr.id===dragBaseTrackId;});if(baseIdx<0)baseIdx=0;var trgIdx=allTrks.findIndex(function(tr){return tr.id===targetTrackId;});if(trgIdx<0)trgIdx=allTrks.length-1;if(trgIdx<0)trgIdx=0;var crossOffset=trgIdx-baseIdx;// Special path for multi-select drag: handle items from multiple tracks
if(drag.multiIds){var dragOrigStart=drag.multiIds[drag.itemId]?drag.multiIds[drag.itemId].start:0;var delta=newStart-dragOrigStart;// Extend activeTracks if target track index exceeds track count
var outTracks=[];Object.keys(drag.multiIds).forEach(function(mid){var inf=drag.multiIds[mid];var oIdx=allTrks.findIndex(function(tr){return tr.id===inf.trackId;});if(oIdx<0)oIdx=baseIdx;var needIdx=oIdx+crossOffset;if(needIdx>=allTrks.length){for(var ai=allTrks.length;ai<=needIdx;ai++){var exists=outTracks.some(function(ot){return ot.id===(ai+1).toString();});if(!exists){var colors=['#0f766e','#1d4ed8','#701a75','#a21caf','#b45309'];outTracks.push({id:(ai+1).toString(),name:'Track '+(ai+1),buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:colors[ai%colors.length],clips:[],midiItems:[],sections:[],markers:[],isArmed:false,height:140});}}}});updateActiveTracks(function(prev){var trkIds=prev.map(function(tr){return tr.id;});var merged=outTracks.length>0?prev.concat(outTracks.filter(function(ot){return!trkIds.includes(ot.id);})):prev;var allIds=merged.map(function(tr){return tr.id;});return merged.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;var oIdx=allIds.indexOf(inf.trackId);if(oIdx<0)oIdx=baseIdx;var needIdx=Math.max(0,oIdx+crossOffset);var targetTid=needIdx<merged.length?merged[needIdx].id:merged[merged.length-1].id;if(inf.type==='section'){resS=resS.filter(function(s){return s.id!==mid;});if(t.id===targetTid){var src=null;for(var p=0;p<prev.length;p++){src=(prev[p].sections||[]).find(function(s){return s.id===mid;});if(src)break;}if(src)resS.push({...src,start:Math.max(0,newVal)});}}else if(inf.type==='midiItem'){resM=resM.filter(function(mx){return mx.id!==mid;});if(t.id===targetTid){var src=null;for(var p=0;p<prev.length;p++){src=(prev[p].midiItems||[]).find(function(mx){return mx.id===mid;});if(src)break;}if(src)resM.push({...src,startTime:Math.max(0,newVal)});}}else if(inf.type==='clip'){var cid3='default_'+t.id;resC=resC.filter(function(cx){return cx.id!==mid&&cx.id!==cid3;});if(t.id===targetTid){var src=null;for(var p=0;p<prev.length;p++){var c2=(prev[p].clips||[]).find(function(cx){return cx.id===mid||'default_'+prev[p].id===mid;});if(c2){src=c2;break;}}if(src)resC.push({...src,startTime:Math.max(0,newVal)});}}});return{...t,sections:resS,midiItems:resM,clips:resC};});});}else{// Single item drag path
updateActiveTracks(function(prev){let movedItem=null;for(let track of prev){const sec=(track.sections||[]).find(function(it){return it.id===drag.itemId;});const mid=(track.midiItems||[]).find(function(it){return it.id===drag.itemId;});if(sec||mid){movedItem=sec||mid;break;}}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};});});}};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
@@ -784,3 +784,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:16] Task: Rewrite drag handler — separate multi/single paths, pre-compute crossOffset
- **Tóm tắt thay đổi:** Tách multiIds path và single path. Pre-compute crossOffset + outTracks ngoài `updateActiveTracks`. outTracks merge vào prev bằng filter id unique. Mỗi item compute targetTid từ origIdx+crossOffset, filter + upsert trên target track.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
---