feat: bổ sung phần menu insert items
This commit is contained in:
+210
-1
@@ -184,6 +184,8 @@ const WaveformLane = ({
|
|||||||
onContextMenu,
|
onContextMenu,
|
||||||
onClipDragStart,
|
onClipDragStart,
|
||||||
onClipStretchStart,
|
onClipStretchStart,
|
||||||
|
onSectionItemDragStart,
|
||||||
|
onSectionItemResizeStart,
|
||||||
onSelectionEdgeDragStart,
|
onSelectionEdgeDragStart,
|
||||||
setSelectedClipId,
|
setSelectedClipId,
|
||||||
selectedClipId,
|
selectedClipId,
|
||||||
@@ -555,6 +557,49 @@ const WaveformLane = ({
|
|||||||
canvasRef.current.style.cursor = 'ew-resize';
|
canvasRef.current.style.cursor = 'ew-resize';
|
||||||
return;
|
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 hoveredClip = clips.find(c => time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0));
|
||||||
const isOverClip = !!hoveredClip;
|
const isOverClip = !!hoveredClip;
|
||||||
if (activeTool === 'pen') {
|
if (activeTool === 'pen') {
|
||||||
@@ -647,6 +692,46 @@ const WaveformLane = ({
|
|||||||
}
|
}
|
||||||
return;
|
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));
|
const clickedClip = clips.find(c => time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0));
|
||||||
|
|
||||||
// Set selected clip ID
|
// Set selected clip ID
|
||||||
@@ -3560,6 +3645,8 @@ const App = () => {
|
|||||||
const [menuOpen, setMenuOpen] = useState(null);
|
const [menuOpen, setMenuOpen] = useState(null);
|
||||||
const [selectedClipId, setSelectedClipId] = useState(null); // { trackId, clipId }
|
const [selectedClipId, setSelectedClipId] = useState(null); // { trackId, clipId }
|
||||||
const [stretchedClip, setStretchedClip] = useState(null); // { trackId, clipId, originalDuration, startTime, originalSpeed, beforeSnap }
|
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 [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited
|
||||||
const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId }
|
const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId }
|
||||||
const [editNameInput, setEditNameInput] = useState('');
|
const [editNameInput, setEditNameInput] = useState('');
|
||||||
@@ -6146,6 +6233,10 @@ const App = () => {
|
|||||||
hoveredTrackIdRef.current = hoveredTrackId;
|
hoveredTrackIdRef.current = hoveredTrackId;
|
||||||
const captureTrackSnapshotRef = useRef(null);
|
const captureTrackSnapshotRef = useRef(null);
|
||||||
captureTrackSnapshotRef.current = captureTrackSnapshot;
|
captureTrackSnapshotRef.current = captureTrackSnapshot;
|
||||||
|
const draggedSectionItemRef = useRef(null);
|
||||||
|
draggedSectionItemRef.current = draggedSectionItem;
|
||||||
|
const resizedSectionItemRef = useRef(null);
|
||||||
|
resizedSectionItemRef.current = resizedSectionItem;
|
||||||
let clipSeqCounter = 0;
|
let clipSeqCounter = 0;
|
||||||
const nextClipId = () => `clip_${Date.now()}_${++clipSeqCounter}`;
|
const nextClipId = () => `clip_${Date.now()}_${++clipSeqCounter}`;
|
||||||
|
|
||||||
@@ -6408,6 +6499,122 @@ const App = () => {
|
|||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
};
|
};
|
||||||
}, [zoom]);
|
}, [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 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));
|
||||||
@@ -10352,6 +10559,8 @@ const App = () => {
|
|||||||
onContextMenu: handleContextMenu,
|
onContextMenu: handleContextMenu,
|
||||||
onClipDragStart: handleClipDragStart,
|
onClipDragStart: handleClipDragStart,
|
||||||
onClipStretchStart: handleClipStretchStart,
|
onClipStretchStart: handleClipStretchStart,
|
||||||
|
onSectionItemDragStart: handleSectionItemDragStart,
|
||||||
|
onSectionItemResizeStart: handleSectionItemResizeStart,
|
||||||
onSelectionEdgeDragStart: handleSelectionEdgeDragStart,
|
onSelectionEdgeDragStart: handleSelectionEdgeDragStart,
|
||||||
setSelectedClipId: setSelectedClipId,
|
setSelectedClipId: setSelectedClipId,
|
||||||
selectedClipId: selectedClipId,
|
selectedClipId: selectedClipId,
|
||||||
@@ -10406,7 +10615,7 @@ const App = () => {
|
|||||||
}), /*#__PURE__*/React.createElement("div", {
|
}), /*#__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",
|
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: () => {
|
onMouseEnter: () => {
|
||||||
if (draggedClipRef.current) {
|
if (draggedClipRef.current || draggedSectionItemRef.current) {
|
||||||
setHoveredTrackId(addNewTrack());
|
setHoveredTrackId(addNewTrack());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -184,6 +184,8 @@ const WaveformLane = ({
|
|||||||
onContextMenu,
|
onContextMenu,
|
||||||
onClipDragStart,
|
onClipDragStart,
|
||||||
onClipStretchStart,
|
onClipStretchStart,
|
||||||
|
onSectionItemDragStart,
|
||||||
|
onSectionItemResizeStart,
|
||||||
onSelectionEdgeDragStart,
|
onSelectionEdgeDragStart,
|
||||||
setSelectedClipId,
|
setSelectedClipId,
|
||||||
selectedClipId,
|
selectedClipId,
|
||||||
@@ -555,6 +557,49 @@ const WaveformLane = ({
|
|||||||
canvasRef.current.style.cursor = 'ew-resize';
|
canvasRef.current.style.cursor = 'ew-resize';
|
||||||
return;
|
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 hoveredClip = clips.find(c => time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0));
|
||||||
const isOverClip = !!hoveredClip;
|
const isOverClip = !!hoveredClip;
|
||||||
if (activeTool === 'pen') {
|
if (activeTool === 'pen') {
|
||||||
@@ -647,6 +692,46 @@ const WaveformLane = ({
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check section/MIDI item edge for resize, then body for drag
|
||||||
|
const secItems = track.sections || [];
|
||||||
|
const midiItemsArray = 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 midiItemsArray) {
|
||||||
|
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 midiItemsArray) {
|
||||||
|
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));
|
const clickedClip = clips.find(c => time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0));
|
||||||
|
|
||||||
// Set selected clip ID
|
// Set selected clip ID
|
||||||
@@ -3611,6 +3696,8 @@ const App = () => {
|
|||||||
const [menuOpen, setMenuOpen] = useState(null);
|
const [menuOpen, setMenuOpen] = useState(null);
|
||||||
const [selectedClipId, setSelectedClipId] = useState(null); // { trackId, clipId }
|
const [selectedClipId, setSelectedClipId] = useState(null); // { trackId, clipId }
|
||||||
const [stretchedClip, setStretchedClip] = useState(null); // { trackId, clipId, originalDuration, startTime, originalSpeed, beforeSnap }
|
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 [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited
|
||||||
const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId }
|
const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId }
|
||||||
const [editNameInput, setEditNameInput] = useState('');
|
const [editNameInput, setEditNameInput] = useState('');
|
||||||
@@ -6213,6 +6300,10 @@ const App = () => {
|
|||||||
hoveredTrackIdRef.current = hoveredTrackId;
|
hoveredTrackIdRef.current = hoveredTrackId;
|
||||||
const captureTrackSnapshotRef = useRef(null);
|
const captureTrackSnapshotRef = useRef(null);
|
||||||
captureTrackSnapshotRef.current = captureTrackSnapshot;
|
captureTrackSnapshotRef.current = captureTrackSnapshot;
|
||||||
|
const draggedSectionItemRef = useRef(null);
|
||||||
|
draggedSectionItemRef.current = draggedSectionItem;
|
||||||
|
const resizedSectionItemRef = useRef(null);
|
||||||
|
resizedSectionItemRef.current = resizedSectionItem;
|
||||||
let clipSeqCounter = 0;
|
let clipSeqCounter = 0;
|
||||||
const nextClipId = () => `clip_${Date.now()}_${++clipSeqCounter}`;
|
const nextClipId = () => `clip_${Date.now()}_${++clipSeqCounter}`;
|
||||||
const handleClipDragStart = (trackId, clipId, clickOffset, isDuplicate = false) => {
|
const handleClipDragStart = (trackId, clipId, clickOffset, isDuplicate = false) => {
|
||||||
@@ -6497,6 +6588,122 @@ const App = () => {
|
|||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
};
|
};
|
||||||
}, [zoom]);
|
}, [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 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));
|
||||||
@@ -10913,6 +11120,8 @@ const App = () => {
|
|||||||
onContextMenu: handleContextMenu,
|
onContextMenu: handleContextMenu,
|
||||||
onClipDragStart: handleClipDragStart,
|
onClipDragStart: handleClipDragStart,
|
||||||
onClipStretchStart: handleClipStretchStart,
|
onClipStretchStart: handleClipStretchStart,
|
||||||
|
onSectionItemDragStart: handleSectionItemDragStart,
|
||||||
|
onSectionItemResizeStart: handleSectionItemResizeStart,
|
||||||
onSelectionEdgeDragStart: handleSelectionEdgeDragStart,
|
onSelectionEdgeDragStart: handleSelectionEdgeDragStart,
|
||||||
setSelectedClipId: setSelectedClipId,
|
setSelectedClipId: setSelectedClipId,
|
||||||
selectedClipId: selectedClipId,
|
selectedClipId: selectedClipId,
|
||||||
@@ -10967,7 +11176,7 @@ const App = () => {
|
|||||||
}), /*#__PURE__*/React.createElement("div", {
|
}), /*#__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",
|
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: () => {
|
onMouseEnter: () => {
|
||||||
if (draggedClipRef.current) {
|
if (draggedClipRef.current || draggedSectionItemRef.current) {
|
||||||
setHoveredTrackId(addNewTrack());
|
setHoveredTrackId(addNewTrack());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user