fix: chỉnh sửa section item dù được lưu lại vẫn là item
This commit is contained in:
+90
-39
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user