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:
+243
-29
@@ -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
Reference in New Issue
Block a user