feat: bổ sung phần menu insert items
This commit is contained in:
+210
-1
@@ -184,6 +184,8 @@ const WaveformLane = ({
|
||||
onContextMenu,
|
||||
onClipDragStart,
|
||||
onClipStretchStart,
|
||||
onSectionItemDragStart,
|
||||
onSectionItemResizeStart,
|
||||
onSelectionEdgeDragStart,
|
||||
setSelectedClipId,
|
||||
selectedClipId,
|
||||
@@ -555,6 +557,49 @@ const WaveformLane = ({
|
||||
canvasRef.current.style.cursor = 'ew-resize';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check section/MIDI item hover for resize or drag
|
||||
const allSections = track.sections || [];
|
||||
const allMidiItems = track.midiItems || [];
|
||||
const sectionTolerance = 8 / zoom;
|
||||
let foundSectionItem = null;
|
||||
let sectionItemEdge = null;
|
||||
const checkEdge = (item, startTime, dur) => {
|
||||
const leftEdge = Math.abs(time - startTime) <= sectionTolerance;
|
||||
const rightEdge = Math.abs(time - (startTime + dur)) <= sectionTolerance;
|
||||
if (leftEdge || rightEdge) return leftEdge ? 'left' : 'right';
|
||||
return null;
|
||||
};
|
||||
for (const sec of allSections) {
|
||||
const edge = checkEdge(sec, sec.start, sec.duration);
|
||||
if (edge) { foundSectionItem = { type: 'section', item: sec }; sectionItemEdge = edge; break; }
|
||||
}
|
||||
if (!foundSectionItem) {
|
||||
for (const midi of allMidiItems) {
|
||||
const edge = checkEdge(midi, midi.startTime, midi.duration);
|
||||
if (edge) { foundSectionItem = { type: 'midiItem', item: midi }; sectionItemEdge = edge; break; }
|
||||
}
|
||||
}
|
||||
if (foundSectionItem && sectionItemEdge) {
|
||||
canvasRef.current.style.cursor = 'ew-resize';
|
||||
return;
|
||||
}
|
||||
// Check body hover for drag
|
||||
if (!foundSectionItem) {
|
||||
for (const sec of allSections) {
|
||||
if (time >= sec.start && time < sec.start + sec.duration) { foundSectionItem = { type: 'section', item: sec }; break; }
|
||||
}
|
||||
}
|
||||
if (!foundSectionItem) {
|
||||
for (const midi of allMidiItems) {
|
||||
if (time >= midi.startTime && time < midi.startTime + midi.duration) { foundSectionItem = { type: 'midiItem', item: midi }; break; }
|
||||
}
|
||||
}
|
||||
if (foundSectionItem) {
|
||||
canvasRef.current.style.cursor = 'grab';
|
||||
return;
|
||||
}
|
||||
|
||||
const hoveredClip = clips.find(c => time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0));
|
||||
const isOverClip = !!hoveredClip;
|
||||
if (activeTool === 'pen') {
|
||||
@@ -647,6 +692,46 @@ const WaveformLane = ({
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check section/MIDI item edge for resize, then body for drag
|
||||
const secItems = track.sections || [];
|
||||
const midiItems = track.midiItems || [];
|
||||
const secTol = 8 / zoom;
|
||||
let hitItem = null;
|
||||
let hitEdge = null;
|
||||
for (const sec of secItems) {
|
||||
if (Math.abs(time - sec.start) <= secTol) { hitItem = { type: 'section', id: sec.id, start: sec.start, dur: sec.duration }; hitEdge = 'left'; break; }
|
||||
if (Math.abs(time - (sec.start + sec.duration)) <= secTol) { hitItem = { type: 'section', id: sec.id, start: sec.start, dur: sec.duration }; hitEdge = 'right'; break; }
|
||||
}
|
||||
if (!hitItem) {
|
||||
for (const midi of midiItems) {
|
||||
if (Math.abs(time - midi.startTime) <= secTol) { hitItem = { type: 'midiItem', id: midi.id, start: midi.startTime, dur: midi.duration }; hitEdge = 'left'; break; }
|
||||
if (Math.abs(time - (midi.startTime + midi.duration)) <= secTol) { hitItem = { type: 'midiItem', id: midi.id, start: midi.startTime, dur: midi.duration }; hitEdge = 'right'; break; }
|
||||
}
|
||||
}
|
||||
if (hitItem && hitEdge) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onSectionItemResizeStart) onSectionItemResizeStart(track.id, hitItem.type, hitItem.id, hitEdge, time);
|
||||
return;
|
||||
}
|
||||
if (!hitItem) {
|
||||
for (const sec of secItems) {
|
||||
if (time >= sec.start && time < sec.start + sec.duration) { hitItem = { type: 'section', id: sec.id, start: sec.start }; break; }
|
||||
}
|
||||
}
|
||||
if (!hitItem) {
|
||||
for (const midi of midiItems) {
|
||||
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) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onSectionItemDragStart) onSectionItemDragStart(track.id, hitItem.type, hitItem.id, time - hitItem.start);
|
||||
return;
|
||||
}
|
||||
|
||||
const clickedClip = clips.find(c => time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0));
|
||||
|
||||
// Set selected clip ID
|
||||
@@ -3560,6 +3645,8 @@ const App = () => {
|
||||
const [menuOpen, setMenuOpen] = useState(null);
|
||||
const [selectedClipId, setSelectedClipId] = useState(null); // { trackId, clipId }
|
||||
const [stretchedClip, setStretchedClip] = useState(null); // { trackId, clipId, originalDuration, startTime, originalSpeed, beforeSnap }
|
||||
const [draggedSectionItem, setDraggedSectionItem] = useState(null);
|
||||
const [resizedSectionItem, setResizedSectionItem] = useState(null);
|
||||
const [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited
|
||||
const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId }
|
||||
const [editNameInput, setEditNameInput] = useState('');
|
||||
@@ -6146,6 +6233,10 @@ const App = () => {
|
||||
hoveredTrackIdRef.current = hoveredTrackId;
|
||||
const captureTrackSnapshotRef = useRef(null);
|
||||
captureTrackSnapshotRef.current = captureTrackSnapshot;
|
||||
const draggedSectionItemRef = useRef(null);
|
||||
draggedSectionItemRef.current = draggedSectionItem;
|
||||
const resizedSectionItemRef = useRef(null);
|
||||
resizedSectionItemRef.current = resizedSectionItem;
|
||||
let clipSeqCounter = 0;
|
||||
const nextClipId = () => `clip_${Date.now()}_${++clipSeqCounter}`;
|
||||
|
||||
@@ -6408,6 +6499,122 @@ const App = () => {
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [zoom]);
|
||||
|
||||
// ── Section / MIDI Item Drag Start ──
|
||||
const handleSectionItemDragStart = (trackId, itemType, itemId, clickOffset) => {
|
||||
setDraggedSectionItem({ trackId, itemType, itemId, clickOffset });
|
||||
};
|
||||
|
||||
// ── Section / MIDI Item Resize Start ──
|
||||
const handleSectionItemResizeStart = (trackId, itemType, itemId, side, clickTime) => {
|
||||
const track = tracks.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;
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const scrollLeft = wrapper.scrollLeft;
|
||||
const mouseX = e.clientX - rect.left + scrollLeft;
|
||||
const time = mouseX / zoom;
|
||||
const newStart = Math.max(0, time - drag.clickOffset);
|
||||
const targetTrackId = hoveredTrackIdRef.current || drag.trackId;
|
||||
setTracks(prev => prev.map(t => {
|
||||
const items = drag.itemType === 'section' ? (t.sections || []) : (t.midiItems || []);
|
||||
const updatedItems = items.filter(it => it.id !== drag.itemId);
|
||||
if (t.id === targetTrackId) {
|
||||
const movedItem = items.find(it => it.id === drag.itemId);
|
||||
if (movedItem) {
|
||||
updatedItems.push(drag.itemType === 'section'
|
||||
? { ...movedItem, start: newStart }
|
||||
: { ...movedItem, startTime: newStart });
|
||||
} else {
|
||||
if (drag.itemType === 'section') {
|
||||
updatedItems.push({ id: drag.itemId, name: 'Section', start: newStart, duration: 4, color: '#06b6d4' });
|
||||
} else {
|
||||
updatedItems.push({ id: drag.itemId, name: 'MIDI Item', startTime: newStart, duration: 4, notes: [], color: '#a78bfa' });
|
||||
}
|
||||
}
|
||||
}
|
||||
return 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]);
|
||||
|
||||
// ── 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;
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const scrollLeft = wrapper.scrollLeft;
|
||||
const mouseX = e.clientX - rect.left + scrollLeft;
|
||||
const time = mouseX / zoom;
|
||||
setTracks(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 newStart = 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 newDuration = Math.max(0.1, time - resize.originalStart);
|
||||
items[idx] = { ...item, duration: newDuration };
|
||||
}
|
||||
return resize.itemType === 'section'
|
||||
? { ...t, sections: items }
|
||||
: { ...t, midiItems: items };
|
||||
}));
|
||||
};
|
||||
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]);
|
||||
|
||||
const handleSelectRange = (start, end, reset) => {
|
||||
const maxLen = maxDuration;
|
||||
const cleanStart = Math.max(0, Math.min(maxLen, start));
|
||||
@@ -10352,6 +10559,8 @@ const App = () => {
|
||||
onContextMenu: handleContextMenu,
|
||||
onClipDragStart: handleClipDragStart,
|
||||
onClipStretchStart: handleClipStretchStart,
|
||||
onSectionItemDragStart: handleSectionItemDragStart,
|
||||
onSectionItemResizeStart: handleSectionItemResizeStart,
|
||||
onSelectionEdgeDragStart: handleSelectionEdgeDragStart,
|
||||
setSelectedClipId: setSelectedClipId,
|
||||
selectedClipId: selectedClipId,
|
||||
@@ -10406,7 +10615,7 @@ const App = () => {
|
||||
}), /*#__PURE__*/React.createElement("div", {
|
||||
className: "h-[48px] flex items-center justify-center text-xs text-zinc-500 hover:bg-zinc-900/20 cursor-pointer select-none border-t border-dashed border-zinc-800",
|
||||
onMouseEnter: () => {
|
||||
if (draggedClipRef.current) {
|
||||
if (draggedClipRef.current || draggedSectionItemRef.current) {
|
||||
setHoveredTrackId(addNewTrack());
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user