fix: group drag cross-track maintains relative track offsets
Each item computes its own target track as srcIdx + crossOffset. Items from different source tracks land on different target tracks, preserving the group's vertical layout shape.
This commit is contained in:
+54
-11
@@ -12049,6 +12049,16 @@ const App = () => {
|
|||||||
wrapper.scrollLeft = Math.max(0, itemPx - keepMargin);
|
wrapper.scrollLeft = Math.max(0, itemPx - keepMargin);
|
||||||
setCanvasRedrawCount(n => n + 1);
|
setCanvasRedrawCount(n => n + 1);
|
||||||
}
|
}
|
||||||
|
var allTrks = activeTracksRef.current || [];
|
||||||
|
var targetTrackId = hoveredTrackIdRef.current || drag.trackId;
|
||||||
|
// Build trackId->index map for relative offset computation
|
||||||
|
var trkIndexMap = {};
|
||||||
|
allTrks.forEach(function(tr, ti) { trkIndexMap[tr.id] = ti; });
|
||||||
|
var baseIdx = trkIndexMap[drag.trackId] || 0;
|
||||||
|
var targetIdx = trkIndexMap[targetTrackId];
|
||||||
|
if (targetIdx === undefined) { targetIdx = allTrks.length - 1; if (targetIdx < 0) targetIdx = 0; }
|
||||||
|
var crossOffset = targetIdx - baseIdx;
|
||||||
|
// Build live currentTrackMap: where each multi-drag item currently lives
|
||||||
updateActiveTracks(prev => {
|
updateActiveTracks(prev => {
|
||||||
let movedItem = null;
|
let movedItem = null;
|
||||||
for (let track of prev) {
|
for (let track of prev) {
|
||||||
@@ -12057,6 +12067,19 @@ const App = () => {
|
|||||||
if (sec || mid) { movedItem = sec || mid; break; }
|
if (sec || mid) { movedItem = sec || mid; break; }
|
||||||
}
|
}
|
||||||
if (drag.multiIds) {
|
if (drag.multiIds) {
|
||||||
|
// Build currentTrackMap from live state
|
||||||
|
var currentTrackMap = {};
|
||||||
|
Object.keys(drag.multiIds).forEach(function(mid) {
|
||||||
|
for (var pi = 0; pi < prev.length; pi++) {
|
||||||
|
var pt = prev[pi];
|
||||||
|
var f = false;
|
||||||
|
var inf = drag.multiIds[mid];
|
||||||
|
if (inf.type === 'section') f = (pt.sections || []).some(function(s) { return s.id === mid; });
|
||||||
|
else if (inf.type === 'midiItem') f = (pt.midiItems || []).some(function(mx) { return mx.id === mid; });
|
||||||
|
else if (inf.type === 'clip') f = (pt.clips || []).some(function(cx) { return cx.id === mid || 'default_' + pt.id === mid; });
|
||||||
|
if (f) { currentTrackMap[mid] = pt.id; break; }
|
||||||
|
}
|
||||||
|
});
|
||||||
var dragOrigStart = drag.multiIds[drag.itemId] ? drag.multiIds[drag.itemId].start : 0;
|
var dragOrigStart = drag.multiIds[drag.itemId] ? drag.multiIds[drag.itemId].start : 0;
|
||||||
var delta = newStart - dragOrigStart;
|
var delta = newStart - dragOrigStart;
|
||||||
return prev.map(t => {
|
return prev.map(t => {
|
||||||
@@ -12064,17 +12087,34 @@ const App = () => {
|
|||||||
var resultMidis = (t.midiItems || []).slice();
|
var resultMidis = (t.midiItems || []).slice();
|
||||||
var resultClips = (t.clips || []).slice();
|
var resultClips = (t.clips || []).slice();
|
||||||
Object.keys(drag.multiIds).forEach(function(mid) {
|
Object.keys(drag.multiIds).forEach(function(mid) {
|
||||||
var info = drag.multiIds[mid];
|
var inf = drag.multiIds[mid];
|
||||||
var newVal = info.start + delta;
|
var newVal = inf.start + delta;
|
||||||
if (info.type === 'section') {
|
var srcTid = currentTrackMap[mid];
|
||||||
var idx = resultSections.findIndex(function(s) { return s.id === mid; });
|
var srcIdx = trkIndexMap[srcTid] !== undefined ? trkIndexMap[srcTid] : baseIdx;
|
||||||
if (idx >= 0) resultSections[idx] = { ...resultSections[idx], start: Math.max(0, newVal) };
|
var itemTargetIdx = Math.max(0, srcIdx + crossOffset);
|
||||||
} else if (info.type === 'midiItem') {
|
var itemTid = itemTargetIdx < allTrks.length ? allTrks[itemTargetIdx].id : null;
|
||||||
var idx = resultMidis.findIndex(function(mx) { return mx.id === mid; });
|
if (!itemTid) return;
|
||||||
if (idx >= 0) resultMidis[idx] = { ...resultMidis[idx], startTime: Math.max(0, newVal) };
|
if (srcTid === t.id && t.id !== itemTid) {
|
||||||
} else if (info.type === 'clip') {
|
// Remove from source track (leaving for another track)
|
||||||
var idx = resultClips.findIndex(function(cx) { return cx.id === mid || 'default_' + t.id === mid; });
|
if (inf.type === 'section') resultSections = resultSections.filter(function(s) { return s.id !== mid; });
|
||||||
if (idx >= 0) resultClips[idx] = { ...resultClips[idx], startTime: Math.max(0, newVal) };
|
else if (inf.type === 'midiItem') resultMidis = resultMidis.filter(function(mx) { return mx.id !== mid; });
|
||||||
|
else if (inf.type === 'clip') { var cid3 = 'default_' + t.id; resultClips = resultClips.filter(function(cx) { return cx.id !== mid && cx.id !== cid3; }); }
|
||||||
|
}
|
||||||
|
if (t.id === itemTid) {
|
||||||
|
// This IS the item's target track — upsert
|
||||||
|
if (inf.type === 'section') {
|
||||||
|
resultSections = resultSections.filter(function(s) { return s.id !== mid; });
|
||||||
|
resultSections.push({ ...(prev.flatMap(function(x) { return x.sections || []; }).find(function(s) { return s.id === mid; }) || {}), start: Math.max(0, newVal) });
|
||||||
|
} else if (inf.type === 'midiItem') {
|
||||||
|
resultMidis = resultMidis.filter(function(mx) { return mx.id !== mid; });
|
||||||
|
resultMidis.push({ ...(prev.flatMap(function(x) { return x.midiItems || []; }).find(function(mx) { return mx.id === mid; }) || {}), startTime: Math.max(0, newVal) });
|
||||||
|
} else if (inf.type === 'clip') {
|
||||||
|
var cid4 = 'default_' + t.id;
|
||||||
|
resultClips = resultClips.filter(function(cx) { return cx.id !== mid && cx.id !== cid4; });
|
||||||
|
var allClips = prev.flatMap(function(x) { return x.clips || []; });
|
||||||
|
var srcClip = allClips.find(function(cx) { return cx.id === mid || 'default_' + (srcTid || '') === mid; });
|
||||||
|
if (srcClip) resultClips.push({ ...srcClip, startTime: Math.max(0, newVal) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { ...t, sections: resultSections, midiItems: resultMidis, clips: resultClips };
|
return { ...t, sections: resultSections, midiItems: resultMidis, clips: resultClips };
|
||||||
@@ -12091,6 +12131,9 @@ const App = () => {
|
|||||||
? { ...t, sections: updatedItems }
|
? { ...t, sections: updatedItems }
|
||||||
: { ...t, midiItems: updatedItems });
|
: { ...t, midiItems: updatedItems });
|
||||||
});
|
});
|
||||||
|
if (drag.trackId !== targetTrackId && typeof setDraggedSectionItem === 'function') {
|
||||||
|
setDraggedSectionItem(function(p) { return { ...p, trackId: targetTrackId }; });
|
||||||
|
}
|
||||||
if (drag.trackId !== targetTrackId) {
|
if (drag.trackId !== targetTrackId) {
|
||||||
setDraggedSectionItem(prev => ({ ...prev, trackId: targetTrackId }));
|
setDraggedSectionItem(prev => ({ ...prev, trackId: targetTrackId }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,7 +342,12 @@ const handleSweepSelectStart=(trackId,startTime,startY)=>{isSweepingRef.current=
|
|||||||
};// ── Section / MIDI Item Drag Start ──
|
};// ── Section / MIDI Item Drag Start ──
|
||||||
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};});(t.midiItems||[]).forEach(function(m){if(selArr.indexOf(m.id)>=0)originals[m.id]={type:'midiItem',start:m.startTime};});(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};});});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 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};});(t.midiItems||[]).forEach(function(m){if(selArr.indexOf(m.id)>=0)originals[m.id]={type:'midiItem',start:m.startTime};});(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};});});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 ──
|
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);}updateActiveTracks(prev=>{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(t=>{var resultSections=(t.sections||[]).slice();var resultMidis=(t.midiItems||[]).slice();var resultClips=(t.clips||[]).slice();Object.keys(drag.multiIds).forEach(function(mid){var info=drag.multiIds[mid];var newVal=info.start+delta;if(info.type==='section'){var idx=resultSections.findIndex(function(s){return s.id===mid;});if(idx>=0)resultSections[idx]={...resultSections[idx],start:Math.max(0,newVal)};}else if(info.type==='midiItem'){var idx=resultMidis.findIndex(function(mx){return mx.id===mid;});if(idx>=0)resultMidis[idx]={...resultMidis[idx],startTime:Math.max(0,newVal)};}else if(info.type==='clip'){var idx=resultClips.findIndex(function(cx){return cx.id===mid||'default_'+t.id===mid;});if(idx>=0)resultClips[idx]={...resultClips[idx],startTime:Math.max(0,newVal)};}});return{...t,sections:resultSections,midiItems:resultMidis,clips:resultClips};});}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){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 ──
|
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 allTrks=activeTracksRef.current||[];var targetTrackId=hoveredTrackIdRef.current||drag.trackId;// Build trackId->index map for relative offset computation
|
||||||
|
var trkIndexMap={};allTrks.forEach(function(tr,ti){trkIndexMap[tr.id]=ti;});var baseIdx=trkIndexMap[drag.trackId]||0;var targetIdx=trkIndexMap[targetTrackId];if(targetIdx===undefined){targetIdx=allTrks.length-1;if(targetIdx<0)targetIdx=0;}var crossOffset=targetIdx-baseIdx;// Build live currentTrackMap: where each multi-drag item currently lives
|
||||||
|
updateActiveTracks(prev=>{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){// Build currentTrackMap from live state
|
||||||
|
var currentTrackMap={};Object.keys(drag.multiIds).forEach(function(mid){for(var pi=0;pi<prev.length;pi++){var pt=prev[pi];var f=false;var inf=drag.multiIds[mid];if(inf.type==='section')f=(pt.sections||[]).some(function(s){return s.id===mid;});else if(inf.type==='midiItem')f=(pt.midiItems||[]).some(function(mx){return mx.id===mid;});else if(inf.type==='clip')f=(pt.clips||[]).some(function(cx){return cx.id===mid||'default_'+pt.id===mid;});if(f){currentTrackMap[mid]=pt.id;break;}}});var dragOrigStart=drag.multiIds[drag.itemId]?drag.multiIds[drag.itemId].start:0;var delta=newStart-dragOrigStart;return prev.map(t=>{var resultSections=(t.sections||[]).slice();var resultMidis=(t.midiItems||[]).slice();var resultClips=(t.clips||[]).slice();Object.keys(drag.multiIds).forEach(function(mid){var inf=drag.multiIds[mid];var newVal=inf.start+delta;var srcTid=currentTrackMap[mid];var srcIdx=trkIndexMap[srcTid]!==undefined?trkIndexMap[srcTid]:baseIdx;var itemTargetIdx=Math.max(0,srcIdx+crossOffset);var itemTid=itemTargetIdx<allTrks.length?allTrks[itemTargetIdx].id:null;if(!itemTid)return;if(srcTid===t.id&&t.id!==itemTid){// Remove from source track (leaving for another track)
|
||||||
|
if(inf.type==='section')resultSections=resultSections.filter(function(s){return s.id!==mid;});else if(inf.type==='midiItem')resultMidis=resultMidis.filter(function(mx){return mx.id!==mid;});else if(inf.type==='clip'){var cid3='default_'+t.id;resultClips=resultClips.filter(function(cx){return cx.id!==mid&&cx.id!==cid3;});}}if(t.id===itemTid){// This IS the item's target track — upsert
|
||||||
|
if(inf.type==='section'){resultSections=resultSections.filter(function(s){return s.id!==mid;});resultSections.push({...(prev.flatMap(function(x){return x.sections||[];}).find(function(s){return s.id===mid;})||{}),start:Math.max(0,newVal)});}else if(inf.type==='midiItem'){resultMidis=resultMidis.filter(function(mx){return mx.id!==mid;});resultMidis.push({...(prev.flatMap(function(x){return x.midiItems||[];}).find(function(mx){return mx.id===mid;})||{}),startTime:Math.max(0,newVal)});}else if(inf.type==='clip'){var cid4='default_'+t.id;resultClips=resultClips.filter(function(cx){return cx.id!==mid&&cx.id!==cid4;});var allClips=prev.flatMap(function(x){return x.clips||[];});var srcClip=allClips.find(function(cx){return cx.id===mid||'default_'+(srcTid||'')===mid;});if(srcClip)resultClips.push({...srcClip,startTime:Math.max(0,newVal)});}}});return{...t,sections:resultSections,midiItems:resultMidis,clips:resultClips};});}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 ──
|
||||||
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=>{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=>{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
|
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
|
||||||
|
|||||||
@@ -736,3 +736,9 @@
|
|||||||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||||||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### [2026-07-29 10:55] Task: Fix group drag cross-track — each item keeps relative track offset
|
||||||
|
- **Tóm tắt thay đổi:** Khi drag group cross-track, mỗi item tính target track riêng dựa trên `srcIdx + crossOffset`. Item trên track 1 kéo xuống 2 track → đến track 3. Item trên track 2 kéo xuống 2 track → đến track 4. Không gộp vào 1 track.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||||||
|
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||||||
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user