fix: chỉnh sửa section item dù được lưu lại vẫn là item

This commit is contained in:
2026-07-24 22:15:28 +07:00
parent d262f2e778
commit 1c0d35f69b
15 changed files with 107 additions and 50 deletions
+90 -39
View File
@@ -4423,7 +4423,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
const KeybedPixelHeight = (128 - PITCH_START) * NoteHeight;
const pixelsPerBeat = rollZoom;
const timeSigNum = 4;
const totalBeats = Math.max((st.duration || 4) * timeSigNum, 64); // at least 64 beats (16 bars) for scrolling
const noteMaxBeat = (st.notes || []).reduce((max, n) => Math.max(max, (n.start_beat || 0) + (n.duration_beats || 1)), 0);
const totalBeats = Math.max(noteMaxBeat + 16, 64); // at least 64 beats (16 bars) for scrolling
const drawWidth = totalBeats * pixelsPerBeat;
const [notes, setNotes] = React.useState(st.notes || []);
@@ -4548,7 +4549,13 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
const delta = e.deltaY < 0 ? 0.05 : -0.05;
if (noteUnderCursor) {
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 };
}));
} else 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));
@@ -4558,12 +4565,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
}
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 };
}));
}
}
};
@@ -4758,8 +4759,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
setNotes(prev => prev.filter(n => n.id !== clickedNote.id));
setSelectedNoteIds(prev => prev.filter(id => id !== clickedNote.id));
showToast('Đã xóa nốt!', 'info');
return;
}
// Start erase sweep
// Start erase sweep (right-click on empty space only)
notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes));
setDraggedNote({ mode: 'erase_sweep', visitedPitches: [pitch] });
return;
@@ -4862,13 +4864,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
const clickedNote = notes[clickedNoteIdx];
let nextSelectedIds;
if (!selectedNoteIds.includes(clickedNote.id)) {
if (e.shiftKey) {
nextSelectedIds = [...selectedNoteIds, clickedNote.id];
setSelectedNoteIds(nextSelectedIds);
} else {
nextSelectedIds = [clickedNote.id];
setSelectedNoteIds(nextSelectedIds);
}
nextSelectedIds = [clickedNote.id];
setSelectedNoteIds(nextSelectedIds);
} else {
nextSelectedIds = selectedNoteIds;
}
@@ -4979,24 +4976,25 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
if (draggedNote.mode === 'draw') {
const rawDur = beat - draggedNote.startOffsetBeat;
const newDur = getSnapBeat(Math.max(0.125, rawDur), snapVal);
setNotes(prev => prev.map(n => {
if (n.id !== draggedNote.drawNoteId) return n;
return { ...n, duration_beats: newDur };
}));
// Brush: track visited pitches and create evenly-spaced notes
const visited = draggedNote.visitedPitches || [];
if (visited.length <= 1) {
setNotes(prev => prev.map(n => {
if (n.id !== draggedNote.drawNoteId) return n;
return { ...n, duration_beats: newDur };
}));
}
if (!visited.includes(pitch)) {
const newPitches = [...visited, pitch];
const totalSpan = Math.max(0.125, beat - draggedNote.initialBeat);
const perNoteDur = totalSpan / newPitches.length;
const brushIds = draggedNote.brushIds || [];
setNotes(prev => {
const cleaned = prev.filter(n => !brushIds.includes(n.id));
const cleaned = prev.filter(n => !brushIds.includes(n.id) && n.id !== draggedNote.drawNoteId);
const brushNotes = newPitches.map((p, i) => ({
id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8) + '_' + i,
pitch: p,
start_beat: draggedNote.initialBeat + i * perNoteDur,
duration_beats: perNoteDur * 0.9,
duration_beats: Math.max(0.125, perNoteDur * 0.9),
velocity: 0.8,
pan: 0.0
}));
@@ -5010,10 +5008,14 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
return;
}
if (draggedNote.mode === 'erase_sweep') {
const erasePitches = draggedNote.visitedPitches || [];
if (!erasePitches.includes(pitch)) {
draggedNote.visitedPitches = [...erasePitches, pitch];
setNotes(prev => prev.filter(n => !(n.pitch === pitch && beat >= n.start_beat && beat < n.start_beat + n.duration_beats)));
const erased = draggedNote.erasedIds || [];
const target = notes.find(n =>
n.pitch === pitch && beat >= n.start_beat && beat < n.start_beat + n.duration_beats
);
if (target && !erased.includes(target.id)) {
draggedNote.erasedIds = [...erased, target.id];
setNotes(prev => prev.filter(n => n.id !== target.id));
setSelectedNoteIds(prev => prev.filter(id => id !== target.id));
}
return;
}
@@ -5021,13 +5023,17 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
const newEnd = getSnapBeat(Math.max(draggedNote.firstStart + 0.125, beat), snapVal);
const range = draggedNote.originalEnd - draggedNote.firstStart;
if (range <= 0) return;
const scaleFactor = (newEnd - draggedNote.firstStart) / range;
const scaleFactor = Math.max(0.01, (newEnd - draggedNote.firstStart) / range);
const ids = draggedNote.selectedNoteIds || [];
const firstStart = draggedNote.firstStart;
const notesBefore = notesBeforeDragRef.current;
setNotes(prev => prev.map(n => {
if (!ids.includes(n.id)) return n;
const relStart = n.start_beat - firstStart;
const relEnd = relStart + n.duration_beats;
const orig = notesBefore ? notesBefore.find(o => o.id === n.id) : null;
const origStart = orig ? orig.start_beat : n.start_beat;
const origDur = orig ? orig.duration_beats : n.duration_beats;
const relStart = origStart - firstStart;
const relEnd = relStart + origDur;
return {
...n,
start_beat: firstStart + relStart * scaleFactor,
@@ -5449,15 +5455,32 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
const sectionStore = {};
// Helper: compute length_bars from tracks content
const computeLengthBars = (tracksArr, spb) => {
let maxSec = 0;
(tracksArr || []).forEach(tr => {
(tr.clips || []).forEach(c => {
const end = (c.startTime || 0) + (c.buffer ? c.buffer.duration / (c.speed || 1.0) : 4);
if (end > maxSec) maxSec = end;
});
(tr.midiItems || []).forEach(m => {
const end = (m.startTime || 0) + (m.duration || 4);
if (end > maxSec) maxSec = end;
});
});
return Math.ceil((maxSec || 4) / spb);
};
// 1. Populate from sessionTabsList (open tabs)
(sessionTabsList || []).forEach(st => {
const serializedTracks = serializeTracksList(st.tracks, secondsPerBar);
sectionStore[st.sectionId] = {
id: st.sectionId,
name: st.name,
is_root: false,
length_bars: st.length_bars || 16.0,
auto_compute_length: st.auto_compute_length !== undefined ? st.auto_compute_length : true,
tracks: serializeTracksList(st.tracks, secondsPerBar),
length_bars: computeLengthBars(st.tracks, secondsPerBar),
auto_compute_length: true,
tracks: serializedTracks,
color: st.color || null
};
});
@@ -5473,7 +5496,7 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
id: secId,
name: s.name,
is_root: false,
length_bars: s.tracks[0]?.length_bars || 16.0,
length_bars: computeLengthBars(s.tracks, secondsPerBar),
auto_compute_length: true,
tracks: serializeTracksList(s.tracks, secondsPerBar),
color: s.color || null
@@ -5518,7 +5541,16 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
id: "main",
name: "MAIN SESSION",
is_root: true,
length_bars: 16.0,
length_bars: (() => {
let maxBar = 16.0;
(mainTracks || []).forEach(t => {
(t.items || []).forEach(item => {
const end = (item.start_bar || 0) + (item.duration_bars || 4);
if (end > maxBar) maxBar = end;
});
});
return maxBar;
})(),
auto_compute_length: true,
tracks: mainTracks
},
@@ -7118,9 +7150,20 @@ const App = () => {
const bpmVal = parseInt(bpm) || 120;
const secondsPerBeat = 60.0 / bpmVal;
const secondsPerBar = secondsPerBeat * 4;
const durationSec = (tab.length_bars || 16.0) * secondsPerBar;
const contentTracks = tab.tracks ? tab.tracks.filter(tr => tr.clips?.length > 0 || tr.midiItems?.length > 0) : [];
let maxEndTime = 0;
(contentTracks || []).forEach(tr => {
(tr.clips || []).forEach(c => {
const end = (c.startTime || 0) + (c.buffer ? c.buffer.duration / (c.speed || 1.0) : 4);
if (end > maxEndTime) maxEndTime = end;
});
(tr.midiItems || []).forEach(m => {
const end = (m.startTime || 0) + (m.duration || 4);
if (end > maxEndTime) maxEndTime = end;
});
});
const durationSec = Math.max(maxEndTime, 4 * secondsPerBar);
setTracks(prev => prev.map(t => {
if (!t.sections || t.sections.length === 0) return t;
@@ -8277,7 +8320,10 @@ const App = () => {
}
});
(t.midiItems || []).forEach(m => {
max = Math.max(max, m.startTime + (m.duration || 4));
max = Math.max(max, (m.startTime || 0) + (m.duration || 4));
});
(t.sections || []).forEach(s => {
max = Math.max(max, (s.start || 0) + (s.duration || 4));
});
});
if (recordingState === 'RECORDING' || recordingState === 'COUNT_IN') {
@@ -12259,8 +12305,13 @@ const App = () => {
const track = tid && currentTracks.find(t => t.id === String(tid) || t.id === 'track_' + tid);
if (!track) return { success: false, error: 'No track found' };
const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ id: 'default_' + track.id, buffer: track.buffer, startTime: track.startTime || 0, name: track.name }] : []);
if (clips.length === 0) return { success: false, error: 'Track has no audio clips' };
const totalDuration = Math.max(...clips.map(c => (c.startTime || 0) + (c.buffer ? c.buffer.duration / (c.speed || 1.0) : 0)));
if (clips.length === 0 && (!track.midiItems || track.midiItems.length === 0)) return { success: false, error: 'Track has no audio clips' };
const beatsPerSec = parseFloat(bpm || 120) / 60;
const totalDuration = Math.max(
clips.length > 0 ? Math.max(...clips.map(c => (c.startTime || 0) + (c.buffer ? c.buffer.duration / (c.speed || 1.0) : 0))) : 0,
...(track.midiItems || []).map(m => (m.startTime || 0) + (m.duration || 4)),
...(track.sections || []).map(s => (s.start || 0) + (s.duration || 4))
);
const barDur = 60 / parseInt(bpm || 120) * 4;
const sel = selectionRef.current;
let rawStart, rawEnd;