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 KeybedPixelHeight = (128 - PITCH_START) * NoteHeight;
const pixelsPerBeat = rollZoom; const pixelsPerBeat = rollZoom;
const timeSigNum = 4; 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 drawWidth = totalBeats * pixelsPerBeat;
const [notes, setNotes] = React.useState(st.notes || []); 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; 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 => { setNotes(prev => prev.map(n => {
if (n.id !== noteUnderCursor.id) return n; if (n.id !== noteUnderCursor.id) return n;
const newVel = Math.max(0.1, Math.min(1.0, (n.velocity ?? 0.8) + delta)); 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 }; 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)); setNotes(prev => prev.filter(n => n.id !== clickedNote.id));
setSelectedNoteIds(prev => prev.filter(id => id !== clickedNote.id)); setSelectedNoteIds(prev => prev.filter(id => id !== clickedNote.id));
showToast('Đã xóa nốt!', 'info'); 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)); notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes));
setDraggedNote({ mode: 'erase_sweep', visitedPitches: [pitch] }); setDraggedNote({ mode: 'erase_sweep', visitedPitches: [pitch] });
return; return;
@@ -4862,13 +4864,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
const clickedNote = notes[clickedNoteIdx]; const clickedNote = notes[clickedNoteIdx];
let nextSelectedIds; let nextSelectedIds;
if (!selectedNoteIds.includes(clickedNote.id)) { if (!selectedNoteIds.includes(clickedNote.id)) {
if (e.shiftKey) { nextSelectedIds = [clickedNote.id];
nextSelectedIds = [...selectedNoteIds, clickedNote.id]; setSelectedNoteIds(nextSelectedIds);
setSelectedNoteIds(nextSelectedIds);
} else {
nextSelectedIds = [clickedNote.id];
setSelectedNoteIds(nextSelectedIds);
}
} else { } else {
nextSelectedIds = selectedNoteIds; nextSelectedIds = selectedNoteIds;
} }
@@ -4979,24 +4976,25 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
if (draggedNote.mode === 'draw') { if (draggedNote.mode === 'draw') {
const rawDur = beat - draggedNote.startOffsetBeat; const rawDur = beat - draggedNote.startOffsetBeat;
const newDur = getSnapBeat(Math.max(0.125, rawDur), snapVal); 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 || []; 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)) { if (!visited.includes(pitch)) {
const newPitches = [...visited, pitch]; const newPitches = [...visited, pitch];
const totalSpan = Math.max(0.125, beat - draggedNote.initialBeat); const totalSpan = Math.max(0.125, beat - draggedNote.initialBeat);
const perNoteDur = totalSpan / newPitches.length; const perNoteDur = totalSpan / newPitches.length;
const brushIds = draggedNote.brushIds || []; const brushIds = draggedNote.brushIds || [];
setNotes(prev => { 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) => ({ const brushNotes = newPitches.map((p, i) => ({
id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8) + '_' + i, id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8) + '_' + i,
pitch: p, pitch: p,
start_beat: draggedNote.initialBeat + i * perNoteDur, start_beat: draggedNote.initialBeat + i * perNoteDur,
duration_beats: perNoteDur * 0.9, duration_beats: Math.max(0.125, perNoteDur * 0.9),
velocity: 0.8, velocity: 0.8,
pan: 0.0 pan: 0.0
})); }));
@@ -5010,10 +5008,14 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
return; return;
} }
if (draggedNote.mode === 'erase_sweep') { if (draggedNote.mode === 'erase_sweep') {
const erasePitches = draggedNote.visitedPitches || []; const erased = draggedNote.erasedIds || [];
if (!erasePitches.includes(pitch)) { const target = notes.find(n =>
draggedNote.visitedPitches = [...erasePitches, pitch]; n.pitch === pitch && beat >= n.start_beat && beat < n.start_beat + n.duration_beats
setNotes(prev => prev.filter(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; 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 newEnd = getSnapBeat(Math.max(draggedNote.firstStart + 0.125, beat), snapVal);
const range = draggedNote.originalEnd - draggedNote.firstStart; const range = draggedNote.originalEnd - draggedNote.firstStart;
if (range <= 0) return; 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 ids = draggedNote.selectedNoteIds || [];
const firstStart = draggedNote.firstStart; const firstStart = draggedNote.firstStart;
const notesBefore = notesBeforeDragRef.current;
setNotes(prev => prev.map(n => { setNotes(prev => prev.map(n => {
if (!ids.includes(n.id)) return n; if (!ids.includes(n.id)) return n;
const relStart = n.start_beat - firstStart; const orig = notesBefore ? notesBefore.find(o => o.id === n.id) : null;
const relEnd = relStart + n.duration_beats; 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 { return {
...n, ...n,
start_beat: firstStart + relStart * scaleFactor, start_beat: firstStart + relStart * scaleFactor,
@@ -5449,15 +5455,32 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
const sectionStore = {}; 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) // 1. Populate from sessionTabsList (open tabs)
(sessionTabsList || []).forEach(st => { (sessionTabsList || []).forEach(st => {
const serializedTracks = serializeTracksList(st.tracks, secondsPerBar);
sectionStore[st.sectionId] = { sectionStore[st.sectionId] = {
id: st.sectionId, id: st.sectionId,
name: st.name, name: st.name,
is_root: false, is_root: false,
length_bars: st.length_bars || 16.0, length_bars: computeLengthBars(st.tracks, secondsPerBar),
auto_compute_length: st.auto_compute_length !== undefined ? st.auto_compute_length : true, auto_compute_length: true,
tracks: serializeTracksList(st.tracks, secondsPerBar), tracks: serializedTracks,
color: st.color || null color: st.color || null
}; };
}); });
@@ -5473,7 +5496,7 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
id: secId, id: secId,
name: s.name, name: s.name,
is_root: false, is_root: false,
length_bars: s.tracks[0]?.length_bars || 16.0, length_bars: computeLengthBars(s.tracks, secondsPerBar),
auto_compute_length: true, auto_compute_length: true,
tracks: serializeTracksList(s.tracks, secondsPerBar), tracks: serializeTracksList(s.tracks, secondsPerBar),
color: s.color || null color: s.color || null
@@ -5518,7 +5541,16 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
id: "main", id: "main",
name: "MAIN SESSION", name: "MAIN SESSION",
is_root: true, 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, auto_compute_length: true,
tracks: mainTracks tracks: mainTracks
}, },
@@ -7118,9 +7150,20 @@ const App = () => {
const bpmVal = parseInt(bpm) || 120; const bpmVal = parseInt(bpm) || 120;
const secondsPerBeat = 60.0 / bpmVal; const secondsPerBeat = 60.0 / bpmVal;
const secondsPerBar = secondsPerBeat * 4; 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) : []; 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 => { setTracks(prev => prev.map(t => {
if (!t.sections || t.sections.length === 0) return t; if (!t.sections || t.sections.length === 0) return t;
@@ -8277,7 +8320,10 @@ const App = () => {
} }
}); });
(t.midiItems || []).forEach(m => { (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') { 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); const track = tid && currentTracks.find(t => t.id === String(tid) || t.id === 'track_' + tid);
if (!track) return { success: false, error: 'No track found' }; 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 }] : []); 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' }; if (clips.length === 0 && (!track.midiItems || track.midiItems.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))); 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 barDur = 60 / parseInt(bpm || 120) * 4;
const sel = selectionRef.current; const sel = selectionRef.current;
let rawStart, rawEnd; let rawStart, rawEnd;
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
{"original_name": "test.sf2", "uuid": "08692ebf-03e7-4c33-b876-36924024215e", "file": "08692ebf-03e7-4c33-b876-36924024215e.sf2"}
@@ -0,0 +1 @@
{"original_name": "test.sf2", "uuid": "17b3aa85-855e-40fe-a7db-4117681c4497", "file": "17b3aa85-855e-40fe-a7db-4117681c4497.sf2"}
@@ -0,0 +1 @@
{"original_name": "test.sf2", "uuid": "1955f5e5-9986-4b3b-8173-f313b38e4dd9", "file": "1955f5e5-9986-4b3b-8173-f313b38e4dd9.sf2"}
@@ -0,0 +1 @@
{"original_name": "test.sf2", "uuid": "3d0a7b50-a640-4fdf-aa02-b1f034206e86", "file": "3d0a7b50-a640-4fdf-aa02-b1f034206e86.sf2"}
@@ -0,0 +1 @@
{"original_name": "test.sf2", "uuid": "6d1e523e-b9cf-405f-92d0-1b62497554e4", "file": "6d1e523e-b9cf-405f-92d0-1b62497554e4.sf2"}
@@ -0,0 +1 @@
{"original_name": "test.sf2", "uuid": "d790737f-40a2-481a-bc7e-5befaf4a8160", "file": "d790737f-40a2-481a-bc7e-5befaf4a8160.sf2"}