fix: thay đổi UX của section, piano roll
This commit is contained in:
+279
-64
@@ -3800,7 +3800,7 @@ const SystemManagerModal = ({
|
||||
className: "px-2 py-1 bg-red-700/60 hover:bg-red-600 rounded text-xs"
|
||||
}, "Xóa")))))))));
|
||||
};
|
||||
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNotes }) => {
|
||||
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNotes, onSaveNotes, setSubTabs }) => {
|
||||
const [activeRollTool, setActiveRollTool] = React.useState('select');
|
||||
const [snapVal, setSnapVal] = React.useState('1/16');
|
||||
const [ccMode, setCcMode] = React.useState('velocity');
|
||||
@@ -3810,6 +3810,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
const ccCanvasRef = React.useRef(null);
|
||||
const gridScrollRef = React.useRef(null);
|
||||
const keybedRef = React.useRef(null);
|
||||
const rulerScrollRef = React.useRef(null);
|
||||
|
||||
const NoteHeight = 18;
|
||||
const pixelsPerBeat = rollZoom;
|
||||
@@ -3818,6 +3819,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
const drawWidth = totalBeats * pixelsPerBeat;
|
||||
|
||||
const [notes, setNotes] = React.useState(st.notes || []);
|
||||
const [selectedNoteIds, setSelectedNoteIds] = React.useState([]);
|
||||
const [selectionMarquee, setSelectionMarquee] = React.useState(null); // { startBeat, startPitch, currentBeat, currentPitch }
|
||||
const [draggedNote, setDraggedNote] = React.useState(null); // { mode: 'move'|'resize', idx, startOffsetBeat, originalStart }
|
||||
const [hoveredResizeIdx, setHoveredResizeIdx] = React.useState(-1);
|
||||
|
||||
@@ -3868,6 +3871,53 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Shift + Scroll event listener to adjust velocity
|
||||
React.useEffect(() => {
|
||||
const handleCanvasWheel = (e) => {
|
||||
if (e.shiftKey) {
|
||||
e.preventDefault();
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
const beat = x / pixelsPerBeat;
|
||||
const pitch = 127 - Math.floor(y / NoteHeight);
|
||||
|
||||
// Find note under cursor
|
||||
const noteUnderCursor = notes.find(n => {
|
||||
return pitch === n.pitch && beat >= n.start_beat && beat < n.start_beat + n.duration_beats;
|
||||
});
|
||||
|
||||
const delta = e.deltaY < 0 ? 0.05 : -0.05;
|
||||
|
||||
if (noteUnderCursor) {
|
||||
setNotes(prev => prev.map(n => {
|
||||
if (n.id !== noteUnderCursor.id) return n;
|
||||
const newVel = Math.max(0.1, Math.min(1.0, (n.velocity ?? 0.8) + delta));
|
||||
return { ...n, velocity: newVel };
|
||||
}));
|
||||
} else if (selectedNoteIds.length > 0) {
|
||||
setNotes(prev => prev.map(n => {
|
||||
if (!selectedNoteIds.includes(n.id)) return n;
|
||||
const newVel = Math.max(0.1, Math.min(1.0, (n.velocity ?? 0.8) + delta));
|
||||
return { ...n, velocity: newVel };
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (canvas) {
|
||||
canvas.addEventListener('wheel', handleCanvasWheel, { passive: false });
|
||||
}
|
||||
return () => {
|
||||
if (canvas) {
|
||||
canvas.removeEventListener('wheel', handleCanvasWheel);
|
||||
}
|
||||
};
|
||||
}, [notes, selectedNoteIds, pixelsPerBeat]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
@@ -3913,26 +3963,48 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw notes with velocity layer representation (Hiển thị layer velocity ngay trên nốt)
|
||||
// Draw notes with velocity layer representation
|
||||
notes.forEach((note) => {
|
||||
const x = note.start_beat * pixelsPerBeat;
|
||||
const y = (127 - note.pitch) * NoteHeight;
|
||||
const w = note.duration_beats * pixelsPerBeat;
|
||||
const isSelected = selectedNoteIds.includes(note.id);
|
||||
|
||||
// Draw background of note (lighter yellow)
|
||||
ctx.fillStyle = 'rgba(234, 179, 8, 0.25)';
|
||||
ctx.strokeStyle = '#ca8a04';
|
||||
ctx.lineWidth = 1;
|
||||
// Draw background of note
|
||||
ctx.fillStyle = isSelected ? 'rgba(59, 130, 246, 0.4)' : 'rgba(234, 179, 8, 0.25)';
|
||||
ctx.strokeStyle = isSelected ? '#3b82f6' : '#ca8a04';
|
||||
ctx.lineWidth = isSelected ? 1.5 : 1;
|
||||
ctx.fillRect(x + 1, y + 1, w - 2, NoteHeight - 2);
|
||||
ctx.strokeRect(x + 1, y + 1, w - 2, NoteHeight - 2);
|
||||
|
||||
// Draw velocity layer (solid yellow bar inside, proportional to velocity)
|
||||
// Draw velocity layer (solid yellow/blue bar inside, proportional to velocity)
|
||||
const vel = note.velocity !== undefined ? note.velocity : 0.8;
|
||||
const velW = Math.max(2, (w - 2) * vel);
|
||||
ctx.fillStyle = '#eab308';
|
||||
ctx.fillStyle = isSelected ? '#3b82f6' : '#eab308';
|
||||
ctx.fillRect(x + 1, y + 1, velW, NoteHeight - 2);
|
||||
});
|
||||
}, [notes, snapVal, rollZoom]);
|
||||
|
||||
// Draw selection marquee if active
|
||||
if (selectionMarquee) {
|
||||
const minBeat = Math.min(selectionMarquee.startBeat, selectionMarquee.currentBeat);
|
||||
const maxBeat = Math.max(selectionMarquee.startBeat, selectionMarquee.currentBeat);
|
||||
const minPitch = Math.min(selectionMarquee.startPitch, selectionMarquee.currentPitch);
|
||||
const maxPitch = Math.max(selectionMarquee.startPitch, selectionMarquee.currentPitch);
|
||||
|
||||
const mx = minBeat * pixelsPerBeat;
|
||||
const my = (127 - maxPitch) * NoteHeight;
|
||||
const mw = (maxBeat - minBeat) * pixelsPerBeat;
|
||||
const mh = (maxPitch - minPitch + 1) * NoteHeight;
|
||||
|
||||
ctx.fillStyle = 'rgba(59, 130, 246, 0.15)';
|
||||
ctx.strokeStyle = '#3b82f6';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.fillRect(mx, my, mw, mh);
|
||||
ctx.strokeRect(mx, my, mw, mh);
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
}, [notes, snapVal, rollZoom, selectedNoteIds, selectionMarquee]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const canvas = ccCanvasRef.current;
|
||||
@@ -3994,6 +4066,38 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
const beat = x / pixelsPerBeat;
|
||||
const pitch = 127 - Math.floor(y / NoteHeight);
|
||||
|
||||
// Right click -> Quick delete note!
|
||||
if (e.button === 2) {
|
||||
e.preventDefault();
|
||||
const clickedNote = notes.find(n => {
|
||||
return pitch === n.pitch && beat >= n.start_beat && beat < n.start_beat + n.duration_beats;
|
||||
});
|
||||
if (clickedNote) {
|
||||
setNotes(prev => prev.filter(n => n.id !== clickedNote.id));
|
||||
setSelectedNoteIds(prev => prev.filter(id => id !== clickedNote.id));
|
||||
showToast('Đã xóa nốt nhanh!', 'info');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.button !== 0) return; // Only handle left click
|
||||
|
||||
// Ctrl+Click -> Quick draw note!
|
||||
if (e.ctrlKey) {
|
||||
const start = getSnapBeat(beat, snapVal);
|
||||
const newNote = {
|
||||
id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 5),
|
||||
pitch: pitch,
|
||||
start_beat: start,
|
||||
duration_beats: getSnapDuration(snapVal),
|
||||
velocity: 0.8,
|
||||
pan: 0.0
|
||||
};
|
||||
setNotes(prev => [...prev, newNote]);
|
||||
showToast('Đã vẽ nhanh nốt mới!', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
if (hoveredResizeIdx !== -1) {
|
||||
setDraggedNote({
|
||||
mode: 'resize',
|
||||
@@ -4007,43 +4111,41 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
return pitch === n.pitch && beat >= n.start_beat && beat < n.start_beat + n.duration_beats;
|
||||
});
|
||||
|
||||
if (activeRollTool === 'select') {
|
||||
if (clickedNoteIdx !== -1) {
|
||||
setDraggedNote({
|
||||
mode: 'move',
|
||||
idx: clickedNoteIdx,
|
||||
startOffsetBeat: beat - notes[clickedNoteIdx].start_beat,
|
||||
originalPitch: notes[clickedNoteIdx].pitch
|
||||
});
|
||||
} else {
|
||||
const start = getSnapBeat(beat, snapVal);
|
||||
const newNote = {
|
||||
id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 5),
|
||||
pitch: pitch,
|
||||
start_beat: start,
|
||||
duration_beats: getSnapDuration(snapVal),
|
||||
velocity: 0.8,
|
||||
pan: 0.0
|
||||
};
|
||||
setNotes(prev => [...prev, newNote]);
|
||||
}
|
||||
} else if (activeRollTool === 'eraser') {
|
||||
if (clickedNoteIdx !== -1) {
|
||||
setNotes(prev => prev.filter((_, idx) => idx !== clickedNoteIdx));
|
||||
}
|
||||
} else if (activeRollTool === 'pen') {
|
||||
if (clickedNoteIdx === -1) {
|
||||
const start = getSnapBeat(beat, snapVal);
|
||||
const newNote = {
|
||||
id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 5),
|
||||
pitch: pitch,
|
||||
start_beat: start,
|
||||
duration_beats: getSnapDuration(snapVal),
|
||||
velocity: 0.8,
|
||||
pan: 0.0
|
||||
};
|
||||
setNotes(prev => [...prev, newNote]);
|
||||
if (clickedNoteIdx !== -1) {
|
||||
const clickedNote = notes[clickedNoteIdx];
|
||||
// Selection management
|
||||
if (!selectedNoteIds.includes(clickedNote.id)) {
|
||||
if (e.shiftKey) {
|
||||
setSelectedNoteIds(prev => [...prev, clickedNote.id]);
|
||||
} else {
|
||||
setSelectedNoteIds([clickedNote.id]);
|
||||
}
|
||||
}
|
||||
|
||||
const selectedNotesOffset = notes
|
||||
.filter(n => selectedNoteIds.includes(n.id) || n.id === clickedNote.id)
|
||||
.map(n => ({
|
||||
id: n.id,
|
||||
originalStartBeat: n.start_beat,
|
||||
originalPitch: n.pitch
|
||||
}));
|
||||
|
||||
setDraggedNote({
|
||||
mode: 'move',
|
||||
idx: clickedNoteIdx,
|
||||
startOffsetBeat: beat - clickedNote.start_beat,
|
||||
startOffsetPitch: pitch - clickedNote.pitch,
|
||||
selectedNotesOffset: selectedNotesOffset
|
||||
});
|
||||
} else {
|
||||
// Clicked in empty space -> Start selection marquee
|
||||
setSelectedNoteIds([]);
|
||||
setSelectionMarquee({
|
||||
startBeat: beat,
|
||||
startPitch: pitch,
|
||||
currentBeat: beat,
|
||||
currentPitch: pitch
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4057,8 +4159,28 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
const beat = x / pixelsPerBeat;
|
||||
const pitch = 127 - Math.floor(y / NoteHeight);
|
||||
|
||||
if (selectionMarquee) {
|
||||
const marquee = {
|
||||
...selectionMarquee,
|
||||
currentBeat: beat,
|
||||
currentPitch: pitch
|
||||
};
|
||||
setSelectionMarquee(marquee);
|
||||
|
||||
const minBeat = Math.min(marquee.startBeat, marquee.currentBeat);
|
||||
const maxBeat = Math.max(marquee.startBeat, marquee.currentBeat);
|
||||
const minPitch = Math.min(marquee.startPitch, marquee.currentPitch);
|
||||
const maxPitch = Math.max(marquee.startPitch, marquee.currentPitch);
|
||||
|
||||
const insideIds = notes
|
||||
.filter(n => n.start_beat >= minBeat && n.start_beat <= maxBeat && n.pitch >= minPitch && n.pitch <= maxPitch)
|
||||
.map(n => n.id);
|
||||
|
||||
setSelectedNoteIds(insideIds);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!draggedNote) {
|
||||
// Check if mouse is near the right edge of any note
|
||||
let foundIdx = -1;
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
const n = notes[i];
|
||||
@@ -4090,15 +4212,18 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
duration_beats: newDuration
|
||||
};
|
||||
}));
|
||||
} else {
|
||||
const newStart = getSnapBeat(Math.max(0, beat - draggedNote.startOffsetBeat), snapVal);
|
||||
const newPitch = Math.max(0, Math.min(127, pitch));
|
||||
setNotes(prev => prev.map((n, idx) => {
|
||||
if (idx !== draggedNote.idx) return n;
|
||||
} else if (draggedNote.mode === 'move') {
|
||||
const baseOriginal = draggedNote.selectedNotesOffset.find(o => o.id === notes[draggedNote.idx].id);
|
||||
const deltaBeat = getSnapBeat(beat - draggedNote.startOffsetBeat, snapVal) - baseOriginal.originalStartBeat;
|
||||
const deltaPitch = Math.round(pitch - draggedNote.startOffsetPitch) - baseOriginal.originalPitch;
|
||||
|
||||
setNotes(prev => prev.map(n => {
|
||||
const offset = draggedNote.selectedNotesOffset.find(o => o.id === n.id);
|
||||
if (!offset) return n;
|
||||
return {
|
||||
...n,
|
||||
start_beat: newStart,
|
||||
pitch: newPitch
|
||||
start_beat: getSnapBeat(Math.max(0, offset.originalStartBeat + deltaBeat), snapVal),
|
||||
pitch: Math.max(0, Math.min(127, offset.originalPitch + deltaPitch))
|
||||
};
|
||||
}));
|
||||
}
|
||||
@@ -4106,6 +4231,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
|
||||
const handleGridMouseUp = () => {
|
||||
setDraggedNote(null);
|
||||
setSelectionMarquee(null);
|
||||
};
|
||||
|
||||
const handleContextMenu = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleCCMouseDown = (e) => {
|
||||
@@ -4117,7 +4247,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
const h = rect.height;
|
||||
const beat = x / pixelsPerBeat;
|
||||
|
||||
// Find note near beat or closest note horizontally
|
||||
let noteIdx = notes.findIndex(n => beat >= n.start_beat && beat <= n.start_beat + n.duration_beats);
|
||||
if (noteIdx === -1) {
|
||||
let minDistance = Infinity;
|
||||
@@ -4155,10 +4284,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
|
||||
keys.push(
|
||||
/*#__PURE__*/React.createElement("div", {
|
||||
key: pitch,
|
||||
style: { height: `${NoteHeight}px` },
|
||||
className: `w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition ${isBlack ? 'bg-zinc-950 text-slate-500 border-zinc-900' : 'bg-white text-zinc-800 border-r border-zinc-400'}`
|
||||
}, showLabel && label)
|
||||
key: pitch,
|
||||
style: { height: `${NoteHeight}px` },
|
||||
className: `w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition ${isBlack ? 'bg-zinc-950 text-slate-500 border-zinc-900' : 'bg-white text-zinc-800 border-r border-zinc-400'}`
|
||||
}, showLabel && label)
|
||||
);
|
||||
}
|
||||
return keys;
|
||||
@@ -4168,6 +4297,29 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
if (keybedRef.current) {
|
||||
keybedRef.current.scrollTop = e.currentTarget.scrollTop;
|
||||
}
|
||||
if (rulerScrollRef.current) {
|
||||
rulerScrollRef.current.scrollLeft = e.currentTarget.scrollLeft;
|
||||
}
|
||||
};
|
||||
|
||||
const renderBarLabels = () => {
|
||||
const labels = [];
|
||||
const barsCount = Math.ceil(totalBeats / 4);
|
||||
for (let bar = 0; bar < barsCount; bar++) {
|
||||
const x = bar * 4 * pixelsPerBeat;
|
||||
labels.push(
|
||||
/*#__PURE__*/React.createElement("div", {
|
||||
key: bar,
|
||||
style: {
|
||||
position: 'absolute',
|
||||
left: `${x}px`,
|
||||
top: '4px'
|
||||
},
|
||||
className: "pl-1 border-l border-zinc-700 h-full select-none"
|
||||
}, `Bar ${bar}`)
|
||||
);
|
||||
}
|
||||
return labels;
|
||||
};
|
||||
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
@@ -4204,13 +4356,34 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
key: mode,
|
||||
onClick: () => setCcMode(mode),
|
||||
className: `px-2.5 py-1 rounded capitalize ${ccMode === mode ? 'bg-purple-900/60 text-purple-300 font-bold border border-purple-700' : 'text-zinc-400 hover:text-zinc-200'}`
|
||||
}, mode)))), /*#__PURE__*/React.createElement("button", {
|
||||
}, mode)))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex items-center gap-1"
|
||||
}, /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes),
|
||||
className: "px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
"data-lucide": "save",
|
||||
className: "w-3 h-3"
|
||||
}), "Lưu"), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: onClose,
|
||||
className: "px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition"
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
"data-lucide": "x",
|
||||
className: "w-3 h-3"
|
||||
}), "Đóng")), /*#__PURE__*/React.createElement("div", {
|
||||
}), "Đóng"))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"
|
||||
}), /*#__PURE__*/React.createElement("div", {
|
||||
ref: rulerScrollRef,
|
||||
className: "flex-1 overflow-hidden"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
style: {
|
||||
width: `${drawWidth}px`,
|
||||
height: '100%'
|
||||
},
|
||||
className: "relative h-full font-mono text-[9px] text-zinc-500 font-bold"
|
||||
}, renderBarLabels()))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex-1 flex overflow-hidden min-h-0 relative"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
ref: keybedRef,
|
||||
@@ -4235,6 +4408,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
onMouseMove: handleGridMouseMove,
|
||||
onMouseUp: handleGridMouseUp,
|
||||
onMouseLeave: handleGridMouseUp,
|
||||
onContextMenu: handleContextMenu,
|
||||
className: "absolute inset-0 cursor-crosshair"
|
||||
})))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "h-20 bg-[#161616] border-t border-zinc-900 flex shrink-0"
|
||||
@@ -5794,9 +5968,11 @@ const App = () => {
|
||||
setActiveTab(tabId);
|
||||
};
|
||||
|
||||
const handleUpdateMidiNotes = (tabId, trackId, midiItemId, updatedNotes) => {
|
||||
setSubTabs(prev => prev.map(s => s.id === tabId ? { ...s, notes: updatedNotes } : s));
|
||||
const handleUpdateMidiNotes = (tabId, notes) => {
|
||||
setSubTabs(prev => prev.map(s => s.id === tabId ? { ...s, notes: notes } : s));
|
||||
};
|
||||
|
||||
const handleSaveMidiNotes = (tabId, trackId, midiItemId, updatedNotes) => {
|
||||
const isSectionTrack = sessionTabs.some(s => s.tracks.some(t => t.id === trackId));
|
||||
if (isSectionTrack) {
|
||||
setSessionTabs(prev => prev.map(s => {
|
||||
@@ -5827,6 +6003,32 @@ const App = () => {
|
||||
};
|
||||
}));
|
||||
}
|
||||
showToast('Đã lưu các chỉnh sửa nốt MIDI vào item cha!', 'success');
|
||||
};
|
||||
|
||||
const handleSaveSectionTab = (tabId) => {
|
||||
const tab = sessionTabs.find(s => s.id === tabId);
|
||||
if (!tab) return;
|
||||
const bpmVal = parseInt(bpm) || 120;
|
||||
const secondsPerBeat = 60.0 / bpmVal;
|
||||
const secondsPerBar = secondsPerBeat * 4;
|
||||
const durationSec = (tab.length_bars || 16.0) * secondsPerBar;
|
||||
|
||||
setTracks(prev => prev.map(t => {
|
||||
if (!t.sections || t.sections.length === 0) return t;
|
||||
return {
|
||||
...t,
|
||||
sections: t.sections.map(s => {
|
||||
if (s.sectionId !== tab.sectionId) return s;
|
||||
return {
|
||||
...s,
|
||||
name: tab.name,
|
||||
duration: durationSec
|
||||
};
|
||||
})
|
||||
};
|
||||
}));
|
||||
showToast(`Đã lưu nội dung Section "${tab.name}" vào Main Session!`, 'success');
|
||||
};
|
||||
|
||||
// ── Double-click/Edit Section: open Main Session in new tab ──
|
||||
@@ -11297,7 +11499,18 @@ const App = () => {
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
"data-lucide": "plus",
|
||||
className: "w-3.5 h-3.5"
|
||||
})), /*#__PURE__*/React.createElement("span", null, "Track")), /*#__PURE__*/React.createElement("div", {
|
||||
})), /*#__PURE__*/React.createElement("span", null, "Track")), sessionTabs.some(s => s.id === activeTab) && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
||||
className: "w-[1px] h-5 bg-zinc-700 mx-0.5"
|
||||
}), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => handleSaveSectionTab(activeTab),
|
||||
className: "px-2 py-1 bg-emerald-600 hover:bg-emerald-500 text-white font-semibold rounded text-xs flex items-center gap-1 shadow transition",
|
||||
title: "Lưu Section vào Main Session"
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "inline-flex items-center shrink-0"
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
"data-lucide": "save",
|
||||
className: "w-3.5 h-3.5"
|
||||
})), /*#__PURE__*/React.createElement("span", null, "Lưu Section"))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "w-[1px] h-5 bg-zinc-700 mx-0.5"
|
||||
}), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: handleUndo,
|
||||
@@ -12629,7 +12842,9 @@ const App = () => {
|
||||
bpm: bpm,
|
||||
viewportWidth: viewportWidth,
|
||||
onClose: () => closeSubTab(st.id),
|
||||
onUpdateNotes: handleUpdateMidiNotes
|
||||
onUpdateNotes: handleUpdateMidiNotes,
|
||||
onSaveNotes: handleSaveMidiNotes,
|
||||
setSubTabs: setSubTabs
|
||||
});
|
||||
}
|
||||
const subTrack = tracks.find(t => t.id === st.trackId);
|
||||
|
||||
Reference in New Issue
Block a user