feat: bổ sung phần section và nhấp đôi để sửa section, qui định tab vừa mở là con của item section

This commit is contained in:
2026-07-22 21:36:16 +07:00
parent 5a80657b2b
commit 97a53fe59a
3 changed files with 220 additions and 37 deletions
+119 -27
View File
@@ -186,6 +186,7 @@ const WaveformLane = ({
onClipStretchStart,
onSectionItemDragStart,
onSectionItemResizeStart,
onEditSectionInTab,
onSelectionEdgeDragStart,
setSelectedClipId,
selectedClipId,
@@ -814,6 +815,18 @@ const WaveformLane = ({
name: track.name,
speed: track.speed || 1.0
}] : [];
// Check double-click on section first
const dblSecItems = track.sections || [];
let dblSecHit = null;
for (const sec of dblSecItems) {
if (time >= sec.start && time < sec.start + sec.duration) { dblSecHit = sec; break; }
}
if (dblSecHit) {
e.preventDefault();
e.stopPropagation();
if (onEditSectionInTab) onEditSectionInTab(track.id, dblSecHit.id);
return;
}
const clickedClip = clips.find(c => time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0));
if (clickedClip) {
e.preventDefault();
@@ -830,7 +843,13 @@ const WaveformLane = ({
const rect = canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left + (scrollLeft || 0);
const time = Math.max(0, x / zoom);
if (onContextMenu) onContextMenu(e, track.id, time);
// Detect section under cursor
const secList = track.sections || [];
let hitSectionId = null;
for (const sec of secList) {
if (time >= sec.start && time < sec.start + sec.duration) { hitSectionId = sec.id; break; }
}
if (onContextMenu) onContextMenu(e, track.id, time, hitSectionId);
}
}));
};
@@ -3727,6 +3746,11 @@ const App = () => {
const [subTabGainVal, setSubTabGainVal] = useState(100);
const [subTabPitchVal, setSubTabPitchVal] = useState(0);
const [subTabs, setSubTabs] = useState([]); // [{id, label, trackId, startTime, endTime, buffer, effects, currentTime, selectionStart, selectionEnd, isPlaying}, ...]
const [sessionTabs, setSessionTabs] = useState([]); // [{id, name, tracks}, ...]
const activeTracks = useMemo(() => {
const st = sessionTabs.find(s => s.id === activeTab);
return st ? st.tracks : tracks;
}, [activeTab, sessionTabs, tracks]);
const [selectionCleared, setSelectionCleared] = useState(false); // LOOP_EDITOR_2.md §4.2
@@ -4693,6 +4717,27 @@ const App = () => {
setActiveTab(tabId);
};
// Double-click/Edit Section: open Main Session in new tab
const handleEditSectionInTab = (trackId, sectionId) => {
const track = tracks.find(t => t.id === trackId);
if (!track) return;
const section = (track.sections || []).find(s => s.id === sectionId);
if (!section) return;
const existing = sessionTabs.find(s => s.sectionId === sectionId);
if (existing) { setActiveTab(existing.id); showToast(`Tab "${section.name}" already open.`, 'info'); return; }
const tabId = 'session_' + Date.now();
const tabName = section.name || 'Section';
const clonedTracks = tracks.map(t => ({
...t,
clips: [],
sections: [],
midiItems: [],
markers: []
}));
setSessionTabs(prev => [...prev, { id: tabId, name: tabName, sectionId: sectionId, tracks: clonedTracks }]);
setActiveTab(tabId);
};
// Sub Tab: apply effects & merge back (LOOP_EDITOR_2.md §1.2)
const applySubTab = tabId => {
const subTab = subTabs.find(s => s.id === tabId);
@@ -5028,6 +5073,10 @@ const App = () => {
setSubTabs(prev => prev.filter(s => s.id !== tabId));
if (activeTab === tabId) setActiveTab('main');
};
const closeSessionTab = tabId => {
setSessionTabs(prev => prev.filter(s => s.id !== tabId));
if (activeTab === tabId) setActiveTab('main');
};
const updateSubTabEffects = (tabId, effects) => {
setSubTabs(prev => prev.map(s => s.id === tabId ? {
...s,
@@ -5073,18 +5122,26 @@ const App = () => {
};
// Context Menu Handlers
const handleContextMenu = (e, trackId, clickTime) => {
const handleContextMenu = (e, trackId, clickTime, sectionId) => {
e.preventDefault();
e.stopPropagation();
setContextMenu({
x: e.clientX,
y: e.clientY,
trackId,
time: clickTime || currentTime
time: clickTime || currentTime,
sectionId: sectionId || null
});
};
const closeContextMenu = () => setContextMenu(null);
const contextMenuEditSection = () => {
if (contextMenu.sectionId) {
handleEditSectionInTab(contextMenu.trackId, contextMenu.sectionId);
}
closeContextMenu();
};
// Close context menu on any click outside
useEffect(() => {
const handler = () => {
@@ -9208,7 +9265,29 @@ const App = () => {
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "layout-dashboard",
className: "w-3 h-3"
})), " Main Session"), subTabs.map(st => /*#__PURE__*/React.createElement("div", {
})), " Main Session"), sessionTabs.map(st => /*#__PURE__*/React.createElement("div", {
key: st.id,
className: "flex items-stretch"
}, /*#__PURE__*/React.createElement("button", {
onClick: () => setActiveTab(st.id),
className: `px-2 text-xs font-medium border-b-2 transition flex items-center gap-1 ${activeTab === st.id ? 'text-cyan-400 border-cyan-500 bg-zinc-800/50' : 'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'}`
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "layout-dashboard",
className: "w-3 h-3"
})), /*#__PURE__*/React.createElement("span", {
className: "max-w-[120px] truncate"
}, st.name)), /*#__PURE__*/React.createElement("button", {
onClick: () => closeSessionTab(st.id),
className: "px-1 text-zinc-600 hover:text-red-400 transition text-xs",
title: "Close tab"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "x",
className: "w-3 h-3"
}))))), subTabs.map(st => /*#__PURE__*/React.createElement("div", {
key: st.id,
className: "flex items-stretch"
}, /*#__PURE__*/React.createElement("button", {
@@ -10197,24 +10276,24 @@ const App = () => {
className: "flex-1 flex flex-col overflow-hidden min-w-0"
}, /*#__PURE__*/React.createElement("div", {
className: "flex-1 flex overflow-hidden"
}, activeTab === 'main' ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
ref: tcpContainerRef,
onScroll: handleTCPScroll,
className: "w-[300px] shrink-0 z-20 bg-[#262626] overflow-hidden flex flex-col border-r border-zinc-900",
style: {
scrollbarWidth: 'none',
msOverflowStyle: 'none'
}
}, /*#__PURE__*/React.createElement("div", {
className: "sticky top-0 z-50 flex items-center justify-between h-10 border-b border-zinc-900 bg-[#242424] px-2 shrink-0"
}, /*#__PURE__*/React.createElement("span", {
className: "text-xs font-bold text-zinc-300 flex items-center gap-1.5"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "sliders",
className: "w-3.5 h-3.5 text-cyan-400"
})), "TRACKS (", tracks.length, ")"), /*#__PURE__*/React.createElement("button", {
}, activeTab === 'main' || sessionTabs.some(s => s.id === activeTab) ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
ref: tcpContainerRef,
onScroll: handleTCPScroll,
className: "w-[300px] shrink-0 z-20 bg-[#262626] overflow-hidden flex flex-col border-r border-zinc-900",
style: {
scrollbarWidth: 'none',
msOverflowStyle: 'none'
}
}, /*#__PURE__*/React.createElement("div", {
className: "sticky top-0 z-50 flex items-center justify-between h-10 border-b border-zinc-900 bg-[#242424] px-2 shrink-0"
}, /*#__PURE__*/React.createElement("span", {
className: "text-xs font-bold text-zinc-300 flex items-center gap-1.5"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "sliders",
className: "w-3.5 h-3.5 text-cyan-400"
})), "TRACKS (", activeTracks.length, ")"), /*#__PURE__*/React.createElement("button", {
onClick: addNewTrack,
className: "px-2.5 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-xs font-semibold flex items-center gap-1 shadow transition"
}, /*#__PURE__*/React.createElement("span", {
@@ -10246,7 +10325,7 @@ const App = () => {
className: "text-xs text-zinc-500"
}, "BPM")))), /*#__PURE__*/React.createElement("div", {
className: "flex-1 flex flex-col divide-y divide-[#141414] bg-[#262626]"
}, tracks.length === 0 ? /*#__PURE__*/React.createElement("div", {
}, activeTracks.length === 0 ? /*#__PURE__*/React.createElement("div", {
className: "p-6 text-center text-zinc-400 flex flex-col items-center justify-center space-y-3"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
@@ -10261,9 +10340,9 @@ const App = () => {
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "plus",
"data-lucide": "plus-circle",
className: "w-3.5 h-3.5"
})), " Thêm Track Mới")) : tracks.map((track, idx) => {
})), " Thêm Track Mới")) : activeTracks.map((track, idx) => {
const isSelected = selectedTrackId === track.id;
return /*#__PURE__*/React.createElement("div", {
key: track.id,
@@ -10531,7 +10610,7 @@ const App = () => {
scrollLeft: scrollLeft
}))), /*#__PURE__*/React.createElement("div", {
className: "flex-1 flex flex-col divide-y divide-[#141414] relative bg-[#111111] min-h-full"
}, tracks.map((track, idx) => {
}, activeTracks.map((track, idx) => {
const isSelected = selectedTrackId === track.id;
return /*#__PURE__*/React.createElement("div", {
key: track.id,
@@ -10567,6 +10646,7 @@ const App = () => {
activeTool: activeTool,
onSplitTrackAtTime: handleSplitTrackAtTime,
onEditClipInSubTab: handleEditClipInSubTab,
onEditSectionInTab: handleEditSectionInTab,
snapValue: snapValue,
bpm: bpm,
selectionMode: selectionMode,
@@ -11295,7 +11375,19 @@ const App = () => {
top: contextMenu.y
},
onClick: e => e.stopPropagation()
}, /*#__PURE__*/React.createElement("button", {
}, contextMenu.sectionId ? /*#__PURE__*/React.createElement("button", {
onClick: contextMenuEditSection,
className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "file-edit",
className: "w-3.5 h-3.5 text-amber-400 shrink-0"
})), /*#__PURE__*/React.createElement("span", {
className: "flex-1"
}, "Edit Section"), /*#__PURE__*/React.createElement("span", {
className: "text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"
}, "Ctrl+E")) : /*#__PURE__*/React.createElement("button", {
onClick: contextMenuEdit,
className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
}, /*#__PURE__*/React.createElement("span", {