feat: bổ sung phần menu insert items
This commit is contained in:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user