feat: bổ sung phần menu insert items

This commit is contained in:
2026-07-22 20:50:07 +07:00
parent 78b9358539
commit 9843a33a9b
3 changed files with 329 additions and 21 deletions
+162 -9
View File
@@ -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',
+167 -12
View File
@@ -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',
Binary file not shown.