From 9843a33a9bfd4aa126719fd67b6518e50ac7c15b Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 22 Jul 2026 20:50:07 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20b=E1=BB=95=20sung=20ph=E1=BA=A7n=20menu?= =?UTF-8?q?=20insert=20items?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 171 +++++++++++++++++++++++++++-- app/static/js/app.precompiled.js | 179 ++++++++++++++++++++++++++++--- app/storage/sonicforge.db | Bin 45056 -> 45056 bytes 3 files changed, 329 insertions(+), 21 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 7667d55..a6f0937 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -440,6 +440,40 @@ const WaveformLane = ({ ctx.fillText('Kéo thả hoặc tải file âm thanh vào Track này', drawWidth / 2, height / 2); } + // Draw sections + const sections = track.sections || []; + sections.forEach(sec => { + const secStartLocal = sec.start * zoom - scrollLeft; + const secWidth = sec.duration * zoom; + if (secStartLocal + secWidth < 0 || secStartLocal > drawWidth) return; + ctx.fillStyle = sec.color ? sec.color + '44' : 'rgba(6, 182, 212, 0.25)'; + ctx.fillRect(secStartLocal, 2, secWidth, height - 4); + ctx.strokeStyle = sec.color || '#06b6d4'; + ctx.lineWidth = 1; + ctx.setLineDash([4, 4]); + ctx.strokeRect(secStartLocal, 2, secWidth, height - 4); + ctx.setLineDash([]); + ctx.fillStyle = '#e4e4e7'; + ctx.font = 'bold 9px sans-serif'; + ctx.fillText(sec.name || 'Section', Math.max(secStartLocal + 4, 4), 14); + }); + + // Draw MIDI items + const midiItems = track.midiItems || []; + midiItems.forEach(midi => { + const midiStartLocal = midi.startTime * zoom - scrollLeft; + const midiWidth = midi.duration * zoom; + if (midiStartLocal + midiWidth < 0 || midiStartLocal > drawWidth) return; + ctx.fillStyle = '#a78bfa33'; + ctx.fillRect(midiStartLocal, 2, midiWidth, height - 4); + ctx.strokeStyle = '#a78bfa'; + ctx.lineWidth = 1.5; + ctx.strokeRect(midiStartLocal, 2, midiWidth, height - 4); + ctx.fillStyle = '#c4b5fd'; + ctx.font = 'bold 9px sans-serif'; + ctx.fillText(midi.name || 'MIDI', Math.max(midiStartLocal + 4, 4), 14); + }); + // Selection highlight - local selection on this track if (selectionMode === 'local' && localSelectionTrackId === track.id && localSelLeft !== null && localSelRight !== null && localSelRight > localSelLeft) { const hlLeftLocal = localSelLeft * zoom - scrollLeft; @@ -3384,7 +3418,9 @@ const App = () => { color: '#0f766e', markers: [], serverFileId: null, - clips: [] + clips: [], + sections: [], + midiItems: [] }, { id: '2', name: 'Track 02', @@ -3397,7 +3433,10 @@ const App = () => { solo: false, color: '#1d4ed8', markers: [], - serverFileId: null + serverFileId: null, + clips: [], + sections: [], + midiItems: [] }]); const [bpm, setBpm] = useState(localStorage.getItem('studio_bpm') || '120'); const [draggedClip, setDraggedClip] = useState(null); // { trackId, clickOffset, buffer, name, volume, color } @@ -6693,13 +6732,121 @@ const App = () => { solo: false, color: selectColor, markers: [], - serverFileId: null + serverFileId: null, + clips: [], + sections: [], + midiItems: [] }]); showToast(`Đã thêm Track ${newId}.`, 'info'); setTimeout(() => lucide.createIcons(), 200); return newId; }; + // ── Insert Track Below Selected ── + const insertTrackBelow = () => { + const newId = `t${Date.now()}`; + const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309']; + const selectColor = colors[tracks.length % colors.length]; + const newTrack = { + id: newId, + name: `Track ${newId}`, + buffer: null, + startTime: 0, + height: 128, + volumeDb: 0, + pan: 0, + muted: false, + solo: false, + color: selectColor, + markers: [], + serverFileId: null, + clips: [], + sections: [], + midiItems: [] + }; + setTracks(prev => { + const idx = prev.findIndex(t => t.id === selectedTrackId); + if (idx === -1) return [...prev, newTrack]; + const copy = [...prev]; + copy.splice(idx + 1, 0, newTrack); + return copy; + }); + setSelectedTrackId(newId); + showToast(`Đã thêm Track ${newId}.`, 'info'); + setTimeout(() => lucide.createIcons(), 200); + }; + + // ── Insert Section at Playhead ── + const insertSectionAtPlayhead = () => { + const track = tracks.find(t => t.id === selectedTrackId); + if (!track) { showToast('Chọn track trước', 'warning'); return; } + const section = { + id: `sec_${Date.now()}`, + name: 'Section', + start: currentTime, + duration: 4, + color: track.color || '#06b6d4' + }; + setTracks(prev => prev.map(t => t.id === selectedTrackId ? { + ...t, + sections: [...(t.sections || []), section] + } : t)); + showToast(`Đã thêm Section tại ${currentTime.toFixed(2)}s`, 'success'); + }; + + // ── Insert MIDI Item at Playhead ── + const insertMidiItemAtPlayhead = () => { + const track = tracks.find(t => t.id === selectedTrackId); + if (!track) { showToast('Chọn track trước', 'warning'); return; } + const midiItem = { + id: `midi_${Date.now()}`, + name: 'MIDI Item', + startTime: currentTime, + duration: 4, + notes: [], + color: '#a78bfa' + }; + setTracks(prev => prev.map(t => t.id === selectedTrackId ? { + ...t, + midiItems: [...(t.midiItems || []), midiItem] + } : t)); + showToast(`Đã thêm MIDI item tại ${currentTime.toFixed(2)}s`, 'success'); + }; + + // ── Insert Sound Clip at Cursor ── + const insertSoundClipAtCursor = () => { + const track = tracks.find(t => t.id === selectedTrackId); + if (!track) { showToast('Chọn track trước', 'warning'); return; } + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'audio/*'; + input.multiple = true; + input.onchange = async e => { + const files = Array.from(e.target.files || []); + if (!files.length) return; + let offset = currentTime; + let count = 0; + for (const file of files) { + try { + uploadToServer(file, track.id); + 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 ? { + ...t, + clips: [...(t.clips || []), newClip] + } : t)); + offset += decodedBuffer.duration; + count++; + } catch (err) { + showToast(`Lỗi nạp ${file.name}`, 'error'); + } + } + if (count > 0) showToast(`Đã chèn ${count} file âm thanh`, 'success'); + }; + input.click(); + }; + // ── Server-side Export ── const triggerWavExport = async () => { let exportTracks; @@ -8570,7 +8717,10 @@ const App = () => { solo: false, color: '#0f766e', markers: [], - serverFileId: null + serverFileId: null, + clips: [], + sections: [], + midiItems: [] }, { id: '2', name: 'Track 02', @@ -8583,7 +8733,10 @@ const App = () => { solo: false, color: '#1d4ed8', markers: [], - serverFileId: null + serverFileId: null, + clips: [], + sections: [], + midiItems: [] }]); setSelectedTrackId('1'); setProjectName(''); @@ -8733,19 +8886,19 @@ const App = () => { items: [{ label: 'Insert Section', icon: 'folder-plus', - action: () => showToast('Insert section', 'info') + action: insertSectionAtPlayhead }, { label: 'Insert MIDI item', icon: 'music', - action: () => showToast('Insert MIDI item', 'info') + action: insertMidiItemAtPlayhead }, { label: 'Insert sound clip', icon: 'file-input', - action: () => showToast('Insert sound clip', 'info') + action: insertSoundClipAtCursor }, { label: 'Insert track', icon: 'plus', - action: addNewTrack + action: insertTrackBelow }] }, { label: 'View', diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index dccd472..5ce5fdb 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -440,6 +440,40 @@ const WaveformLane = ({ ctx.fillText('Kéo thả hoặc tải file âm thanh vào Track này', drawWidth / 2, height / 2); } + // Draw sections + const sections = track.sections || []; + sections.forEach(sec => { + const secStartLocal = sec.start * zoom - scrollLeft; + const secWidth = sec.duration * zoom; + if (secStartLocal + secWidth < 0 || secStartLocal > drawWidth) return; + ctx.fillStyle = sec.color ? sec.color + '44' : 'rgba(6, 182, 212, 0.25)'; + ctx.fillRect(secStartLocal, 2, secWidth, height - 4); + ctx.strokeStyle = sec.color || '#06b6d4'; + ctx.lineWidth = 1; + ctx.setLineDash([4, 4]); + ctx.strokeRect(secStartLocal, 2, secWidth, height - 4); + ctx.setLineDash([]); + ctx.fillStyle = '#e4e4e7'; + ctx.font = 'bold 9px sans-serif'; + ctx.fillText(sec.name || 'Section', Math.max(secStartLocal + 4, 4), 14); + }); + + // Draw MIDI items + const midiItems = track.midiItems || []; + midiItems.forEach(midi => { + const midiStartLocal = midi.startTime * zoom - scrollLeft; + const midiWidth = midi.duration * zoom; + if (midiStartLocal + midiWidth < 0 || midiStartLocal > drawWidth) return; + ctx.fillStyle = '#a78bfa33'; + ctx.fillRect(midiStartLocal, 2, midiWidth, height - 4); + ctx.strokeStyle = '#a78bfa'; + ctx.lineWidth = 1.5; + ctx.strokeRect(midiStartLocal, 2, midiWidth, height - 4); + ctx.fillStyle = '#c4b5fd'; + ctx.font = 'bold 9px sans-serif'; + ctx.fillText(midi.name || 'MIDI', Math.max(midiStartLocal + 4, 4), 14); + }); + // Selection highlight - local selection on this track if (selectionMode === 'local' && localSelectionTrackId === track.id && localSelLeft !== null && localSelRight !== null && localSelRight > localSelLeft) { const hlLeftLocal = localSelLeft * zoom - scrollLeft; @@ -3444,7 +3478,10 @@ const App = () => { solo: false, color: '#1d4ed8', markers: [], - serverFileId: null + serverFileId: null, + clips: [], + sections: [], + midiItems: [] }]); const [bpm, setBpm] = useState(localStorage.getItem('studio_bpm') || '120'); const [draggedClip, setDraggedClip] = useState(null); // { trackId, clickOffset, buffer, name, volume, color } @@ -4333,7 +4370,10 @@ const App = () => { solo: false, color: '#0f766e', markers: [], - serverFileId: null + serverFileId: null, + clips: [], + sections: [], + midiItems: [] }, { id: '2', name: 'Track 02', @@ -4346,10 +4386,11 @@ const App = () => { solo: false, color: '#1d4ed8', markers: [], - serverFileId: null + serverFileId: null, + clips: [], + sections: [], + midiItems: [] }]); - setSelectedTrackId('1'); - showToast('New project created', 'info'); return; } if (ctrl && !alt && e.key === 's') { @@ -6786,13 +6827,121 @@ const App = () => { solo: false, color: selectColor, markers: [], - serverFileId: null + serverFileId: null, + clips: [], + sections: [], + midiItems: [] }]); showToast(`Đã thêm Track ${newId}.`, 'info'); setTimeout(() => lucide.createIcons(), 200); return newId; }; + // ── Insert Track Below Selected ── + const insertTrackBelow = () => { + const newId = `t${Date.now()}`; + const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309']; + const selectColor = colors[tracks.length % colors.length]; + const newTrack = { + id: newId, + name: `Track ${newId}`, + buffer: null, + startTime: 0, + height: 128, + volumeDb: 0, + pan: 0, + muted: false, + solo: false, + color: selectColor, + markers: [], + serverFileId: null, + clips: [], + sections: [], + midiItems: [] + }; + setTracks(prev => { + const idx = prev.findIndex(t => t.id === selectedTrackId); + if (idx === -1) return [...prev, newTrack]; + const copy = [...prev]; + copy.splice(idx + 1, 0, newTrack); + return copy; + }); + setSelectedTrackId(newId); + showToast(`Đã thêm Track ${newId}.`, 'info'); + setTimeout(() => lucide.createIcons(), 200); + }; + + // ── Insert Section at Playhead ── + const insertSectionAtPlayhead = () => { + const track = tracks.find(t => t.id === selectedTrackId); + if (!track) { showToast('Chọn track trước', 'warning'); return; } + const section = { + id: `sec_${Date.now()}`, + name: 'Section', + start: currentTime, + duration: 4, + color: track.color || '#06b6d4' + }; + setTracks(prev => prev.map(t => t.id === selectedTrackId ? { + ...t, + sections: [...(t.sections || []), section] + } : t)); + showToast(`Đã thêm Section tại ${currentTime.toFixed(2)}s`, 'success'); + }; + + // ── Insert MIDI Item at Playhead ── + const insertMidiItemAtPlayhead = () => { + const track = tracks.find(t => t.id === selectedTrackId); + if (!track) { showToast('Chọn track trước', 'warning'); return; } + const midiItem = { + id: `midi_${Date.now()}`, + name: 'MIDI Item', + startTime: currentTime, + duration: 4, + notes: [], + color: '#a78bfa' + }; + setTracks(prev => prev.map(t => t.id === selectedTrackId ? { + ...t, + midiItems: [...(t.midiItems || []), midiItem] + } : t)); + showToast(`Đã thêm MIDI item tại ${currentTime.toFixed(2)}s`, 'success'); + }; + + // ── Insert Sound Clip at Cursor ── + const insertSoundClipAtCursor = () => { + const track = tracks.find(t => t.id === selectedTrackId); + if (!track) { showToast('Chọn track trước', 'warning'); return; } + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'audio/*'; + input.multiple = true; + input.onchange = async e => { + const files = Array.from(e.target.files || []); + if (!files.length) return; + let offset = currentTime; + let count = 0; + for (const file of files) { + try { + uploadToServer(file, track.id); + 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 ? { + ...t, + clips: [...(t.clips || []), newClip] + } : t)); + offset += decodedBuffer.duration; + count++; + } catch (err) { + showToast(`Lỗi nạp ${file.name}`, 'error'); + } + } + if (count > 0) showToast(`Đã chèn ${count} file âm thanh`, 'success'); + }; + input.click(); + }; + // ── Server-side Export ── const triggerWavExport = async () => { let exportTracks; @@ -9061,7 +9210,10 @@ const App = () => { solo: false, color: '#0f766e', markers: [], - serverFileId: null + serverFileId: null, + clips: [], + sections: [], + midiItems: [] }, { id: '2', name: 'Track 02', @@ -9074,7 +9226,10 @@ const App = () => { solo: false, color: '#1d4ed8', markers: [], - serverFileId: null + serverFileId: null, + clips: [], + sections: [], + midiItems: [] }]); setSelectedTrackId('1'); setProjectName(''); @@ -9224,19 +9379,19 @@ const App = () => { items: [{ label: 'Insert Section', icon: 'folder-plus', - action: () => showToast('Insert section', 'info') + action: insertSectionAtPlayhead }, { label: 'Insert MIDI item', icon: 'music', - action: () => showToast('Insert MIDI item', 'info') + action: insertMidiItemAtPlayhead }, { label: 'Insert sound clip', icon: 'file-input', - action: () => showToast('Insert sound clip', 'info') + action: insertSoundClipAtCursor }, { label: 'Insert track', icon: 'plus', - action: addNewTrack + action: insertTrackBelow }] }, { label: 'View', diff --git a/app/storage/sonicforge.db b/app/storage/sonicforge.db index 1c628e3ba17ca75c156a21d55a6f688fb48cf1c5..f987ea57c3468f88136895dcd7f7b366074806f7 100644 GIT binary patch delta 38 ucmZp8z|`=7X@WH4g^4oGj2AX0IG1x8wI$RPFfgzQZEWOY+q|pR%?JP)O%0j= delta 38 ucmZp8z|`=7X@WH4>4`GVjHfpyIG1x8{(GVShk=1jXk#NE+vZ)hZbkqr&koK2