FIX: sửa lỗi không hiển thị đúng nội dung của SECTION-TAB trong section item

This commit is contained in:
2026-08-07 18:58:29 +07:00
parent 1d9b426d15
commit c3182f6baa
4 changed files with 414 additions and 53 deletions
+236 -22
View File
@@ -2579,6 +2579,10 @@ const WaveformLane = ({
notes.forEach(note => { notes.forEach(note => {
const beatSec = 60.0 / (parseInt(bpm) || 120); const beatSec = 60.0 / (parseInt(bpm) || 120);
const noteStartSec = (note.start_beat || 0) * beatSec; const noteStartSec = (note.start_beat || 0) * beatSec;
// CLIP theo item duration: item b KÉO NGN (trim duration 4 bars
// nhưng notes vn còn 8 bars) note ngoài duration KHÔNG v
// canvas MAIN phi hin th đúng phn đã trim (user 08:20)
if (noteStartSec >= (item.duration || 0) - 0.01) return;
const noteDurSec = Math.max(0.02, (note.duration_beats || 0.25) * beatSec); const noteDurSec = Math.max(0.02, (note.duration_beats || 0.25) * beatSec);
const noteStartLocal = itemStartLocal + noteStartSec * zoom; const noteStartLocal = itemStartLocal + noteStartSec * zoom;
const nw = noteDurSec * zoom; const nw = noteDurSec * zoom;
@@ -2892,6 +2896,10 @@ const WaveformLane = ({
if (hitItem && !e.altKey && !e.shiftKey) { if (hitItem && !e.altKey && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// Chn TRACK cha item (user 07:55: Ctrl+V phi paste vào track đang
// select click item không qua onTrackLaneMouseDown selectedTrackId
// gi track cũ paste sai track)
if (onSelectTrack) onSelectTrack(track.id);
if (e.ctrlKey) { if (e.ctrlKey) {
// Ctrl+Click: toggle selection (add/remove from group) + set pending copy-drag // Ctrl+Click: toggle selection (add/remove from group) + set pending copy-drag
var preToggle = selectedItemIds ? new Set(selectedItemIds) : new Set(); var preToggle = selectedItemIds ? new Set(selectedItemIds) : new Set();
@@ -3084,10 +3092,12 @@ const WaveformLane = ({
if (time >= sec.start && time < sec.start + sec.duration) { hitSectionId = sec.id; break; } if (time >= sec.start && time < sec.start + sec.duration) { hitSectionId = sec.id; break; }
} }
// Detect MIDI item dưi chut (user 07:25 Cut/Copy item phi copy // Detect MIDI item dưi chut (user 07:25 Cut/Copy item phi copy
// ĐÚNG item trưc đây context menu ch biết track copy nhm buffer) // ĐÚNG item bounds × secondsPerBar (m.duration tính BEATS như
// contextMenuDelete 17452) thiếu × spb detect miss copy nhm track)
const _spb = (60.0 / (parseInt(bpm) || 120)) * 4;
let hitMidiId = null; let hitMidiId = null;
for (const m of (track.midiItems || [])) { for (const m of (track.midiItems || [])) {
if (time >= m.startTime && time < m.startTime + (m.duration || 4)) { hitMidiId = m.id; break; } if (time >= m.startTime && time < m.startTime + (m.duration || 4) * _spb) { hitMidiId = m.id; break; }
} }
// Detect clip dưi chut // Detect clip dưi chut
let hitClipId = null; let hitClipId = null;
@@ -14949,10 +14959,12 @@ const App = () => {
} }
const prevSub = subTabsRef.current.find(s => s.id === prevTab); const prevSub = subTabsRef.current.find(s => s.id === prevTab);
if (prevSub && prevSub.isPlaying) { if (prevSub && prevSub.isPlaying) {
// PIANO ROLL tab: KHÔNG stop âm khi ri tab (SF notes đã schedule // Chuyn sang SUB-TAB KHÁC (piano roll khác/audio tab) STOP âm tab
// tiếp tc kêu t nhiên user: chuyn tab qua li KHÔNG đưc câm). // cũ play ĐÚNG ni dung TNG TAB (user 07:30: piano roll tab 1
// Audio sub-tab (buffer source riêng): vn stop như cũ. // tab 2 không đưc nghe âm tab 1). V MAIN/SECTION gi lut cũ
if (prevSub.type !== 'PIANO_ROLL') { // (04:15+ âm tiếp/resume).
const _newIsSub = activeTab && activeTab !== 'main' && !activeTab.startsWith('session_');
if (prevSub.type !== 'PIANO_ROLL' || _newIsSub) {
try { stopAllPlayback(); } catch (e) {} try { stopAllPlayback(); } catch (e) {}
} }
setSubTabs(prev => prev.map(s => s.id === prevTab ? { ...s, isPlaying: false } : s)); setSubTabs(prev => prev.map(s => s.id === prevTab ? { ...s, isPlaying: false } : s));
@@ -15770,6 +15782,23 @@ const App = () => {
const handleDeleteSelectedItemsRef = useRef(() => {}); const handleDeleteSelectedItemsRef = useRef(() => {});
handleDeleteSelectedItemsRef.current = (idsToDelete) => { handleDeleteSelectedItemsRef.current = (idsToDelete) => {
const count = idsToDelete.size; const count = idsToDelete.size;
// Close piano roll tabs ca midi items b xóa (user 07:40 áp c
// SECTION-TAB: xóa midi item close tab đã m ca item đó)
setSubTabs(prev => {
const next = prev.filter(s => !(s.target_id && idsToDelete.has(s.target_id)));
// CH v main khi tab đang active là PIANO ROLL (midi_*) b close
// KHÔNG đá SECTION-TAB v main (user 07:45)
if (activeTabRef.current && activeTabRef.current.startsWith('midi_') && !next.some(s => s.id === activeTabRef.current)) {
setActiveTab('main');
}
return next;
});
// Sync ni dung section tab section item trên MAIN (canvas v li
// user 07:45) setTimeout 0: chy SAU updateActiveTracks (data mi)
if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
const _tabId = activeTabRef.current;
setTimeout(() => { try { syncSectionTabToMain(_tabId); } catch (e) {} }, 0);
}
setSelectedItemIds(new Set()); setSelectedItemIds(new Set());
updateActiveTracks(prev => prev.map(t => { updateActiveTracks(prev => prev.map(t => {
let changed = false; let changed = false;
@@ -16100,6 +16129,58 @@ const App = () => {
} : s)); } : s));
showToast(`Đã lặp vùng chọn ${loopCount} lần.`, 'success'); showToast(`Đã lặp vùng chọn ${loopCount} lần.`, 'success');
}; };
// Item copy/cut helpers (Ctrl+C/X user 07:50: phi copy/cut ITEM đưc
// chn, không phi track)
const findItemContext = (itemId) => {
for (const t of (activeTracksRef.current || [])) {
if ((t.midiItems || []).some(m => m.id === itemId)) return { trackId: t.id, itemType: 'midi' };
if ((t.clips || []).some(c => c.id === itemId)) return { trackId: t.id, itemType: 'clip' };
}
return null;
};
const copyItemToClipboard = (trackId, itemId, itemType) => {
// Dùng REF (không phi activeTracks closure cũ): Ctrl+C/X chy trong keydown
// effect deps [] closure gi activeTracks RENDER ĐU item TO SAU phiên
// (vd midi_1786099252336) không tn ti trong closure cũ copy fail ct
// nhm track (user 08:10 log [CutItem] found= 2/midi nhưng không ct).
const trk = (activeTracksRef.current || []).find(t => t.id === trackId);
if (!trk) return false;
const isMidi = itemType === 'midi' || (!itemType && (trk.midiItems || []).some(m => m.id === itemId));
if (isMidi) {
const item = (trk.midiItems || []).find(m => m.id === itemId);
if (!item) return false;
// duration midiItem tính GIÂY (insertMidiItem = 4*spb scheduling dùng
// seconds) KHÔNG nhân (60/bpm) nhân làm duration gp đôi block
// paste b dài/ngn sai (user 07:55).
clipboardRef.current = {
type: 'midi',
notes: (item.notes || []).map(n => ({ ...n })),
duration: item.duration || 4,
name: item.name || 'MIDI Item',
color: item.color || '#a855f7'
};
window.globalStudioClipboard = clipboardRef.current;
return true;
}
const item = (trk.clips || []).find(c => c.id === itemId);
if (item && item.buffer) {
clipboardRef.current = {
buffer: item.buffer,
name: item.name || trk.name,
volumeDb: trk.volumeDb,
pan: trk.pan,
color: item.color || trk.color,
sampleRate: item.buffer.sampleRate,
channels: item.buffer.numberOfChannels,
speed: item.speed || 1.0
};
window.globalStudioClipboard = clipboardRef.current;
return true;
}
return false;
};
useEffect(() => { useEffect(() => {
const handler = e => { const handler = e => {
// Bypass global hotkeys when typing inside input/textarea/contentEditable elements // Bypass global hotkeys when typing inside input/textarea/contentEditable elements
@@ -16323,17 +16404,47 @@ const App = () => {
} }
if (ctrl && !alt && e.key === 'c') { if (ctrl && !alt && e.key === 'c') {
e.preventDefault(); e.preventDefault();
const selItems = selectedItemIdsRef.current;
if (selItems && selItems.size > 0) {
// Copy ITEM đu tiên đưc chn (user 07:50 Ctrl+C phi copy item)
const firstId = selItems.values().next().value;
const found = findItemContext(firstId);
if (found && copyItemToClipboard(found.trackId, firstId, found.itemType)) {
showToast('Đã sao chép item.', 'info');
} else {
handleCopyTrack(); handleCopyTrack();
}
} else {
handleCopyTrack();
}
return; return;
} }
if (ctrl && !alt && e.key === 'x') { if (ctrl && !alt && e.key === 'x') {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); // chn browser cut (user 07:55 Ctrl-X b capture)
const selItems = selectedItemIdsRef.current;
if (selItems && selItems.size > 0) {
// Cut ITEM đu tiên đưc chn: copy + xóa item (+ close piano roll
// tab + sync section handleDeleteSelectedItemsRef đã x lý)
const firstId = selItems.values().next().value;
const found = findItemContext(firstId);
console.log('[CutItem] Ctrl+X — itemId=', firstId, 'found=', found ? found.trackId + '/' + found.itemType : 'null', 'selSize=', selItems.size);
if (found && copyItemToClipboard(found.trackId, firstId, found.itemType)) {
handleDeleteSelectedItemsRef.current(new Set([firstId]));
showToast('Đã cắt item.', 'info');
} else {
handleCutTrack(); handleCutTrack();
}
} else {
handleCutTrack();
}
return; return;
} }
if (ctrl && !alt && e.key === 'v') { if (ctrl && !alt && e.key === 'v') {
e.preventDefault(); e.preventDefault();
handlePasteTrack(); // Dùng REF effect deps [] handlePasteTrack closure CŨ (selectedTrackId
// stale = '1' paste sai track user 08:05)
handlePasteTrackRef.current();
return; return;
} }
if (e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Del') { if (e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Del') {
@@ -16386,10 +16497,10 @@ const App = () => {
if (ctrl && !alt && e.key === 's') { if (ctrl && !alt && e.key === 's') {
e.preventDefault(); e.preventDefault();
const curTab = activeTabRef.current; const curTab = activeTabRef.current;
if (curTab === 'main') { if (curTab === 'main' || (curTab && curTab.startsWith('session_'))) {
// handled by main handler // MAIN (hoc SECTION): save project + save TT C dirty sub-tabs
} else if (curTab.startsWith('session_')) { // (piano roll/audio/section) user 07:35: nhn lưu MAIN phi lưu
// Main session: save project + save all dirty sub-tabs // c dirty tab (trưc đây nhánh main RNG dirty piano roll b mt).
handleSaveProject(); handleSaveProject();
subTabsRef.current.filter(s => s.isDirty).forEach(st => { subTabsRef.current.filter(s => s.isDirty).forEach(st => {
if (st.type === 'PIANO_ROLL') { if (st.type === 'PIANO_ROLL') {
@@ -16403,12 +16514,12 @@ const App = () => {
updateActiveTracks(prev => prev.map(t => t.id === st.trackId ? { ...t, buffer: st.buffer } : t)); updateActiveTracks(prev => prev.map(t => t.id === st.trackId ? { ...t, buffer: st.buffer } : t));
} }
} }
showToast('Đã lưu', 'success');
}); });
} else if (curTab.startsWith('session_')) { if (curTab.startsWith('session_')) {
// Section tab: save section // SECTION-TAB: cũng lưu section hin ti
handleSaveSectionTabRef.current(curTab); handleSaveSectionTabRef.current(curTab);
showToast('Đã lưu Section', 'success'); }
showToast('Đã lưu dự án + các tab đã sửa', 'success');
} else { } else {
// Sub-tab: save current tab // Sub-tab: save current tab
const st = subTabsRef.current.find(s => s.id === curTab); const st = subTabsRef.current.find(s => s.id === curTab);
@@ -16737,10 +16848,25 @@ const App = () => {
showToast('Đã lưu các chỉnh sửa nốt MIDI vào item cha!', 'success'); showToast('Đã lưu các chỉnh sửa nốt MIDI vào item cha!', 'success');
}; };
// Sync ni dung SECTION-TAB section item trên MAIN (sec.tracks) cp nht
// canvas section item ngay khi thay đi ni dung tab (user 07:45 xóa item
// trong SECTION-TAB canvas section item MAIN phi v li). KHÔNG đi
// duration (0505 gi kích thưc user resize).
const syncSectionTabToMain = (tabId) => {
const tab = sessionTabsRef.current.find(s => s.id === tabId);
if (!tab || !tab.sectionId) return;
setTracks(prev => prev.map(t => ({
...t,
sections: (t.sections || []).map(s => {
if (s.sectionId !== tab.sectionId && s.id !== tab.sectionId) return s;
return { ...s, tracks: tab.tracks };
})
})));
};
const handleSaveSectionTab = async (tabId) => { const handleSaveSectionTab = async (tabId) => {
const tab = sessionTabs.find(s => s.id === tabId); const tab = sessionTabs.find(s => s.id === tabId);
if (!tab) return; if (!tab) return;
// Upload buffer-only clips (chưa có serverFileId upload sm tht bi/ // Upload buffer-only clips (chưa có serverFileId upload sm tht bi/
// race) TRƯC khi ghi vào section item serialized AUDIO_ITEM có file // race) TRƯC khi ghi vào section item serialized AUDIO_ITEM có file
// reload không mt audioclip. // reload không mt audioclip.
@@ -17471,12 +17597,29 @@ const App = () => {
const afterSnap = captureTrackSnapshot(tid); const afterSnap = captureTrackSnapshot(tid);
pushAction('DELETE_SECTION', tid, beforeSnap, afterSnap); pushAction('DELETE_SECTION', tid, beforeSnap, afterSnap);
closeContextMenu(); closeContextMenu();
// Sync section tab section item trên MAIN (canvas v li 07:45)
if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
setTimeout(() => { try { syncSectionTabToMain(activeTabRef.current); } catch (e) {} }, 0);
}
showToast('Đã xoá section item.', 'info'); showToast('Đã xoá section item.', 'info');
return; return;
} }
if (clickedMidi) { if (clickedMidi) {
const beforeSnap = captureTrackSnapshot(tid); const beforeSnap = captureTrackSnapshot(tid);
// Close piano roll tab m ca midi item này (user 07:35)
setSubTabs(prev => {
const next = prev.filter(s => s.target_id !== clickedMidi.id);
// CH v main khi tab active là PIANO ROLL b close gi SECTION-TAB
if (activeTabRef.current && activeTabRef.current.startsWith('midi_') && !next.some(s => s.id === activeTabRef.current)) {
setActiveTab('main');
}
return next;
});
// Sync section tab section item trên MAIN (canvas v li 07:45)
if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
setTimeout(() => { try { syncSectionTabToMain(activeTabRef.current); } catch (e) {} }, 0);
}
updateActiveTracks(prev => prev.map(t => { updateActiveTracks(prev => prev.map(t => {
if (t.id !== tid) return t; if (t.id !== tid) return t;
return { return {
@@ -17507,6 +17650,10 @@ const App = () => {
const afterSnap = captureTrackSnapshot(tid); const afterSnap = captureTrackSnapshot(tid);
pushAction('DELETE_CLIP', tid, beforeSnap, afterSnap); pushAction('DELETE_CLIP', tid, beforeSnap, afterSnap);
closeContextMenu(); closeContextMenu();
// Sync section tab section item trên MAIN (canvas v li 07:45)
if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
setTimeout(() => { try { syncSectionTabToMain(activeTabRef.current); } catch (e) {} }, 0);
}
showToast('Đã xoá audio clip.', 'info'); showToast('Đã xoá audio clip.', 'info');
return; return;
} }
@@ -17559,11 +17706,10 @@ const App = () => {
const trk = activeTracks.find(t => t.id === contextMenu.trackId); const trk = activeTracks.find(t => t.id === contextMenu.trackId);
const item = trk && (trk.midiItems || []).find(m => m.id === contextMenu.itemId); const item = trk && (trk.midiItems || []).find(m => m.id === contextMenu.itemId);
if (item) { if (item) {
const bpmVal = parseInt(bpm) || 120;
clipboardRef.current = { clipboardRef.current = {
type: 'midi', type: 'midi',
notes: (item.notes || []).map(n => ({ ...n })), notes: (item.notes || []).map(n => ({ ...n })),
duration: (item.duration || 4) * (60.0 / bpmVal), duration: item.duration || 4,
name: item.name || 'MIDI Item', name: item.name || 'MIDI Item',
color: item.color || '#a855f7' color: item.color || '#a855f7'
}; };
@@ -17650,15 +17796,27 @@ const App = () => {
const trk = activeTracks.find(t => t.id === contextMenu.trackId); const trk = activeTracks.find(t => t.id === contextMenu.trackId);
const item = trk && (trk.midiItems || []).find(m => m.id === contextMenu.itemId); const item = trk && (trk.midiItems || []).find(m => m.id === contextMenu.itemId);
if (item) { if (item) {
const bpmVal = parseInt(bpm) || 120;
clipboardRef.current = { clipboardRef.current = {
type: 'midi', type: 'midi',
notes: (item.notes || []).map(n => ({ ...n })), notes: (item.notes || []).map(n => ({ ...n })),
duration: (item.duration || 4) * (60.0 / bpmVal), duration: item.duration || 4,
name: item.name || 'MIDI Item', name: item.name || 'MIDI Item',
color: item.color || '#a855f7' color: item.color || '#a855f7'
}; };
window.globalStudioClipboard = clipboardRef.current; window.globalStudioClipboard = clipboardRef.current;
// Close piano roll tab m ca midi item này (user 07:35)
setSubTabs(prev => {
const next = prev.filter(s => s.target_id !== contextMenu.itemId);
// CH v main khi tab active là PIANO ROLL b close gi SECTION-TAB
if (activeTabRef.current && activeTabRef.current.startsWith('midi_') && !next.some(s => s.id === activeTabRef.current)) {
setActiveTab('main');
}
return next;
});
// Sync section tab section item trên MAIN (canvas v li 07:45)
if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
setTimeout(() => { try { syncSectionTabToMain(activeTabRef.current); } catch (e) {} }, 0);
}
updateActiveTracks(prev => prev.map(t => t.id === contextMenu.trackId ? { updateActiveTracks(prev => prev.map(t => t.id === contextMenu.trackId ? {
...t, ...t,
midiItems: (t.midiItems || []).filter(m => m.id !== contextMenu.itemId) midiItems: (t.midiItems || []).filter(m => m.id !== contextMenu.itemId)
@@ -17888,7 +18046,28 @@ const App = () => {
showToast('Đã dán track mới từ clipboard.', 'success'); showToast('Đã dán track mới từ clipboard.', 'success');
return rearrangeNewId; return rearrangeNewId;
}; };
const handlePasteTrack = () => doPaste(selectedTrackId, currentTime); const handlePasteTrack = () => {
// Paste vào TRACK ca ITEM đang chn (nếu có) không phi track 1 mc
// đnh (user 07:55: Ctrl+V dán vào track 1 dù track khác đưc chn item).
let targetId = selectedTrackId;
try {
const selItems = selectedItemIdsRef.current;
if (selItems && selItems.size > 0) {
const firstId = selItems.values().next().value;
const found = findItemContext(firstId);
if (found) targetId = found.trackId;
}
} catch (e) {}
// Paste ti v trí CLICK (bên phi playhead) không phi playhead;
// click bên TRÁI playhead paste ti playhead (user 08:05)
const _clickT = lastClickTimeRef.current;
const pasteTime = (_clickT != null && _clickT > currentTime) ? _clickT : currentTime;
doPaste(targetId, pasteTime);
};
// Ref cho keydown handler (effect deps [] closure STALE: selectedTrackId
// render đu = '1' Ctrl+V luôn paste track 1 user 08:05)
const handlePasteTrackRef = useRef(handlePasteTrack);
handlePasteTrackRef.current = handlePasteTrack;
const contextMenuPaste = () => { const contextMenuPaste = () => {
const result = doPaste(contextMenu.trackId, contextMenu.time || currentTime); const result = doPaste(contextMenu.trackId, contextMenu.time || currentTime);
closeContextMenu(); closeContextMenu();
@@ -20500,7 +20679,12 @@ const App = () => {
}; };
// Playhead set with seek+play // Playhead set with seek+play
// V trí click gn nht trên timeline paste dùng v trí CLICK (bên phi
// playhead) thay vì playhead (user 08:05: paste ti con tr click; click bên
// trái playhead paste ti playhead)
const lastClickTimeRef = useRef(null);
const handlePlayheadSet = (time, shiftKey) => { const handlePlayheadSet = (time, shiftKey) => {
lastClickTimeRef.current = time;
setPlayheadWithUndo(time); setPlayheadWithUndo(time);
}; };
const clearLocalSelection = () => { const clearLocalSelection = () => {
@@ -21381,6 +21565,12 @@ const App = () => {
} }
} }
setDraggedSectionItem(null); setDraggedSectionItem(null);
// Sync section tab section item trên MAIN sau khi MOVE item (user
// 08:15 v trí item đi canvas section item MAIN phi theo)
if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
const _tabId = activeTabRef.current;
setTimeout(() => { try { syncSectionTabToMain(_tabId); } catch (e) {} }, 0);
}
showToast('Đã di chuyển ' + (drag.itemType === 'section' ? 'section' : 'MIDI item') + '.', 'success'); showToast('Đã di chuyển ' + (drag.itemType === 'section' ? 'section' : 'MIDI item') + '.', 'success');
}; };
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
@@ -21472,6 +21662,13 @@ const App = () => {
const resize = resizedSectionItemRef.current; const resize = resizedSectionItemRef.current;
if (!resize) return; if (!resize) return;
setResizedSectionItem(null); setResizedSectionItem(null);
// Sync ni dung section tab section item trên MAIN sau khi RESIZE item
// (user 08:15 kéo ngn midi item trong SECTION-TAB canvas section
// item MAIN phi theo đúng duration mi trưc đây ch sync khi XÓA)
if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
const _tabId = activeTabRef.current;
setTimeout(() => { try { syncSectionTabToMain(_tabId); } catch (e) {} }, 0);
}
}; };
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mouseup', handleMouseUp);
@@ -25368,7 +25565,24 @@ STRICT CONSTRAINTS:
label: 'Save Project', label: 'Save Project',
icon: 'upload-cloud', icon: 'upload-cloud',
shortcut: 'Ctrl+S', shortcut: 'Ctrl+S',
action: () => handleSaveProject() action: () => {
// Save project + save TT C dirty sub-tabs (user 07:35 nhn lưu
// MAIN phi lưu c piano roll/audio tab đã sa)
handleSaveProject();
subTabsRef.current.filter(s => s.isDirty).forEach(st => {
if (st.type === 'PIANO_ROLL') {
handleSaveMidiNotes(st.id, st.trackId, st.target_id, st.notes || []);
} else if (st.type === 'SECTION') {
handleSaveSectionTab(st.id);
} else if (st.buffer) {
const subTrack = activeTracksRef.current.find(t => t.id === st.trackId);
if (subTrack) {
updateActiveTracks(prev => prev.map(t => t.id === st.trackId ? { ...t, buffer: st.buffer } : t));
}
}
});
showToast('Đã lưu dự án + các tab đã sửa', 'success');
}
}, { }, {
label: 'Save As...', label: 'Save As...',
icon: 'download', icon: 'download',
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -24,7 +24,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script> <script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script> <script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script> <script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608070725" defer></script> <script src="/static/js/app.precompiled.js?v=202608070820" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016"> <link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style> <style>
:root { :root {
+89
View File
@@ -2694,3 +2694,92 @@
(3) contextMenuCopy/Cut — xử lý ITEM TRƯỚC track: MIDI → clipboard {type:'midi', notes, duration(sec), name, color} — paste = MIDI item (doPaste nhánh midi đã có); clip → clipboard {buffer,...}; Cut = copy + XÓA item (updateActiveTracks filter). (3) contextMenuCopy/Cut — xử lý ITEM TRƯỚC track: MIDI → clipboard {type:'midi', notes, duration(sec), name, color} — paste = MIDI item (doPaste nhánh midi đã có); clip → clipboard {buffer,...}; Cut = copy + XÓA item (updateActiveTracks filter).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070725), `wiki.md`. Rebuild precompiled (build PASS). - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070725), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → chuột phải MIDI item → Cut → Paste → ra MIDI ITEM (không phải audio); chuột phải clip → Copy → Paste → clip; chuột phải track (không item) → Copy → track buffer như cũ. - **Ghi chú/Test:** hard refresh → chuột phải MIDI item → Cut → Paste → ra MIDI ITEM (không phải audio); chuột phải clip → Copy → Paste → clip; chuột phải track (không item) → Copy → track buffer như cũ.
### [2026-08-07 07:30] Task: (1) Lưu project phải lưu tất cả tab — XÁC NHẬN đã đúng (2) Chuyển PIANO ROLL tab → stop âm tab cũ
- **Yêu cầu (1):** ở MAIN bấm lưu/Ctrl+S phải lưu tất cả các tab.
- **Xác nhận:** serializeProjectToSchema ĐÃ lưu đủ — sessionTabs → section_store (tracks đầy đủ) + sub_tabs (id/trackId/target_id/parent_tab_id/notes/currentTime); deserialize restore đủ (9327-9358). Ctrl+S (16295) + nút Save Project (25368) → handleSaveProject → serialize đủ. KHÔNG cần sửa — nếu user thấy thiếu tab sau load → dán schema đã lưu.
- **Yêu cầu (2):** chuyển PIANO ROLL TAB 1 → TAB 2 phải play đúng nội dung từng tab.
- **Nguyên nhân:** rời piano roll tab đang play (luật 04:15 "không stop") → sang tab 2 → âm tab 1 tiếp tục (nghe nhầm nội dung tab 1 khi ở tab 2).
- **FIX (app.jsx effect 14814):** prevSub PIANO_ROLL đang play + activeTab MỚI là SUB-TAB (midi_*/không main/session) → stopAllPlayback (dừng âm tab cũ — play tab mới đúng nội dung). Về MAIN/SECTION → giữ luật cũ (âm tiếp/resume).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070730), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play piano roll tab 1 → chuyển tab 2 → ÂM TAB 1 DỪNG (im — bấm play tab 2 → nghe tab 2); về MAIN → hành vi cũ.
### [2026-08-07 07:35] Task: (1) Lưu ở MAIN phải lưu dirty tabs (2) Xóa midi item → close piano roll tab tương ứng
- **Báo cáo user:** (1) nhấn lưu ở MAIN SESSION không lưu dirty tab (PIANO ROLL đã sửa notes); (2) xóa midi item phải close PIANO ROLL TAB đã mở của item đó.
- **Nguyên nhân (1):** keydown Ctrl+S handler thứ 2 (16388) — nhánh `curTab === 'main'` RỖNG ("handled by main handler" — không làm gì) → MAIN Ctrl+S không save project + không lưu dirty; nút menu Save Project (25370) chỉ gọi handleSaveProject (không lưu dirty).
- **FIX (app.jsx):**
(1) Ctrl+S (16388): nhánh main/session → `handleSaveProject()` + save TẤT CẢ dirty sub-tabs (PIANO_ROLL → handleSaveMidiNotes; SECTION → saveSectionTab; audio → buffer về track); nút Save Project → cùng logic.
(2) Xóa midi item (contextMenuDelete 17480 + contextMenuCut 17666) → `setSubTabs(filter target_id !== itemId)` — close piano roll tab của item; nếu tab đang active bị close → `setActiveTab('main')`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070735), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → sửa notes piano roll (dirty) → về MAIN → Ctrl+S / Save Project → reload → notes ĐÃ LƯU (tab vẫn mở + nội dung mới); xóa midi item → piano roll tab của item đó tự close (+ về main nếu đang active tab đó).
### [2026-08-07 07:40] Task: SECTION-TAB — xóa midi item (Delete key) → close piano roll tab tương ứng
- **Báo cáo user:** trong SECTION-TAB, xóa midi item phải close PIANO ROLL TAB đã mở của item đó.
- **Nguyên nhân:** đường xóa chính là `handleDeleteSelectedItemsRef` (Delete key — chọn item → Delete) — xóa midiItems nhưng KHÔNG close subTab (target_id = item id) — áp cho cả MAIN lẫn SECTION (updateActiveTracks theo context).
- **FIX (app.jsx handleDeleteSelectedItemsRef):** đầu hàm — `setSubTabs(filter: bỏ tab có target_id ∈ idsToDelete)` — close piano roll tab của midi item bị xóa; tab đang active bị close → `setActiveTab('main')`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070740), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → SECTION-TAB chọn midi item → Delete → piano roll tab của item đó TỰ CLOSE (về main nếu đang ở tab đó); MAIN tương tự.
### [2026-08-07 07:45] Task: (1) FIX Cut midi → paste audio (detect bounds sai) (2) Xóa item SECTION-TAB → sync canvas main + giữ tab
- **Báo cáo (1) [lặp]:** cut midi item (context menu) → paste ra audio item — fix 0725 chưa hiệu lực.
- **Nguyên nhân (1):** onContextMenu detect MIDI bounds dùng `(m.duration || 4)` KHÔNG × secondsPerBar (m.duration tính BEATS — contextMenuDelete 17452 có ×) → detect miss → itemType null → Cut/Copy xử lý TRACK buffer (audio). **FIX:** bounds × `_spb` (60/bpm*4).
- **Báo cáo (2) [mới]:** xóa item trong SECTION-TAB → phải cập nhật canvas section item ở MAIN + giữ vị trí SECTION-TAB (không quay về MAIN khi close tab).
- **FIX (2):** (a) helper `syncSectionTabToMain(tabId)` — sync `tab.tracks``sec.tracks` trên MAIN (canvas section item vẽ lại — KHÔNG đổi duration 0505); gọi sau mọi xóa item (Delete key handleDeleteSelectedItemsRef + contextMenuDelete: section/midi/clip + contextMenuCut). (b) điều kiện "về main" khi close tab — CHỈ khi activeTab là `midi_*` (piano roll) bị close — KHÔNG đá SECTION-TAB về main.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070745), `wiki.md`. Rebuild precompiled (build PASS — syncSectionTabToMain ×6).
- **Ghi chú/Test:** hard refresh → (1) chuột phải MIDI item (track có midi + audio) → Cut → Paste → MIDI item (không audio); (2) SECTION-TAB xóa item → section item trên MAIN vẽ lại (nội dung mới) + VẪN Ở SECTION-TAB (không về main).
### [2026-08-07 07:50] Task: (2) Sync canvas MAIN realtime sau cut/delete SECTION-TAB (3) Ctrl+X/C phải cut/copy ITEM
- **Báo cáo user:** (2) cut/delete item SECTION-TAB 1 → quay MAIN → phải cập nhật realtime canvas section item 1 (không cần save); (3) Ctrl+X không cut được item.
- **Nguyên nhân:** (2) syncSectionTabToMain (0745) chạy NGAY (TRƯỚC updateActiveTracks — state chưa áp) → sync data CŨ → canvas cũ; (3) Ctrl+C/X (16344/16349) LUÔN handleCopyTrack/handleCutTrack (track) — không xử lý item được chọn.
- **FIX (app.jsx):**
(2) syncSectionTabToMain — gọi qua `setTimeout(0)` (SAU updateActiveTracks — data mới) ở mọi điểm xóa (handleDeleteSelectedItemsRef + contextMenuDelete section/midi/clip + contextMenuCut).
(3) Ctrl+C/X — có ITEM được chọn (selectedItemIds) → `findItemContext` + `copyItemToClipboard` (MIDI notes / clip buffer — helpers dùng chung) → copy item; Ctrl+X → copy + `handleDeleteSelectedItemsRef` (xóa + close piano roll tab + sync section); không chọn item → fallback track cũ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070750), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → (2) SECTION-TAB cut/delete item → quay MAIN → section item canvas CẬP NHẬT realtime (nội dung mới — không save); (3) chọn MIDI item → Ctrl+C → Ctrl+V → paste MIDI item; chọn item → Ctrl+X → item bị cắt (clipboard) + tab close + canvas sync.
### [2026-08-07 07:55] Task: (2) Paste midi block ngắn — duration sai (3) Ctrl+V paste vào track 1
- **Báo cáo user:** (1) [lặp] cut midi → paste audio — fix 0745 chờ xác nhận bản mới; (2) context menu cut midi → paste → item block NGẮN (không giữ duration); (3) Ctrl+V paste vào TRACK 1 (không phải track được chọn).
- **Nguyên nhân:** (2) midiItem.duration tính GIÂY (insertMidiItem = 4*spb; scheduling 19331 dùng seconds) — nhưng copy midi (copyItemToClipboard + contextMenuCopy/Cut) nhân thêm `(60.0/bpm)` → duration gấp đôi → block sai. (3) handlePasteTrack = `doPaste(selectedTrackId, ...)` — chọn ITEM không set selectedTrackId (giữ '1') → paste track 1.
- **FIX (app.jsx):**
(2) copy midi duration = `item.duration || 4` (GIÂY — bỏ × 60/bpm) — 3 chỗ (helper + contextMenuCopy + contextMenuCut) + bỏ biến bpmVal thừa.
(3) handlePasteTrack — có ITEM được chọn (selectedItemIds) → target = track chứa item (findItemContext); ngược lại selectedTrackId.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070755), `wiki.md`. Rebuild precompiled (build PASS — type:'midi' ×4, duration ×3).
- **Ghi chú/Test:** hard refresh → (2) context menu cut midi → paste → block GIỮ ĐÚNG duration; (3) chọn item trên track 2 → Ctrl+V → paste vào track 2 (không phải track 1); (1) cut midi → paste → MIDI item.
### [2026-08-07 08:00] Task: (1) Ctrl+X bị browser capture (2) Ctrl+V paste sai track 1
- **Báo cáo user:** (1) Ctrl-X bị browser capture — không cut item được; (2) Ctrl+V vẫn paste track 1 — phải paste vào track đang select.
- **Nguyên nhân:** (1) Ctrl+X handler thiếu stopPropagation (browser cut text chiếm); (2) click ITEM — mousedown nhánh hitItem `return` sớm (2919) → `onTrackLaneMouseDown` (setSelectedTrackId) KHÔNG chạy → selectedTrackId giữ track cũ ('1') → Ctrl+V paste sai.
- **FIX (app.jsx):** (1) Ctrl+X — thêm `e.stopPropagation()` (cùng preventDefault). (2) click item (mousedown hitItem) → `onSelectTrack(track.id)` — chọn track chứa item → Ctrl+V paste đúng track (handlePasteTrack đã ưu tiên item → fallback selectedTrackId).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070800), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → (1) chọn item → Ctrl+X → item bị cắt (không bị browser chiếm); (2) chọn item trên track 2 → Ctrl+V → paste vào track 2 (đúng track đang select).
### [2026-08-07 08:05] Task: (1) Ctrl-X không cắt item (2) paste tại vị trí click (3) paste sai track — closure stale
- **Báo cáo user:** (1) click item → Ctrl-X không thấy cắt; (2) paste nên tại vị trí con trỏ CLICK (bên phải playhead → paste tại đó; bên trái → paste tại playhead); (3) Ctrl+V vẫn paste track đầu tiên.
- **Nguyên nhân:** (3) keydown effect `deps []` — closure giữ handlePasteTrack RENDER ĐẦU → `selectedTrackId` stale ('1') → paste track 1 LUÔN (bất kể track đang chọn). (2) doPaste luôn dùng currentTime (playhead). (1) chưa rõ — thêm log.
- **FIX (app.jsx):**
(1) Ctrl+X — thêm `console.log('[CutItem] Ctrl+X — itemId=... found=...')` — xác định đường chạy (item hay fallback track).
(2) `lastClickTimeRef` — set trong handlePlayheadSet (click timeline/ruler) — handlePasteTrack: pasteTime = click (nếu > playhead) else playhead.
(3) `handlePasteTrackRef` (ref mới mỗi render) — Ctrl+V gọi ref (hết stale selectedTrackId).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070805), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → (1) chọn item → Ctrl+X → nếu không cắt → dán log `[CutItem]`; (2) click timeline bên phải playhead → Ctrl+V → paste tại chỗ click; click bên trái → paste tại playhead; (3) chọn item track 2 → Ctrl+V → paste track 2.
### [2026-08-07 08:10] Task: Ctrl+X không cắt item — copyItemToClipboard closure stale (activeTracks)
- **Log user (0805):** `[CutItem] Ctrl+X — itemId= midi_1786099252336 found= 2/midi selSize= 1` — handler chạy, detect đúng item — NHƯNG timeline không cắt.
- **Nguyên nhân:** Ctrl+C/X chạy trong keydown effect `deps []` → closure giữ `copyItemToClipboard` RENDER ĐẦU — `activeTracks` (state) là ARRAY CŨ — item `midi_1786099252336` TẠO SAU render đầu → `activeTracks.find` KHÔNG có item → copy fail → fallback `handleCutTrack` (cắt track — không cắt item).
- **FIX (app.jsx copyItemToClipboard):** `const trk = (activeTracksRef.current || []).find(...)` — dùng REF (luôn mới) thay closure state.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070810), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → chọn item (mới tạo) → Ctrl+X → item bị CẮT (log [CutItem] found= track/type + toast 'Đã cắt item'); Ctrl+C → copy item → Ctrl+V paste đúng.
### [2026-08-07 08:15] Task: Kéo ngắn midi item SECTION-TAB → MAIN canvas vẫn 8 bars
- **Báo cáo user:** midi item 2 (8 bars content) kéo NGẮN còn 4 bars trong SECTION-TAB — MAIN section item canvas vẫn hiển thị 8 bars.
- **Nguyên nhân:** syncSectionTabToMain (0745) chỉ chạy khi XÓA item — kéo NGẮN (resize) / MOVE item KHÔNG sync → sec.tracks giữ duration/vị trí cũ → canvas MAIN vẽ 8 bars.
- **FIX (app.jsx):** thêm sync sau thao tác (activeTab session → setTimeout(0) → syncSectionTabToMain): (a) handleMouseUp của RESIZE item (21651 — kéo edge — duration mới); (b) handleMouseUp của MOVE item (21563 — vị trí mới).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070815), `wiki.md`. Rebuild precompiled (build PASS — syncSectionTabToMain ×8).
- **Ghi chú/Test:** hard refresh → SECTION-TAB kéo ngắn midi item 2 (4 bars) → quay MAIN → section item canvas hiển thị ĐÚNG 4 bars (không còn 8 bars); kéo di chuyển item → MAIN canvas theo vị trí mới.
### [2026-08-07 08:20] Task: MAIN canvas section item vẫn 8 bars — notes không clip theo duration
- **Test user:** kéo ngắn midi item 2 (4 bars) SECTION-TAB → MAIN canvas KHÔNG hiển thị 4 bars (vẫn 8).
- **Nguyên nhân:** trim (kéo ngắn) KHÔNG cắt notes (notes 8 bars vẫn trong item — duration giảm 4) — canvas MAIN vẽ section content: `notes.forEach` vẽ MỌI note (2579) KHÔNG clip theo `item.duration` → 8 bars vẫn hiển thị (sync 08:15 đã cập nhật sec.tracks — nhưng vẽ không clip).
- **FIX (app.jsx canvas MAIN — drawSection subMidi notes):** skip note `noteStartSec >= (item.duration || 0) - 0.01` — note ngoài duration (phần đã trim) KHÔNG vẽ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070820), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → SECTION-TAB kéo ngắn midi item (4 bars) → quay MAIN → section item canvas hiển thị ĐÚNG 4 bars (phần 5-8 trống); scheduling đã skip note ngoài duration (19331).