feat: bổ sung logic của phần section: thêm các items, xóa track

This commit is contained in:
2026-07-22 22:03:17 +07:00
parent 97a53fe59a
commit 6545e1746e
3 changed files with 168 additions and 66 deletions
+74 -23
View File
@@ -5176,15 +5176,27 @@ const App = () => {
};
const contextMenuDelete = () => {
const tid = contextMenu.trackId;
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');
}
if (selectedTrackId === tid) setSelectedTrackId(tracks.filter(t => t.id !== tid)[0]?.id || '1');
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;
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(tracks.filter(t => t.id !== tid)[0]?.id || '1');
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 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;
}
const beforeSnap = captureTrackSnapshot(trackId);
setTracks(prev => {
const filtered = prev.filter(t => t.id !== trackId);
if (filtered.length > 0) {
setSelectedTrackId(filtered[0].id);
}
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
+74 -23
View File
@@ -5235,15 +5235,27 @@ const App = () => {
};
const contextMenuDelete = () => {
const tid = contextMenu.trackId;
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');
}
if (selectedTrackId === tid) setSelectedTrackId(tracks.filter(t => t.id !== tid)[0]?.id || '1');
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;
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(tracks.filter(t => t.id !== tid)[0]?.id || '1');
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 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;
}
const beforeSnap = captureTrackSnapshot(trackId);
setTracks(prev => {
const filtered = prev.filter(t => t.id !== trackId);
if (filtered.length > 0) {
setSelectedTrackId(filtered[0].id);
}
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
Binary file not shown.