feat: Ctrl+drag sweep select + multi-item copy/drag
- Ctrl+drag on empty space: sweep range selection overlay - Mouseup selects all items (MIDI, sections, clips) in sweep - Selected items rendered with amber border highlight - Ctrl+drag on section/MIDI item: duplicates the item - Drag selected items: moves entire selected group together - Ctrl+drag selected items: duplicates the entire group
This commit is contained in:
+198
-22
@@ -410,6 +410,8 @@ const WaveformLane = ({
|
|||||||
activeTool,
|
activeTool,
|
||||||
onSplitTrackAtTime,
|
onSplitTrackAtTime,
|
||||||
onEditClipInSubTab,
|
onEditClipInSubTab,
|
||||||
|
selectedItemIds,
|
||||||
|
onSweepSelectStart,
|
||||||
snapValue,
|
snapValue,
|
||||||
bpm,
|
bpm,
|
||||||
scrollLeft,
|
scrollLeft,
|
||||||
@@ -701,11 +703,12 @@ const WaveformLane = ({
|
|||||||
const secStartLocal = sec.start * zoom - scrollLeftVal; // use secStartLocal NOT secStart
|
const secStartLocal = sec.start * zoom - scrollLeftVal; // use secStartLocal NOT secStart
|
||||||
const secWidth = sec.duration * zoom;
|
const secWidth = sec.duration * zoom;
|
||||||
if (secStartLocal + secWidth < 0 || secStartLocal > drawWidth) return;
|
if (secStartLocal + secWidth < 0 || secStartLocal > drawWidth) return;
|
||||||
ctx.fillStyle = sec.color ? sec.color + '44' : 'rgba(6, 182, 212, 0.25)';
|
var isSecSelected = selectedItemIds && selectedItemIds.has(sec.id);
|
||||||
|
ctx.fillStyle = isSecSelected ? 'rgba(245, 158, 11, 0.35)' : (sec.color ? sec.color + '44' : 'rgba(6, 182, 212, 0.25)');
|
||||||
ctx.fillRect(secStartLocal, 2, secWidth, height - 4);
|
ctx.fillRect(secStartLocal, 2, secWidth, height - 4);
|
||||||
ctx.strokeStyle = sec.color || '#06b6d4';
|
ctx.strokeStyle = isSecSelected ? '#f59e0b' : (sec.color || '#06b6d4');
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = isSecSelected ? 2.5 : 1;
|
||||||
ctx.setLineDash([4, 4]);
|
ctx.setLineDash(isSecSelected ? [] : [4, 4]);
|
||||||
ctx.strokeRect(secStartLocal, 2, secWidth, height - 4);
|
ctx.strokeRect(secStartLocal, 2, secWidth, height - 4);
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
ctx.fillStyle = '#e4e4e7';
|
ctx.fillStyle = '#e4e4e7';
|
||||||
@@ -792,10 +795,11 @@ const WaveformLane = ({
|
|||||||
console.log(`[DevLog] [Canvas Draw] Rendering MIDI item: ID=${midi.id}, Name="${midi.name}", Start=${midiStartLocal.toFixed(1)}px, Width=${midiWidth.toFixed(1)}px, NotesCount=${midi.notes?.length || 0}`);
|
console.log(`[DevLog] [Canvas Draw] Rendering MIDI item: ID=${midi.id}, Name="${midi.name}", Start=${midiStartLocal.toFixed(1)}px, Width=${midiWidth.toFixed(1)}px, NotesCount=${midi.notes?.length || 0}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillStyle = isRecordingItem ? 'rgba(239, 68, 68, 0.2)' : '#a78bfa33';
|
var isMidiSelected = selectedItemIds && selectedItemIds.has(midi.id);
|
||||||
|
ctx.fillStyle = isMidiSelected ? 'rgba(245, 158, 11, 0.35)' : (isRecordingItem ? 'rgba(239, 68, 68, 0.2)' : '#a78bfa33');
|
||||||
ctx.fillRect(midiStartLocal, 2, midiWidth, height - 4);
|
ctx.fillRect(midiStartLocal, 2, midiWidth, height - 4);
|
||||||
ctx.strokeStyle = isRecordingItem ? '#ef4444' : '#a78bfa';
|
ctx.strokeStyle = isMidiSelected ? '#f59e0b' : (isRecordingItem ? '#ef4444' : '#a78bfa');
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = isMidiSelected ? 2.5 : 1.5;
|
||||||
ctx.strokeRect(midiStartLocal, 2, midiWidth, height - 4);
|
ctx.strokeRect(midiStartLocal, 2, midiWidth, height - 4);
|
||||||
ctx.fillStyle = isRecordingItem ? '#fca5a5' : '#c4b5fd';
|
ctx.fillStyle = isRecordingItem ? '#fca5a5' : '#c4b5fd';
|
||||||
ctx.font = 'bold 9px sans-serif';
|
ctx.font = 'bold 9px sans-serif';
|
||||||
@@ -833,7 +837,7 @@ const WaveformLane = ({
|
|||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.strokeRect(hlLeftLocal, 0, hlWidth, height);
|
ctx.strokeRect(hlLeftLocal, 0, hlWidth, height);
|
||||||
}
|
}
|
||||||
}, [track, zoom, timelineWidth, viewportWidth, isSelected, markers, selectionMode, localSelectionTrackId, localSelLeft, localSelRight, snapValue, bpm, scrollLeft, recordingState, recTempMidiNotes, recStartTimelineTime, canvasRedrawCount, currentTime]);
|
}, [track, zoom, timelineWidth, viewportWidth, isSelected, markers, selectionMode, localSelectionTrackId, localSelLeft, localSelRight, snapValue, bpm, scrollLeft, recordingState, recTempMidiNotes, recStartTimelineTime, canvasRedrawCount, currentTime, selectedItemIds]);
|
||||||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
||||||
key: "virtual-spacer",
|
key: "virtual-spacer",
|
||||||
style: {
|
style: {
|
||||||
@@ -1072,10 +1076,10 @@ const WaveformLane = ({
|
|||||||
if (time >= midi.startTime && time < midi.startTime + midi.duration) { hitItem = { type: 'midiItem', id: midi.id, start: midi.startTime }; break; }
|
if (time >= midi.startTime && time < midi.startTime + midi.duration) { hitItem = { type: 'midiItem', id: midi.id, start: midi.startTime }; break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hitItem && !e.altKey && !e.ctrlKey && !e.shiftKey) {
|
if (hitItem && !e.altKey && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onSectionItemDragStart) onSectionItemDragStart(track.id, hitItem.type, hitItem.id, time - hitItem.start);
|
if (onSectionItemDragStart) onSectionItemDragStart(track.id, hitItem.type, hitItem.id, time - hitItem.start, e.ctrlKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1136,12 +1140,11 @@ const WaveformLane = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Ctrl+Click outside active local selection to clear
|
// Ctrl+Click on empty space: start sweep select
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey && !clickedClip && !hitItem) {
|
||||||
if (onClearLocalSelection) onClearLocalSelection();
|
e.preventDefault();
|
||||||
if (onSetSelectionMode) onSetSelectionMode(null);
|
e.stopPropagation();
|
||||||
if (onSetSelectionStart) onSetSelectionStart(null);
|
if (onSweepSelectStart) onSweepSelectStart(track.id, time);
|
||||||
if (onSetSelectionEnd) onSetSelectionEnd(null);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onPlayheadSet(time);
|
onPlayheadSet(time);
|
||||||
@@ -6905,6 +6908,7 @@ const App = () => {
|
|||||||
bpmRef.current = bpm;
|
bpmRef.current = bpm;
|
||||||
// BMP for Tempo Track - LOOP_EDITOR_2.md §6
|
// BMP for Tempo Track - LOOP_EDITOR_2.md §6
|
||||||
const [selectedTrackId, setSelectedTrackId] = useState('1');
|
const [selectedTrackId, setSelectedTrackId] = useState('1');
|
||||||
|
const [selectedItemIds, setSelectedItemIds] = useState(new Set());
|
||||||
const [currentTime, setCurrentTime] = useState(0);
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [selectionStart, setSelectionStart] = useState(null);
|
const [selectionStart, setSelectionStart] = useState(null);
|
||||||
@@ -6913,6 +6917,10 @@ const App = () => {
|
|||||||
const selectionRef = useRef({ start: null, end: null });
|
const selectionRef = useRef({ start: null, end: null });
|
||||||
selectionRef.current = { start: selectionStart, end: selectionEnd };
|
selectionRef.current = { start: selectionStart, end: selectionEnd };
|
||||||
const [selectionMode, setSelectionMode] = useState(null); // 'global' (from ruler) | 'local' (from track)
|
const [selectionMode, setSelectionMode] = useState(null); // 'global' (from ruler) | 'local' (from track)
|
||||||
|
const [sweepSelect, setSweepSelect] = useState(null);
|
||||||
|
const isSweepingRef = useRef(false);
|
||||||
|
const sweepStartRef = useRef(0);
|
||||||
|
const sweepTrackIdRef = useRef(null);
|
||||||
const [localSelectionTrackId, setLocalSelectionTrackId] = useState(null);
|
const [localSelectionTrackId, setLocalSelectionTrackId] = useState(null);
|
||||||
const [localSelectionStart, setLocalSelectionStart] = useState(null);
|
const [localSelectionStart, setLocalSelectionStart] = useState(null);
|
||||||
const [localSelectionEnd, setLocalSelectionEnd] = useState(null);
|
const [localSelectionEnd, setLocalSelectionEnd] = useState(null);
|
||||||
@@ -11611,9 +11619,94 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
}, [zoom]);
|
}, [zoom]);
|
||||||
|
|
||||||
|
// ── Sweep Select ──
|
||||||
|
const handleSweepSelectStart = (trackId, startTime) => {
|
||||||
|
isSweepingRef.current = true;
|
||||||
|
sweepStartRef.current = startTime;
|
||||||
|
sweepTrackIdRef.current = trackId;
|
||||||
|
setSweepSelect({ startTime, endTime: startTime });
|
||||||
|
setSelectedItemIds(new Set());
|
||||||
|
};
|
||||||
|
|
||||||
// ── Section / MIDI Item Drag Start ──
|
// ── Section / MIDI Item Drag Start ──
|
||||||
const handleSectionItemDragStart = (trackId, itemType, itemId, clickOffset) => {
|
const handleSectionItemDragStart = (trackId, itemType, itemId, clickOffset, isDuplicate) => {
|
||||||
setDraggedSectionItem({ trackId, itemType, itemId, clickOffset });
|
var curTracks = activeTracksRef.current || activeTracks;
|
||||||
|
var multiIds = null;
|
||||||
|
if (selectedItemIds && selectedItemIds.size > 0 && selectedItemIds.has(itemId)) {
|
||||||
|
var selArr = Array.from(selectedItemIds);
|
||||||
|
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 newId = 'sec_dup_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
|
||||||
|
updatedSections.push({ ...sec, id: newId, name: sec.name + ' (Copy)' });
|
||||||
|
newOriginals[newId] = { type: 'section', start: sec.start };
|
||||||
|
}
|
||||||
|
} else if (info.type === 'midiItem') {
|
||||||
|
var mid = (t.midiItems || []).find(function(m) { return m.id === oid; });
|
||||||
|
if (mid) {
|
||||||
|
var newId = 'midi_dup_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
|
||||||
|
updatedMidi.push({ ...mid, id: newId, name: mid.name + ' (Copy)' });
|
||||||
|
newOriginals[newId] = { 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 newId = 'clip_dup_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
|
||||||
|
updatedClips.push({ ...clip, id: newId, startTime: clip.startTime, name: clip.name + ' (Copy)' });
|
||||||
|
newOriginals[newId] = { type: 'clip', start: clip.startTime };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var changed = t.sections && t.sections.length !== updatedSections.length;
|
||||||
|
return t.id === trackId ? { ...t, sections: updatedSections, midiItems: updatedMidi, clips: updatedClips } : t;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setDraggedSectionItem({ trackId, itemType, itemId, 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 newId = itemType + '_dup_' + Date.now();
|
||||||
|
var newItem = { ...item, id: newId, 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: newId, clickOffset, isDuplicate: false });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDraggedSectionItem({ trackId, itemType, itemId, clickOffset, multiIds });
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Section / MIDI Item Resize Start ──
|
// ── Section / MIDI Item Resize Start ──
|
||||||
@@ -11666,6 +11759,26 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return prev.map(t => {
|
return prev.map(t => {
|
||||||
|
if (drag.multiIds) {
|
||||||
|
var dragOrigStart = drag.multiIds[drag.itemId] ? drag.multiIds[drag.itemId].start : 0;
|
||||||
|
var delta = newStart - dragOrigStart;
|
||||||
|
var midSections = (t.sections || []).slice();
|
||||||
|
var midMidis = (t.midiItems || []).slice();
|
||||||
|
Object.keys(drag.multiIds).forEach(function(mid) {
|
||||||
|
var info = drag.multiIds[mid];
|
||||||
|
var newVal = info.start + delta;
|
||||||
|
if (info.type === 'section') {
|
||||||
|
var idx = midSections.findIndex(function(s) { return s.id === mid; });
|
||||||
|
if (idx >= 0) midSections[idx] = { ...midSections[idx], start: Math.max(0, newVal) };
|
||||||
|
} else if (info.type === 'midiItem') {
|
||||||
|
var idx = midMidis.findIndex(function(m) { return m.id === mid; });
|
||||||
|
if (idx >= 0) midMidis[idx] = { ...midMidis[idx], startTime: Math.max(0, newVal) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return t.id === targetTrackId
|
||||||
|
? { ...t, sections: midSections, midiItems: midMidis }
|
||||||
|
: { ...t, sections: (t.id === drag.trackId ? midSections : t.sections), midiItems: (t.id === drag.trackId ? midMidis : t.midiItems) };
|
||||||
|
}
|
||||||
const items = drag.itemType === 'section' ? (t.sections || []) : (t.midiItems || []);
|
const items = drag.itemType === 'section' ? (t.sections || []) : (t.midiItems || []);
|
||||||
const updatedItems = items.filter(it => it.id !== drag.itemId);
|
const updatedItems = items.filter(it => it.id !== drag.itemId);
|
||||||
if (t.id === targetTrackId) {
|
if (t.id === targetTrackId) {
|
||||||
@@ -11767,6 +11880,61 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
}, [zoom, activeTab, sessionTabs]);
|
}, [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 => prev ? { ...prev, endTime: time } : null);
|
||||||
|
};
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
if (!isSweepingRef.current) return;
|
||||||
|
isSweepingRef.current = false;
|
||||||
|
const sweep = sweepSelect;
|
||||||
|
if (sweep && sweepTrackIdRef.current) {
|
||||||
|
const tId = sweepTrackIdRef.current;
|
||||||
|
const start = Math.min(sweep.startTime, sweep.endTime);
|
||||||
|
const end = Math.max(sweep.startTime, sweep.endTime);
|
||||||
|
const curTracks = activeTracksRef.current || [];
|
||||||
|
const found = new Set();
|
||||||
|
curTracks.forEach(t => {
|
||||||
|
if (t.id !== tId) return;
|
||||||
|
(t.midiItems || []).forEach(m => {
|
||||||
|
if (m.startTime < end && m.startTime + m.duration > start) {
|
||||||
|
found.add(m.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(t.sections || []).forEach(s => {
|
||||||
|
if (s.start < end && s.start + s.duration > start) {
|
||||||
|
found.add(s.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(t.clips || []).forEach(c => {
|
||||||
|
const dur = c.buffer ? c.buffer.duration / (c.speed || 1.0) : 4;
|
||||||
|
if (c.startTime < end && c.startTime + dur > start) {
|
||||||
|
const cid = c.id === 'default' ? 'default_' + t.id : c.id;
|
||||||
|
found.add(cid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setSelectedItemIds(found);
|
||||||
|
setSweepSelect(null);
|
||||||
|
sweepTrackIdRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
}, [zoom]);
|
||||||
|
|
||||||
const handleSelectRange = (start, end, reset) => {
|
const handleSelectRange = (start, end, reset) => {
|
||||||
const maxLen = maxDuration;
|
const maxLen = maxDuration;
|
||||||
const cleanStart = Math.max(0, Math.min(maxLen, start));
|
const cleanStart = Math.max(0, Math.min(maxLen, start));
|
||||||
@@ -16308,9 +16476,11 @@ const App = () => {
|
|||||||
onSelectRange: handleSelectRange,
|
onSelectRange: handleSelectRange,
|
||||||
onPlayheadSet: handlePlayheadSet,
|
onPlayheadSet: handlePlayheadSet,
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
onSelectTrack: setSelectedTrackId,
|
selectedItemIds: selectedItemIds,
|
||||||
markers: track.markers,
|
onSelectTrack: setSelectedTrackId,
|
||||||
onTrackLaneMouseDown: handleTrackLaneMouseDown,
|
markers: track.markers,
|
||||||
|
onTrackLaneMouseDown: handleTrackLaneMouseDown,
|
||||||
|
onSweepSelectStart: handleSweepSelectStart,
|
||||||
onContextMenu: handleContextMenu,
|
onContextMenu: handleContextMenu,
|
||||||
onClipDragStart: handleClipDragStart,
|
onClipDragStart: handleClipDragStart,
|
||||||
onClipStretchStart: handleClipStretchStart,
|
onClipStretchStart: handleClipStretchStart,
|
||||||
@@ -16359,7 +16529,13 @@ const App = () => {
|
|||||||
}), /*#__PURE__*/React.createElement("div", {
|
}), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "absolute -right-[1px] top-0 bottom-0 w-[2px] bg-amber-400 z-30 pointer-events-auto cursor-ew-resize",
|
className: "absolute -right-[1px] top-0 bottom-0 w-[2px] bg-amber-400 z-30 pointer-events-auto cursor-ew-resize",
|
||||||
onMouseDown: e => handleHandleDragStart(e, 'right')
|
onMouseDown: e => handleHandleDragStart(e, 'right')
|
||||||
})), track.buffer && /*#__PURE__*/React.createElement("div", {
|
})), sweepSelect && sweepTrackIdRef.current === track.id && /*#__PURE__*/React.createElement("div", {
|
||||||
|
className: "absolute top-0 bottom-0 border-l border-r border-amber-400 bg-amber-500/10 z-20 pointer-events-none",
|
||||||
|
style: {
|
||||||
|
left: `${Math.min(sweepSelect.startTime, sweepSelect.endTime) * zoom}px`,
|
||||||
|
width: `${Math.abs(sweepSelect.endTime - sweepSelect.startTime) * zoom}px`
|
||||||
|
}
|
||||||
|
}), track.buffer && /*#__PURE__*/React.createElement("div", {
|
||||||
className: "absolute right-2 top-2 flex items-center gap-1 z-10 opacity-70 hover:opacity-100"
|
className: "absolute right-2 top-2 flex items-center gap-1 z-10 opacity-70 hover:opacity-100"
|
||||||
}, /*#__PURE__*/React.createElement("button", {
|
}, /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: () => handleSplitTrack(track.id),
|
onClick: () => handleSplitTrack(track.id),
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -503,3 +503,9 @@
|
|||||||
- **Các file ảnh hưởng:** `app/static/js/app.jsx` (line 11133-11142), `app/static/js/app.precompiled.js`
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx` (line 11133-11142), `app/static/js/app.precompiled.js`
|
||||||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: Ctrl+drag trên ruler → selection overlay xuất hiện. Shift+click vẫn extend selection.
|
- **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: Ctrl+drag trên ruler → selection overlay xuất hiện. Shift+click vẫn extend selection.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### [2026-07-27 21:47] Task: Ctrl+drag sweep select + multi-item drag/copy
|
||||||
|
- **Tóm tắt thay đổi:** (1) Ctrl+drag trên empty space (track lane) → sweep select (vùng chọn màu amber), mouseup → chọn tất cả items chạm vùng sweep. (2) Selected items highlight (viền vàng dày). (3) Ctrl+drag trên section/MIDI item → duplicate item. (4) Drag trên selected item → move cả nhóm items đã chọn. (5) Ctrl+drag trên selected item → copy cả nhóm.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||||||
|
- **Ghi chú/Test (nếu có):** Cần test: Ctrl+drag empty space → sweep overlay + selected items highlight. Click+drag selected item → cả nhóm move. Ctrl+drag selected item → copy nhóm.
|
||||||
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user