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 NGẮN (trim — duration 4 bars
// nhưng notes vẫn còn 8 bars) → note ngoài duration KHÔNG vẽ —
// canvas MAIN phải hiển thị đúng phần đã 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();
// Chọn TRACK chứa item (user 07:55: Ctrl+V phải 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 chuột (user 07:25 — Cut/Copy item phải copy
// ĐÚNG item — trước đây context menu chỉ biết track → copy nhầm buffer)
// ĐÚNG item — bounds × secondsPerBar (m.duration tính BEATS — như
// contextMenuDelete 17452) — thiếu × spb → detect miss → copy nhầm 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 chuột
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 rời tab (SF notes đã schedule —
// tiếp tục kêu tự nhiên — user: chuyển tab qua lại KHÔNG được câm).
// Audio sub-tab (buffer source riêng): vẫn stop như cũ.
if (prevSub.type !== 'PIANO_ROLL') {
// Chuyển sang SUB-TAB KHÁC (piano roll khác/audio tab) → STOP âm tab
// cũ — play ĐÚNG nội dung TỪNG TAB (user 07:30: piano roll tab 1 →
// tab 2 — không được nghe âm tab 1). Về MAIN/SECTION → giữ luật 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 của midi items bị xóa (user 07:40 — áp cả
// SECTION-TAB: xóa midi item → close tab đã mở của 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 nội dung section tab → section item trên MAIN (canvas vẽ lại —
// user 07:45) — setTimeout 0: chạy SAU updateActiveTracks (data mới)
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: phải copy/cut ITEM được
// chọn, không phải 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 phải activeTracks closure cũ): Ctrl+C/X chạy trong keydown
// effect deps [] → closure giữ activeTracks RENDER ĐẦU — item TẠO SAU phiên
// (vd midi_1786099252336) không tồn tại trong closure cũ → copy fail → cắt
// nhầm track (user 08:10 — log [CutItem] found= 2/midi nhưng không cắt).
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 gấp đôi → block
// paste bị dài/ngắn 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 chọn (user 07:50 — Ctrl+C phải 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(); // chặn browser cut (user 07:55 — Ctrl-X bị capture)
const selItems = selectedItemIdsRef.current;
if (selItems && selItems.size > 0) {
// Cut ITEM đầu tiên được chọn: 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 (hoặc SECTION): save project + save TẤT CẢ dirty sub-tabs
// (piano roll/audio/section) — user 07:35: nhấn lưu ở MAIN phải lưu
// cả dirty tab (trước đây nhánh main RỖNG — dirty piano roll bị mất).
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 hiện tại
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 nội dung SECTION-TAB → section item trên MAIN (sec.tracks) — cập nhật
// canvas section item ngay khi thay đổi nội dung tab (user 07:45 — xóa item
// trong SECTION-TAB → canvas section item ở MAIN phải vẽ lại). 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 sớm thất bại/
// race) TRƯỚC khi ghi vào section item → serialized AUDIO_ITEM có file →
// reload không mất 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ẽ lại — 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ở của 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ẽ lại — 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ẽ lại — 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ở của 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ẽ lại — 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 của ITEM đang chọn (nếu có) — không phải track 1 mặc
// định (user 07:55: Ctrl+V dán vào track 1 dù track khác được chọn 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 tại vị trí CLICK (bên phải playhead) — không phải playhead;
// click bên TRÁI playhead → paste tại 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 gần nhất trên timeline — paste dùng vị trí CLICK (bên phải
// playhead) thay vì playhead (user 08:05: paste tại con trỏ click; click bên
// trái playhead → paste tại 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 phải 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 nội dung section tab → section item trên MAIN sau khi RESIZE item
// (user 08:15 — kéo ngắn midi item trong SECTION-TAB → canvas section
// item ở MAIN phải theo đúng duration mới — 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 TẤT CẢ dirty sub-tabs (user 07:35 — nhấn lưu ở
// MAIN phải lưu cả piano roll/audio tab đã sửa)
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',
File diff suppressed because one or more lines are too long