From 6545e1746e5b37c75476622ee107614451209806 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 22 Jul 2026 22:03:17 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20b=E1=BB=95=20sung=20logic=20c=E1=BB=A7a?= =?UTF-8?q?=20ph=E1=BA=A7n=20section:=20th=C3=AAm=20c=C3=A1c=20items,=20x?= =?UTF-8?q?=C3=B3a=20track?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 117 ++++++++++++++++++++++--------- app/static/js/app.precompiled.js | 117 ++++++++++++++++++++++--------- app/storage/sonicforge.db | Bin 45056 -> 45056 bytes 3 files changed, 168 insertions(+), 66 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index acd9d32..5643916 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -5176,15 +5176,27 @@ const App = () => { }; const contextMenuDelete = () => { const tid = contextMenu.trackId; - const beforeSnap = captureTrackSnapshot(tid); - setTracks(prev => prev.filter(t => t.id !== tid)); - const afterSnap = captureTrackSnapshot(tid); - pushAction('DELETE', tid, beforeSnap, afterSnap); - if (selectedTrackId === tid) { - setSelectedTrackId(tracks.filter(t => t.id !== tid)[0]?.id || '1'); + const sessionTab = sessionTabs.find(s => s.id === activeTab); + if (sessionTab) { + updateActiveTracks(prev => prev.filter(t => t.id !== tid)); + if (selectedTrackId === tid) setSelectedTrackId('1'); + closeContextMenu(); + showToast('Đã xoá track.', 'info'); + } else { + const track = tracks.find(t => t.id === tid); + if (track && track.sections && track.sections.length > 0) { + closeContextMenu(); + showToast('Không thể xoá track chứa Section item.', 'warning'); + return; + } + const beforeSnap = captureTrackSnapshot(tid); + setTracks(prev => prev.filter(t => t.id !== tid)); + const afterSnap = captureTrackSnapshot(tid); + pushAction('DELETE', tid, beforeSnap, afterSnap); + if (selectedTrackId === tid) setSelectedTrackId(tracks.filter(t => t.id !== tid)[0]?.id || '1'); + closeContextMenu(); + showToast('Đã xoá track.', 'info'); } - closeContextMenu(); - showToast('Đã xoá track.', 'info'); }; const contextMenuCopy = () => { const track = tracks.find(t => t.id === contextMenu.trackId); @@ -5558,8 +5570,20 @@ const App = () => { }; const handleDeleteTrack = () => { const tid = selectedTrackId; - setTracks(p => p.filter(t => t.id !== tid)); - setSelectedTrackId(tracks.filter(t => t.id !== tid)[0]?.id || '1'); + const sessionTab = sessionTabs.find(s => s.id === activeTab); + if (sessionTab) { + updateActiveTracks(prev => prev.filter(t => t.id !== tid)); + setSelectedTrackId(activeTracks.filter(t => t.id !== tid)[0]?.id || '1'); + } else { + const curTracks = tracks; + const track = curTracks.find(t => t.id === tid); + if (track && track.sections && track.sections.length > 0) { + showToast('Không thể xoá track chứa Section item.', 'warning'); + return; + } + setTracks(p => p.filter(t => t.id !== tid)); + setSelectedTrackId(curTracks.filter(t => t.id !== tid)[0]?.id || '1'); + } showToast('Deleted track.', 'info'); }; @@ -6415,14 +6439,26 @@ const App = () => { document.addEventListener('mouseup', handleMouseUp); }; const deleteTrack = trackId => { - const beforeSnap = captureTrackSnapshot(trackId); - setTracks(prev => { - const filtered = prev.filter(t => t.id !== trackId); - if (filtered.length > 0) { - setSelectedTrackId(filtered[0].id); + const sessionTab = sessionTabs.find(s => s.id === activeTab); + if (sessionTab) { + updateActiveTracks(prev => { + const filtered = prev.filter(t => t.id !== trackId); + if (filtered.length > 0) setSelectedTrackId(filtered[0].id); + return filtered; + }); + } else { + const track = tracks.find(t => t.id === trackId); + if (track && track.sections && track.sections.length > 0) { + showToast('Không thể xoá track chứa Section item.', 'warning'); + return; } - return filtered; - }); + const beforeSnap = captureTrackSnapshot(trackId); + setTracks(prev => { + const filtered = prev.filter(t => t.id !== trackId); + if (filtered.length > 0) setSelectedTrackId(filtered[0].id); + return filtered; + }); + } showToast('Đã xóa track.', 'info'); }; useEffect(() => { @@ -6564,7 +6600,8 @@ const App = () => { // ── Section / MIDI Item Resize Start ── const handleSectionItemResizeStart = (trackId, itemType, itemId, side, clickTime) => { - const track = tracks.find(t => t.id === trackId); + const curTracks = activeTracks; + const track = curTracks.find(t => t.id === trackId); if (!track) return; const items = itemType === 'section' ? track.sections : track.midiItems; const item = (items || []).find(it => it.id === itemId); @@ -6586,7 +6623,7 @@ const App = () => { const time = mouseX / zoom; const newStart = Math.max(0, time - drag.clickOffset); const targetTrackId = hoveredTrackIdRef.current || drag.trackId; - setTracks(prev => prev.map(t => { + updateActiveTracks(prev => prev.map(t => { const items = drag.itemType === 'section' ? (t.sections || []) : (t.midiItems || []); const updatedItems = items.filter(it => it.id !== drag.itemId); if (t.id === targetTrackId) { @@ -6623,7 +6660,7 @@ const App = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; - }, [zoom]); + }, [zoom, activeTab, sessionTabs]); // ── Document-level mousemove/mouseup for Section/MIDI item resize ── useEffect(() => { @@ -6636,7 +6673,7 @@ const App = () => { const scrollLeft = wrapper.scrollLeft; const mouseX = e.clientX - rect.left + scrollLeft; const time = mouseX / zoom; - setTracks(prev => prev.map(t => { + updateActiveTracks(prev => prev.map(t => { if (t.id !== resize.trackId) return t; const items = resize.itemType === 'section' ? [...(t.sections || [])] : [...(t.midiItems || [])]; const idx = items.findIndex(it => it.id === resize.itemId); @@ -6670,7 +6707,7 @@ const App = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; - }, [zoom]); + }, [zoom, activeTab, sessionTabs]); const handleSelectRange = (start, end, reset) => { const maxLen = maxDuration; @@ -6981,10 +7018,11 @@ const App = () => { // ── Add Track ── const addNewTrack = () => { - const newId = (tracks.length + 1).toString(); + const curTracks = activeTracks; + const newId = (curTracks.length + 1).toString(); const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309']; - const selectColor = colors[tracks.length % colors.length]; - setTracks(prev => [...prev, { + const selectColor = colors[curTracks.length % colors.length]; + updateActiveTracks(prev => [...prev, { id: newId, name: `Track ${newId}`, buffer: null, @@ -7006,11 +7044,22 @@ const App = () => { return newId; }; + // ── Update tracks in active context (main session or section tab) ── + const updateActiveTracks = updater => { + const sessionTab = sessionTabs.find(s => s.id === activeTab); + if (sessionTab) { + setSessionTabs(prev => prev.map(st => st.id === activeTab ? { ...st, tracks: updater(st.tracks) } : st)); + } else { + setTracks(updater); + } + }; + // ── Insert Track Below Selected ── const insertTrackBelow = () => { + const curTracks = activeTracks; const newId = `t${Date.now()}`; const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309']; - const selectColor = colors[tracks.length % colors.length]; + const selectColor = colors[(curTracks.length) % colors.length]; const newTrack = { id: newId, name: `Track ${newId}`, @@ -7028,7 +7077,7 @@ const App = () => { sections: [], midiItems: [] }; - setTracks(prev => { + updateActiveTracks(prev => { const idx = prev.findIndex(t => t.id === selectedTrackId); if (idx === -1) return [...prev, newTrack]; const copy = [...prev]; @@ -7060,7 +7109,8 @@ const App = () => { // ── Insert MIDI Item at Playhead ── const insertMidiItemAtPlayhead = () => { - const track = tracks.find(t => t.id === selectedTrackId); + const curTracks = activeTracks; + const track = curTracks.find(t => t.id === selectedTrackId); if (!track) { showToast('Chọn track trước', 'warning'); return; } const midiItem = { id: `midi_${Date.now()}`, @@ -7070,7 +7120,7 @@ const App = () => { notes: [], color: '#a78bfa' }; - setTracks(prev => prev.map(t => t.id === selectedTrackId ? { + updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? { ...t, midiItems: [...(t.midiItems || []), midiItem] } : t)); @@ -7079,7 +7129,8 @@ const App = () => { // ── Insert Sound Clip at Cursor ── const insertSoundClipAtCursor = () => { - const track = tracks.find(t => t.id === selectedTrackId); + const curTracks = activeTracks; + const track = curTracks.find(t => t.id === selectedTrackId); if (!track) { showToast('Chọn track trước', 'warning'); return; } const input = document.createElement('input'); input.type = 'file'; @@ -7096,7 +7147,7 @@ const App = () => { const { audioBuffer: decodedBuffer, channelInfo } = await window.SonicAudio.decodeAudioFile(file); const clipId = `clip_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`; const newClip = { id: clipId, buffer: decodedBuffer, startTime: offset, name: file.name, speed: 1.0 }; - setTracks(prev => prev.map(t => t.id === selectedTrackId ? { + updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? { ...t, clips: [...(t.clips || []), newClip] } : t)); @@ -9147,11 +9198,11 @@ const App = () => { }] }, { label: 'Insert', - items: [{ + items: [...(!sessionTabs.some(s => s.id === activeTab) ? [{ label: 'Insert Section', icon: 'folder-plus', action: insertSectionAtPlayhead - }, { + }] : []), { label: 'Insert MIDI item', icon: 'music', action: insertMidiItemAtPlayhead diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 66317ad..6742eda 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -5235,15 +5235,27 @@ const App = () => { }; const contextMenuDelete = () => { const tid = contextMenu.trackId; - const beforeSnap = captureTrackSnapshot(tid); - setTracks(prev => prev.filter(t => t.id !== tid)); - const afterSnap = captureTrackSnapshot(tid); - pushAction('DELETE', tid, beforeSnap, afterSnap); - if (selectedTrackId === tid) { - setSelectedTrackId(tracks.filter(t => t.id !== tid)[0]?.id || '1'); + const sessionTab = sessionTabs.find(s => s.id === activeTab); + if (sessionTab) { + updateActiveTracks(prev => prev.filter(t => t.id !== tid)); + if (selectedTrackId === tid) setSelectedTrackId('1'); + closeContextMenu(); + showToast('Đã xoá track.', 'info'); + } else { + const track = tracks.find(t => t.id === tid); + if (track && track.sections && track.sections.length > 0) { + closeContextMenu(); + showToast('Không thể xoá track chứa Section item.', 'warning'); + return; + } + const beforeSnap = captureTrackSnapshot(tid); + setTracks(prev => prev.filter(t => t.id !== tid)); + const afterSnap = captureTrackSnapshot(tid); + pushAction('DELETE', tid, beforeSnap, afterSnap); + if (selectedTrackId === tid) setSelectedTrackId(tracks.filter(t => t.id !== tid)[0]?.id || '1'); + closeContextMenu(); + showToast('Đã xoá track.', 'info'); } - closeContextMenu(); - showToast('Đã xoá track.', 'info'); }; const contextMenuCopy = () => { const track = tracks.find(t => t.id === contextMenu.trackId); @@ -5625,8 +5637,20 @@ const App = () => { }; const handleDeleteTrack = () => { const tid = selectedTrackId; - setTracks(p => p.filter(t => t.id !== tid)); - setSelectedTrackId(tracks.filter(t => t.id !== tid)[0]?.id || '1'); + const sessionTab = sessionTabs.find(s => s.id === activeTab); + if (sessionTab) { + updateActiveTracks(prev => prev.filter(t => t.id !== tid)); + setSelectedTrackId(activeTracks.filter(t => t.id !== tid)[0]?.id || '1'); + } else { + const curTracks = tracks; + const track = curTracks.find(t => t.id === tid); + if (track && track.sections && track.sections.length > 0) { + showToast('Không thể xoá track chứa Section item.', 'warning'); + return; + } + setTracks(p => p.filter(t => t.id !== tid)); + setSelectedTrackId(curTracks.filter(t => t.id !== tid)[0]?.id || '1'); + } showToast('Deleted track.', 'info'); }; @@ -6503,14 +6527,26 @@ const App = () => { document.addEventListener('mouseup', handleMouseUp); }; const deleteTrack = trackId => { - const beforeSnap = captureTrackSnapshot(trackId); - setTracks(prev => { - const filtered = prev.filter(t => t.id !== trackId); - if (filtered.length > 0) { - setSelectedTrackId(filtered[0].id); + const sessionTab = sessionTabs.find(s => s.id === activeTab); + if (sessionTab) { + updateActiveTracks(prev => { + const filtered = prev.filter(t => t.id !== trackId); + if (filtered.length > 0) setSelectedTrackId(filtered[0].id); + return filtered; + }); + } else { + const track = tracks.find(t => t.id === trackId); + if (track && track.sections && track.sections.length > 0) { + showToast('Không thể xoá track chứa Section item.', 'warning'); + return; } - return filtered; - }); + const beforeSnap = captureTrackSnapshot(trackId); + setTracks(prev => { + const filtered = prev.filter(t => t.id !== trackId); + if (filtered.length > 0) setSelectedTrackId(filtered[0].id); + return filtered; + }); + } showToast('Đã xóa track.', 'info'); }; useEffect(() => { @@ -6652,7 +6688,8 @@ const App = () => { // ── Section / MIDI Item Resize Start ── const handleSectionItemResizeStart = (trackId, itemType, itemId, side, clickTime) => { - const track = tracks.find(t => t.id === trackId); + const curTracks = activeTracks; + const track = curTracks.find(t => t.id === trackId); if (!track) return; const items = itemType === 'section' ? track.sections : track.midiItems; const item = (items || []).find(it => it.id === itemId); @@ -6674,7 +6711,7 @@ const App = () => { const time = mouseX / zoom; const newStart = Math.max(0, time - drag.clickOffset); const targetTrackId = hoveredTrackIdRef.current || drag.trackId; - setTracks(prev => prev.map(t => { + updateActiveTracks(prev => prev.map(t => { const items = drag.itemType === 'section' ? (t.sections || []) : (t.midiItems || []); const updatedItems = items.filter(it => it.id !== drag.itemId); if (t.id === targetTrackId) { @@ -6711,7 +6748,7 @@ const App = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; - }, [zoom]); + }, [zoom, activeTab, sessionTabs]); // ── Document-level mousemove/mouseup for Section/MIDI item resize ── useEffect(() => { @@ -6724,7 +6761,7 @@ const App = () => { const scrollLeft = wrapper.scrollLeft; const mouseX = e.clientX - rect.left + scrollLeft; const time = mouseX / zoom; - setTracks(prev => prev.map(t => { + updateActiveTracks(prev => prev.map(t => { if (t.id !== resize.trackId) return t; const items = resize.itemType === 'section' ? [...(t.sections || [])] : [...(t.midiItems || [])]; const idx = items.findIndex(it => it.id === resize.itemId); @@ -6758,7 +6795,7 @@ const App = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; - }, [zoom]); + }, [zoom, activeTab, sessionTabs]); const handleSelectRange = (start, end, reset) => { const maxLen = maxDuration; @@ -7073,12 +7110,23 @@ const App = () => { showToast(`Đã nạp sóng âm tổng hợp: ${type.toUpperCase()}`, 'success'); }; + // ── Update tracks in active context (main session or section tab) ── + const updateActiveTracks = updater => { + const sessionTab = sessionTabs.find(s => s.id === activeTab); + if (sessionTab) { + setSessionTabs(prev => prev.map(st => st.id === activeTab ? { ...st, tracks: updater(st.tracks) } : st)); + } else { + setTracks(updater); + } + }; + // ── Add Track ── const addNewTrack = () => { - const newId = (tracks.length + 1).toString(); + const curTracks = activeTracks; + const newId = (curTracks.length + 1).toString(); const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309']; - const selectColor = colors[tracks.length % colors.length]; - setTracks(prev => [...prev, { + const selectColor = colors[curTracks.length % colors.length]; + updateActiveTracks(prev => [...prev, { id: newId, name: `Track ${newId}`, buffer: null, @@ -7102,9 +7150,10 @@ const App = () => { // ── Insert Track Below Selected ── const insertTrackBelow = () => { + const curTracks = activeTracks; const newId = `t${Date.now()}`; const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309']; - const selectColor = colors[tracks.length % colors.length]; + const selectColor = colors[curTracks.length % colors.length]; const newTrack = { id: newId, name: `Track ${newId}`, @@ -7122,7 +7171,7 @@ const App = () => { sections: [], midiItems: [] }; - setTracks(prev => { + updateActiveTracks(prev => { const idx = prev.findIndex(t => t.id === selectedTrackId); if (idx === -1) return [...prev, newTrack]; const copy = [...prev]; @@ -7154,7 +7203,8 @@ const App = () => { // ── Insert MIDI Item at Playhead ── const insertMidiItemAtPlayhead = () => { - const track = tracks.find(t => t.id === selectedTrackId); + const curTracks = activeTracks; + const track = curTracks.find(t => t.id === selectedTrackId); if (!track) { showToast('Chọn track trước', 'warning'); return; } const midiItem = { id: `midi_${Date.now()}`, @@ -7164,7 +7214,7 @@ const App = () => { notes: [], color: '#a78bfa' }; - setTracks(prev => prev.map(t => t.id === selectedTrackId ? { + updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? { ...t, midiItems: [...(t.midiItems || []), midiItem] } : t)); @@ -7173,7 +7223,8 @@ const App = () => { // ── Insert Sound Clip at Cursor ── const insertSoundClipAtCursor = () => { - const track = tracks.find(t => t.id === selectedTrackId); + const curTracks = activeTracks; + const track = curTracks.find(t => t.id === selectedTrackId); if (!track) { showToast('Chọn track trước', 'warning'); return; } const input = document.createElement('input'); input.type = 'file'; @@ -7190,7 +7241,7 @@ const App = () => { const { audioBuffer: decodedBuffer, channelInfo } = await window.SonicAudio.decodeAudioFile(file); const clipId = `clip_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`; const newClip = { id: clipId, buffer: decodedBuffer, startTime: offset, name: file.name, speed: 1.0 }; - setTracks(prev => prev.map(t => t.id === selectedTrackId ? { + updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? { ...t, clips: [...(t.clips || []), newClip] } : t)); @@ -9639,11 +9690,11 @@ const App = () => { }] }, { label: 'Insert', - items: [{ + items: [...(!sessionTabs.some(s => s.id === activeTab) ? [{ label: 'Insert Section', icon: 'folder-plus', action: insertSectionAtPlayhead - }, { + }] : []), { label: 'Insert MIDI item', icon: 'music', action: insertMidiItemAtPlayhead diff --git a/app/storage/sonicforge.db b/app/storage/sonicforge.db index 83ef1078e1adbf63147754d1923f725f832c122b..2ff350819995fb75a293acc2693204c156870512 100644 GIT binary patch delta 136 zcmZp8z|`=7X@V3Jlhi~RCm^{oVPQTm?=A*r&OQeI^_+d&lQ#1eG;s2?FmiK-hKjZY zaxk$^{@$lD`Bkkxh~BJMSHUQ>$nn+;vu9;N;Vr5>objRdZK;#zRBzhsUl+yuLor001hMF>(L^ delta 163 zcmV;U09^lo-~xc)0+1R40r8O>LIMHtv0$NZ4FCWD0}GP?53dcn3zM@BaDWN1kOcw> zgd_|JUsXSbW0RYEvXkI?rjyuu838hrV0>4TN_#ydEFyDtVpL&bb0RuhU3~$oLE4x# zH5$W2gK`TARaF&+GYA3)lkIsVlfHQ`6>xHIYGq?|Uokg0G&eFZH8nLiGc~gjdUyc| RlR?^;D>ig8akH;~Q7|eBIJE!(