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
+243 -29
View File
@@ -2579,6 +2579,10 @@ const WaveformLane = ({
notes.forEach(note => {
const beatSec = 60.0 / (parseInt(bpm) || 120);
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 noteStartLocal = itemStartLocal + noteStartSec * zoom;
const nw = noteDurSec * zoom;
@@ -2892,6 +2896,10 @@ const WaveformLane = ({
if (hitItem && !e.altKey && !e.shiftKey) {
e.preventDefault();
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) {
// Ctrl+Click: toggle selection (add/remove from group) + set pending copy-drag
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; }
}
// 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;
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
let hitClipId = null;
@@ -14949,10 +14959,12 @@ const App = () => {
}
const prevSub = subTabsRef.current.find(s => s.id === prevTab);
if (prevSub && prevSub.isPlaying) {
// PIANO ROLL tab: KHÔNG stop âm khi ri tab (SF notes đã schedule
// tiếp tc kêu t nhiên user: chuyn tab qua li KHÔNG đưc câm).
// Audio sub-tab (buffer source riêng): vn stop như cũ.
if (prevSub.type !== 'PIANO_ROLL') {
// Chuyn sang SUB-TAB KHÁC (piano roll khác/audio tab) STOP âm tab
// cũ play ĐÚNG ni dung TNG TAB (user 07:30: piano roll tab 1
// tab 2 không đưc nghe âm tab 1). V MAIN/SECTION gi lut cũ
// (04:15+ âm tiếp/resume).
const _newIsSub = activeTab && activeTab !== 'main' && !activeTab.startsWith('session_');
if (prevSub.type !== 'PIANO_ROLL' || _newIsSub) {
try { stopAllPlayback(); } catch (e) {}
}
setSubTabs(prev => prev.map(s => s.id === prevTab ? { ...s, isPlaying: false } : s));
@@ -15770,6 +15782,23 @@ const App = () => {
const handleDeleteSelectedItemsRef = useRef(() => {});
handleDeleteSelectedItemsRef.current = (idsToDelete) => {
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());
updateActiveTracks(prev => prev.map(t => {
let changed = false;
@@ -16100,6 +16129,58 @@ const App = () => {
} : s));
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(() => {
const handler = e => {
// Bypass global hotkeys when typing inside input/textarea/contentEditable elements
@@ -16323,17 +16404,47 @@ const App = () => {
}
if (ctrl && !alt && e.key === 'c') {
e.preventDefault();
handleCopyTrack();
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();
}
} else {
handleCopyTrack();
}
return;
}
if (ctrl && !alt && e.key === 'x') {
e.preventDefault();
handleCutTrack();
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();
}
} else {
handleCutTrack();
}
return;
}
if (ctrl && !alt && e.key === 'v') {
e.preventDefault();
handlePasteTrack();
// Dùng REF effect deps [] handlePasteTrack closure CŨ (selectedTrackId
// stale = '1' paste sai track user 08:05)
handlePasteTrackRef.current();
return;
}
if (e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Del') {
@@ -16386,10 +16497,10 @@ const App = () => {
if (ctrl && !alt && e.key === 's') {
e.preventDefault();
const curTab = activeTabRef.current;
if (curTab === 'main') {
// handled by main handler
} else if (curTab.startsWith('session_')) {
// Main session: save project + save all dirty sub-tabs
if (curTab === 'main' || (curTab && curTab.startsWith('session_'))) {
// MAIN (hoc SECTION): save project + save TT C dirty sub-tabs
// (piano roll/audio/section) user 07:35: nhn lưu MAIN phi lưu
// c dirty tab (trưc đây nhánh main RNG dirty piano roll b mt).
handleSaveProject();
subTabsRef.current.filter(s => s.isDirty).forEach(st => {
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));
}
}
showToast('Đã lưu', 'success');
});
} else if (curTab.startsWith('session_')) {
// Section tab: save section
handleSaveSectionTabRef.current(curTab);
showToast('Đã lưu Section', 'success');
if (curTab.startsWith('session_')) {
// SECTION-TAB: cũng lưu section hin ti
handleSaveSectionTabRef.current(curTab);
}
showToast('Đã lưu dự án + các tab đã sửa', 'success');
} else {
// Sub-tab: save current tab
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');
};
// 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 tab = sessionTabs.find(s => s.id === tabId);
if (!tab) return;
// 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
// reload không mt audioclip.
@@ -17471,12 +17597,29 @@ const App = () => {
const afterSnap = captureTrackSnapshot(tid);
pushAction('DELETE_SECTION', tid, beforeSnap, afterSnap);
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');
return;
}
if (clickedMidi) {
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 => {
if (t.id !== tid) return t;
return {
@@ -17507,6 +17650,10 @@ const App = () => {
const afterSnap = captureTrackSnapshot(tid);
pushAction('DELETE_CLIP', tid, beforeSnap, afterSnap);
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');
return;
}
@@ -17559,11 +17706,10 @@ const App = () => {
const trk = activeTracks.find(t => t.id === contextMenu.trackId);
const item = trk && (trk.midiItems || []).find(m => m.id === contextMenu.itemId);
if (item) {
const bpmVal = parseInt(bpm) || 120;
clipboardRef.current = {
type: 'midi',
notes: (item.notes || []).map(n => ({ ...n })),
duration: (item.duration || 4) * (60.0 / bpmVal),
duration: item.duration || 4,
name: item.name || 'MIDI Item',
color: item.color || '#a855f7'
};
@@ -17650,15 +17796,27 @@ const App = () => {
const trk = activeTracks.find(t => t.id === contextMenu.trackId);
const item = trk && (trk.midiItems || []).find(m => m.id === contextMenu.itemId);
if (item) {
const bpmVal = parseInt(bpm) || 120;
clipboardRef.current = {
type: 'midi',
notes: (item.notes || []).map(n => ({ ...n })),
duration: (item.duration || 4) * (60.0 / bpmVal),
duration: item.duration || 4,
name: item.name || 'MIDI Item',
color: item.color || '#a855f7'
};
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 ? {
...t,
midiItems: (t.midiItems || []).filter(m => m.id !== contextMenu.itemId)
@@ -17888,7 +18046,28 @@ const App = () => {
showToast('Đã dán track mới từ clipboard.', 'success');
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 result = doPaste(contextMenu.trackId, contextMenu.time || currentTime);
closeContextMenu();
@@ -20500,7 +20679,12 @@ const App = () => {
};
// 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) => {
lastClickTimeRef.current = time;
setPlayheadWithUndo(time);
};
const clearLocalSelection = () => {
@@ -21381,6 +21565,12 @@ const App = () => {
}
}
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');
};
document.addEventListener('mousemove', handleMouseMove);
@@ -21472,6 +21662,13 @@ const App = () => {
const resize = resizedSectionItemRef.current;
if (!resize) return;
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('mouseup', handleMouseUp);
@@ -25365,11 +25562,28 @@ STRICT CONSTRAINTS:
shortcut: 'Ctrl+O',
action: () => setOpenProjectModalOpen(true)
}, {
label: 'Save Project',
icon: 'upload-cloud',
shortcut: 'Ctrl+S',
action: () => handleSaveProject()
}, {
label: 'Save Project',
icon: 'upload-cloud',
shortcut: 'Ctrl+S',
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...',
icon: 'download',
shortcut: 'Ctrl+Alt+S',