feat: bổ sung phần menu insert items
This commit is contained in:
+162
-9
@@ -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);
|
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
|
// Selection highlight - local selection on this track
|
||||||
if (selectionMode === 'local' && localSelectionTrackId === track.id && localSelLeft !== null && localSelRight !== null && localSelRight > localSelLeft) {
|
if (selectionMode === 'local' && localSelectionTrackId === track.id && localSelLeft !== null && localSelRight !== null && localSelRight > localSelLeft) {
|
||||||
const hlLeftLocal = localSelLeft * zoom - scrollLeft;
|
const hlLeftLocal = localSelLeft * zoom - scrollLeft;
|
||||||
@@ -3384,7 +3418,9 @@ const App = () => {
|
|||||||
color: '#0f766e',
|
color: '#0f766e',
|
||||||
markers: [],
|
markers: [],
|
||||||
serverFileId: null,
|
serverFileId: null,
|
||||||
clips: []
|
clips: [],
|
||||||
|
sections: [],
|
||||||
|
midiItems: []
|
||||||
}, {
|
}, {
|
||||||
id: '2',
|
id: '2',
|
||||||
name: 'Track 02',
|
name: 'Track 02',
|
||||||
@@ -3397,7 +3433,10 @@ const App = () => {
|
|||||||
solo: false,
|
solo: false,
|
||||||
color: '#1d4ed8',
|
color: '#1d4ed8',
|
||||||
markers: [],
|
markers: [],
|
||||||
serverFileId: null
|
serverFileId: null,
|
||||||
|
clips: [],
|
||||||
|
sections: [],
|
||||||
|
midiItems: []
|
||||||
}]);
|
}]);
|
||||||
const [bpm, setBpm] = useState(localStorage.getItem('studio_bpm') || '120');
|
const [bpm, setBpm] = useState(localStorage.getItem('studio_bpm') || '120');
|
||||||
const [draggedClip, setDraggedClip] = useState(null); // { trackId, clickOffset, buffer, name, volume, color }
|
const [draggedClip, setDraggedClip] = useState(null); // { trackId, clickOffset, buffer, name, volume, color }
|
||||||
@@ -6693,13 +6732,121 @@ const App = () => {
|
|||||||
solo: false,
|
solo: false,
|
||||||
color: selectColor,
|
color: selectColor,
|
||||||
markers: [],
|
markers: [],
|
||||||
serverFileId: null
|
serverFileId: null,
|
||||||
|
clips: [],
|
||||||
|
sections: [],
|
||||||
|
midiItems: []
|
||||||
}]);
|
}]);
|
||||||
showToast(`Đã thêm Track ${newId}.`, 'info');
|
showToast(`Đã thêm Track ${newId}.`, 'info');
|
||||||
setTimeout(() => lucide.createIcons(), 200);
|
setTimeout(() => lucide.createIcons(), 200);
|
||||||
return newId;
|
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 ──
|
// ── Server-side Export ──
|
||||||
const triggerWavExport = async () => {
|
const triggerWavExport = async () => {
|
||||||
let exportTracks;
|
let exportTracks;
|
||||||
@@ -8570,7 +8717,10 @@ const App = () => {
|
|||||||
solo: false,
|
solo: false,
|
||||||
color: '#0f766e',
|
color: '#0f766e',
|
||||||
markers: [],
|
markers: [],
|
||||||
serverFileId: null
|
serverFileId: null,
|
||||||
|
clips: [],
|
||||||
|
sections: [],
|
||||||
|
midiItems: []
|
||||||
}, {
|
}, {
|
||||||
id: '2',
|
id: '2',
|
||||||
name: 'Track 02',
|
name: 'Track 02',
|
||||||
@@ -8583,7 +8733,10 @@ const App = () => {
|
|||||||
solo: false,
|
solo: false,
|
||||||
color: '#1d4ed8',
|
color: '#1d4ed8',
|
||||||
markers: [],
|
markers: [],
|
||||||
serverFileId: null
|
serverFileId: null,
|
||||||
|
clips: [],
|
||||||
|
sections: [],
|
||||||
|
midiItems: []
|
||||||
}]);
|
}]);
|
||||||
setSelectedTrackId('1');
|
setSelectedTrackId('1');
|
||||||
setProjectName('');
|
setProjectName('');
|
||||||
@@ -8733,19 +8886,19 @@ const App = () => {
|
|||||||
items: [{
|
items: [{
|
||||||
label: 'Insert Section',
|
label: 'Insert Section',
|
||||||
icon: 'folder-plus',
|
icon: 'folder-plus',
|
||||||
action: () => showToast('Insert section', 'info')
|
action: insertSectionAtPlayhead
|
||||||
}, {
|
}, {
|
||||||
label: 'Insert MIDI item',
|
label: 'Insert MIDI item',
|
||||||
icon: 'music',
|
icon: 'music',
|
||||||
action: () => showToast('Insert MIDI item', 'info')
|
action: insertMidiItemAtPlayhead
|
||||||
}, {
|
}, {
|
||||||
label: 'Insert sound clip',
|
label: 'Insert sound clip',
|
||||||
icon: 'file-input',
|
icon: 'file-input',
|
||||||
action: () => showToast('Insert sound clip', 'info')
|
action: insertSoundClipAtCursor
|
||||||
}, {
|
}, {
|
||||||
label: 'Insert track',
|
label: 'Insert track',
|
||||||
icon: 'plus',
|
icon: 'plus',
|
||||||
action: addNewTrack
|
action: insertTrackBelow
|
||||||
}]
|
}]
|
||||||
}, {
|
}, {
|
||||||
label: 'View',
|
label: 'View',
|
||||||
|
|||||||
@@ -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);
|
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
|
// Selection highlight - local selection on this track
|
||||||
if (selectionMode === 'local' && localSelectionTrackId === track.id && localSelLeft !== null && localSelRight !== null && localSelRight > localSelLeft) {
|
if (selectionMode === 'local' && localSelectionTrackId === track.id && localSelLeft !== null && localSelRight !== null && localSelRight > localSelLeft) {
|
||||||
const hlLeftLocal = localSelLeft * zoom - scrollLeft;
|
const hlLeftLocal = localSelLeft * zoom - scrollLeft;
|
||||||
@@ -3444,7 +3478,10 @@ const App = () => {
|
|||||||
solo: false,
|
solo: false,
|
||||||
color: '#1d4ed8',
|
color: '#1d4ed8',
|
||||||
markers: [],
|
markers: [],
|
||||||
serverFileId: null
|
serverFileId: null,
|
||||||
|
clips: [],
|
||||||
|
sections: [],
|
||||||
|
midiItems: []
|
||||||
}]);
|
}]);
|
||||||
const [bpm, setBpm] = useState(localStorage.getItem('studio_bpm') || '120');
|
const [bpm, setBpm] = useState(localStorage.getItem('studio_bpm') || '120');
|
||||||
const [draggedClip, setDraggedClip] = useState(null); // { trackId, clickOffset, buffer, name, volume, color }
|
const [draggedClip, setDraggedClip] = useState(null); // { trackId, clickOffset, buffer, name, volume, color }
|
||||||
@@ -4333,7 +4370,10 @@ const App = () => {
|
|||||||
solo: false,
|
solo: false,
|
||||||
color: '#0f766e',
|
color: '#0f766e',
|
||||||
markers: [],
|
markers: [],
|
||||||
serverFileId: null
|
serverFileId: null,
|
||||||
|
clips: [],
|
||||||
|
sections: [],
|
||||||
|
midiItems: []
|
||||||
}, {
|
}, {
|
||||||
id: '2',
|
id: '2',
|
||||||
name: 'Track 02',
|
name: 'Track 02',
|
||||||
@@ -4346,10 +4386,11 @@ const App = () => {
|
|||||||
solo: false,
|
solo: false,
|
||||||
color: '#1d4ed8',
|
color: '#1d4ed8',
|
||||||
markers: [],
|
markers: [],
|
||||||
serverFileId: null
|
serverFileId: null,
|
||||||
|
clips: [],
|
||||||
|
sections: [],
|
||||||
|
midiItems: []
|
||||||
}]);
|
}]);
|
||||||
setSelectedTrackId('1');
|
|
||||||
showToast('New project created', 'info');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ctrl && !alt && e.key === 's') {
|
if (ctrl && !alt && e.key === 's') {
|
||||||
@@ -6786,13 +6827,121 @@ const App = () => {
|
|||||||
solo: false,
|
solo: false,
|
||||||
color: selectColor,
|
color: selectColor,
|
||||||
markers: [],
|
markers: [],
|
||||||
serverFileId: null
|
serverFileId: null,
|
||||||
|
clips: [],
|
||||||
|
sections: [],
|
||||||
|
midiItems: []
|
||||||
}]);
|
}]);
|
||||||
showToast(`Đã thêm Track ${newId}.`, 'info');
|
showToast(`Đã thêm Track ${newId}.`, 'info');
|
||||||
setTimeout(() => lucide.createIcons(), 200);
|
setTimeout(() => lucide.createIcons(), 200);
|
||||||
return newId;
|
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 ──
|
// ── Server-side Export ──
|
||||||
const triggerWavExport = async () => {
|
const triggerWavExport = async () => {
|
||||||
let exportTracks;
|
let exportTracks;
|
||||||
@@ -9061,7 +9210,10 @@ const App = () => {
|
|||||||
solo: false,
|
solo: false,
|
||||||
color: '#0f766e',
|
color: '#0f766e',
|
||||||
markers: [],
|
markers: [],
|
||||||
serverFileId: null
|
serverFileId: null,
|
||||||
|
clips: [],
|
||||||
|
sections: [],
|
||||||
|
midiItems: []
|
||||||
}, {
|
}, {
|
||||||
id: '2',
|
id: '2',
|
||||||
name: 'Track 02',
|
name: 'Track 02',
|
||||||
@@ -9074,7 +9226,10 @@ const App = () => {
|
|||||||
solo: false,
|
solo: false,
|
||||||
color: '#1d4ed8',
|
color: '#1d4ed8',
|
||||||
markers: [],
|
markers: [],
|
||||||
serverFileId: null
|
serverFileId: null,
|
||||||
|
clips: [],
|
||||||
|
sections: [],
|
||||||
|
midiItems: []
|
||||||
}]);
|
}]);
|
||||||
setSelectedTrackId('1');
|
setSelectedTrackId('1');
|
||||||
setProjectName('');
|
setProjectName('');
|
||||||
@@ -9224,19 +9379,19 @@ const App = () => {
|
|||||||
items: [{
|
items: [{
|
||||||
label: 'Insert Section',
|
label: 'Insert Section',
|
||||||
icon: 'folder-plus',
|
icon: 'folder-plus',
|
||||||
action: () => showToast('Insert section', 'info')
|
action: insertSectionAtPlayhead
|
||||||
}, {
|
}, {
|
||||||
label: 'Insert MIDI item',
|
label: 'Insert MIDI item',
|
||||||
icon: 'music',
|
icon: 'music',
|
||||||
action: () => showToast('Insert MIDI item', 'info')
|
action: insertMidiItemAtPlayhead
|
||||||
}, {
|
}, {
|
||||||
label: 'Insert sound clip',
|
label: 'Insert sound clip',
|
||||||
icon: 'file-input',
|
icon: 'file-input',
|
||||||
action: () => showToast('Insert sound clip', 'info')
|
action: insertSoundClipAtCursor
|
||||||
}, {
|
}, {
|
||||||
label: 'Insert track',
|
label: 'Insert track',
|
||||||
icon: 'plus',
|
icon: 'plus',
|
||||||
action: addNewTrack
|
action: insertTrackBelow
|
||||||
}]
|
}]
|
||||||
}, {
|
}, {
|
||||||
label: 'View',
|
label: 'View',
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user