feat: bổ sung logic của phần section: thêm các items, xóa track
This commit is contained in:
+74
-23
@@ -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
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user