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:
+119
-27
@@ -186,6 +186,7 @@ const WaveformLane = ({
|
|||||||
onClipStretchStart,
|
onClipStretchStart,
|
||||||
onSectionItemDragStart,
|
onSectionItemDragStart,
|
||||||
onSectionItemResizeStart,
|
onSectionItemResizeStart,
|
||||||
|
onEditSectionInTab,
|
||||||
onSelectionEdgeDragStart,
|
onSelectionEdgeDragStart,
|
||||||
setSelectedClipId,
|
setSelectedClipId,
|
||||||
selectedClipId,
|
selectedClipId,
|
||||||
@@ -814,6 +815,18 @@ const WaveformLane = ({
|
|||||||
name: track.name,
|
name: track.name,
|
||||||
speed: track.speed || 1.0
|
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));
|
const clickedClip = clips.find(c => time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0));
|
||||||
if (clickedClip) {
|
if (clickedClip) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -830,7 +843,13 @@ const WaveformLane = ({
|
|||||||
const rect = canvasRef.current.getBoundingClientRect();
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left + (scrollLeft || 0);
|
const x = e.clientX - rect.left + (scrollLeft || 0);
|
||||||
const time = Math.max(0, x / zoom);
|
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 [subTabGainVal, setSubTabGainVal] = useState(100);
|
||||||
const [subTabPitchVal, setSubTabPitchVal] = useState(0);
|
const [subTabPitchVal, setSubTabPitchVal] = useState(0);
|
||||||
const [subTabs, setSubTabs] = useState([]); // [{id, label, trackId, startTime, endTime, buffer, effects, currentTime, selectionStart, selectionEnd, isPlaying}, ...]
|
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
|
const [selectionCleared, setSelectionCleared] = useState(false); // LOOP_EDITOR_2.md §4.2
|
||||||
|
|
||||||
|
|
||||||
@@ -4693,6 +4717,27 @@ const App = () => {
|
|||||||
setActiveTab(tabId);
|
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) ──
|
// ── Sub Tab: apply effects & merge back (LOOP_EDITOR_2.md §1.2) ──
|
||||||
const applySubTab = tabId => {
|
const applySubTab = tabId => {
|
||||||
const subTab = subTabs.find(s => s.id === tabId);
|
const subTab = subTabs.find(s => s.id === tabId);
|
||||||
@@ -5028,6 +5073,10 @@ const App = () => {
|
|||||||
setSubTabs(prev => prev.filter(s => s.id !== tabId));
|
setSubTabs(prev => prev.filter(s => s.id !== tabId));
|
||||||
if (activeTab === tabId) setActiveTab('main');
|
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) => {
|
const updateSubTabEffects = (tabId, effects) => {
|
||||||
setSubTabs(prev => prev.map(s => s.id === tabId ? {
|
setSubTabs(prev => prev.map(s => s.id === tabId ? {
|
||||||
...s,
|
...s,
|
||||||
@@ -5073,18 +5122,26 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Context Menu Handlers ──
|
// ── Context Menu Handlers ──
|
||||||
const handleContextMenu = (e, trackId, clickTime) => {
|
const handleContextMenu = (e, trackId, clickTime, sectionId) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setContextMenu({
|
setContextMenu({
|
||||||
x: e.clientX,
|
x: e.clientX,
|
||||||
y: e.clientY,
|
y: e.clientY,
|
||||||
trackId,
|
trackId,
|
||||||
time: clickTime || currentTime
|
time: clickTime || currentTime,
|
||||||
|
sectionId: sectionId || null
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const closeContextMenu = () => setContextMenu(null);
|
const closeContextMenu = () => setContextMenu(null);
|
||||||
|
|
||||||
|
const contextMenuEditSection = () => {
|
||||||
|
if (contextMenu.sectionId) {
|
||||||
|
handleEditSectionInTab(contextMenu.trackId, contextMenu.sectionId);
|
||||||
|
}
|
||||||
|
closeContextMenu();
|
||||||
|
};
|
||||||
|
|
||||||
// Close context menu on any click outside
|
// Close context menu on any click outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
@@ -9208,7 +9265,29 @@ const App = () => {
|
|||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
"data-lucide": "layout-dashboard",
|
"data-lucide": "layout-dashboard",
|
||||||
className: "w-3 h-3"
|
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,
|
key: st.id,
|
||||||
className: "flex items-stretch"
|
className: "flex items-stretch"
|
||||||
}, /*#__PURE__*/React.createElement("button", {
|
}, /*#__PURE__*/React.createElement("button", {
|
||||||
@@ -10197,24 +10276,24 @@ const App = () => {
|
|||||||
className: "flex-1 flex flex-col overflow-hidden min-w-0"
|
className: "flex-1 flex flex-col overflow-hidden min-w-0"
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
}, /*#__PURE__*/React.createElement("div", {
|
||||||
className: "flex-1 flex overflow-hidden"
|
className: "flex-1 flex overflow-hidden"
|
||||||
}, activeTab === 'main' ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
}, activeTab === 'main' || sessionTabs.some(s => s.id === activeTab) ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
||||||
ref: tcpContainerRef,
|
ref: tcpContainerRef,
|
||||||
onScroll: handleTCPScroll,
|
onScroll: handleTCPScroll,
|
||||||
className: "w-[300px] shrink-0 z-20 bg-[#262626] overflow-hidden flex flex-col border-r border-zinc-900",
|
className: "w-[300px] shrink-0 z-20 bg-[#262626] overflow-hidden flex flex-col border-r border-zinc-900",
|
||||||
style: {
|
style: {
|
||||||
scrollbarWidth: 'none',
|
scrollbarWidth: 'none',
|
||||||
msOverflowStyle: 'none'
|
msOverflowStyle: 'none'
|
||||||
}
|
}
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
}, /*#__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"
|
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", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
className: "text-xs font-bold text-zinc-300 flex items-center gap-1.5"
|
className: "text-xs font-bold text-zinc-300 flex items-center gap-1.5"
|
||||||
}, /*#__PURE__*/React.createElement("span", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
className: "inline-flex items-center shrink-0"
|
className: "inline-flex items-center shrink-0"
|
||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
"data-lucide": "sliders",
|
"data-lucide": "sliders",
|
||||||
className: "w-3.5 h-3.5 text-cyan-400"
|
className: "w-3.5 h-3.5 text-cyan-400"
|
||||||
})), "TRACKS (", tracks.length, ")"), /*#__PURE__*/React.createElement("button", {
|
})), "TRACKS (", activeTracks.length, ")"), /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: addNewTrack,
|
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"
|
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", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
@@ -10246,7 +10325,7 @@ const App = () => {
|
|||||||
className: "text-xs text-zinc-500"
|
className: "text-xs text-zinc-500"
|
||||||
}, "BPM")))), /*#__PURE__*/React.createElement("div", {
|
}, "BPM")))), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "flex-1 flex flex-col divide-y divide-[#141414] bg-[#262626]"
|
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"
|
className: "p-6 text-center text-zinc-400 flex flex-col items-center justify-center space-y-3"
|
||||||
}, /*#__PURE__*/React.createElement("span", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
className: "inline-flex items-center shrink-0"
|
className: "inline-flex items-center shrink-0"
|
||||||
@@ -10261,9 +10340,9 @@ const App = () => {
|
|||||||
}, /*#__PURE__*/React.createElement("span", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
className: "inline-flex items-center shrink-0"
|
className: "inline-flex items-center shrink-0"
|
||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
"data-lucide": "plus",
|
"data-lucide": "plus-circle",
|
||||||
className: "w-3.5 h-3.5"
|
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;
|
const isSelected = selectedTrackId === track.id;
|
||||||
return /*#__PURE__*/React.createElement("div", {
|
return /*#__PURE__*/React.createElement("div", {
|
||||||
key: track.id,
|
key: track.id,
|
||||||
@@ -10531,7 +10610,7 @@ const App = () => {
|
|||||||
scrollLeft: scrollLeft
|
scrollLeft: scrollLeft
|
||||||
}))), /*#__PURE__*/React.createElement("div", {
|
}))), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "flex-1 flex flex-col divide-y divide-[#141414] relative bg-[#111111] min-h-full"
|
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;
|
const isSelected = selectedTrackId === track.id;
|
||||||
return /*#__PURE__*/React.createElement("div", {
|
return /*#__PURE__*/React.createElement("div", {
|
||||||
key: track.id,
|
key: track.id,
|
||||||
@@ -10567,6 +10646,7 @@ const App = () => {
|
|||||||
activeTool: activeTool,
|
activeTool: activeTool,
|
||||||
onSplitTrackAtTime: handleSplitTrackAtTime,
|
onSplitTrackAtTime: handleSplitTrackAtTime,
|
||||||
onEditClipInSubTab: handleEditClipInSubTab,
|
onEditClipInSubTab: handleEditClipInSubTab,
|
||||||
|
onEditSectionInTab: handleEditSectionInTab,
|
||||||
snapValue: snapValue,
|
snapValue: snapValue,
|
||||||
bpm: bpm,
|
bpm: bpm,
|
||||||
selectionMode: selectionMode,
|
selectionMode: selectionMode,
|
||||||
@@ -11295,7 +11375,19 @@ const App = () => {
|
|||||||
top: contextMenu.y
|
top: contextMenu.y
|
||||||
},
|
},
|
||||||
onClick: e => e.stopPropagation()
|
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,
|
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"
|
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", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ const WaveformLane = ({
|
|||||||
onClipStretchStart,
|
onClipStretchStart,
|
||||||
onSectionItemDragStart,
|
onSectionItemDragStart,
|
||||||
onSectionItemResizeStart,
|
onSectionItemResizeStart,
|
||||||
|
onEditSectionInTab,
|
||||||
onSelectionEdgeDragStart,
|
onSelectionEdgeDragStart,
|
||||||
setSelectedClipId,
|
setSelectedClipId,
|
||||||
selectedClipId,
|
selectedClipId,
|
||||||
@@ -814,6 +815,18 @@ const WaveformLane = ({
|
|||||||
name: track.name,
|
name: track.name,
|
||||||
speed: track.speed || 1.0
|
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));
|
const clickedClip = clips.find(c => time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0));
|
||||||
if (clickedClip) {
|
if (clickedClip) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -830,7 +843,12 @@ const WaveformLane = ({
|
|||||||
const rect = canvasRef.current.getBoundingClientRect();
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left + (scrollLeft || 0);
|
const x = e.clientX - rect.left + (scrollLeft || 0);
|
||||||
const time = Math.max(0, x / zoom);
|
const time = Math.max(0, x / zoom);
|
||||||
if (onContextMenu) onContextMenu(e, track.id, time);
|
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);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@@ -3778,6 +3796,11 @@ const App = () => {
|
|||||||
const [subTabGainVal, setSubTabGainVal] = useState(100);
|
const [subTabGainVal, setSubTabGainVal] = useState(100);
|
||||||
const [subTabPitchVal, setSubTabPitchVal] = useState(0);
|
const [subTabPitchVal, setSubTabPitchVal] = useState(0);
|
||||||
const [subTabs, setSubTabs] = useState([]); // [{id, label, trackId, startTime, endTime, buffer, effects, currentTime, selectionStart, selectionEnd, isPlaying}, ...]
|
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
|
const [selectionCleared, setSelectionCleared] = useState(false); // LOOP_EDITOR_2.md §4.2
|
||||||
|
|
||||||
// ── Temp Edit Tab (LOOP_EDITOR_2.md §1.2 Sub Tab) ──
|
// ── Temp Edit Tab (LOOP_EDITOR_2.md §1.2 Sub Tab) ──
|
||||||
@@ -4753,6 +4776,27 @@ const App = () => {
|
|||||||
setActiveTab(tabId);
|
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) ──
|
// ── Sub Tab: apply effects & merge back (LOOP_EDITOR_2.md §1.2) ──
|
||||||
const applySubTab = tabId => {
|
const applySubTab = tabId => {
|
||||||
const subTab = subTabs.find(s => s.id === tabId);
|
const subTab = subTabs.find(s => s.id === tabId);
|
||||||
@@ -5088,6 +5132,10 @@ const App = () => {
|
|||||||
setSubTabs(prev => prev.filter(s => s.id !== tabId));
|
setSubTabs(prev => prev.filter(s => s.id !== tabId));
|
||||||
if (activeTab === tabId) setActiveTab('main');
|
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) => {
|
const updateSubTabEffects = (tabId, effects) => {
|
||||||
setSubTabs(prev => prev.map(s => s.id === tabId ? {
|
setSubTabs(prev => prev.map(s => s.id === tabId ? {
|
||||||
...s,
|
...s,
|
||||||
@@ -5133,18 +5181,26 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Context Menu Handlers ──
|
// ── Context Menu Handlers ──
|
||||||
const handleContextMenu = (e, trackId, clickTime) => {
|
const handleContextMenu = (e, trackId, clickTime, sectionId) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setContextMenu({
|
setContextMenu({
|
||||||
x: e.clientX,
|
x: e.clientX,
|
||||||
y: e.clientY,
|
y: e.clientY,
|
||||||
trackId,
|
trackId,
|
||||||
time: clickTime || currentTime
|
time: clickTime || currentTime,
|
||||||
|
sectionId: sectionId || null
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const closeContextMenu = () => setContextMenu(null);
|
const closeContextMenu = () => setContextMenu(null);
|
||||||
|
|
||||||
|
const contextMenuEditSection = () => {
|
||||||
|
if (contextMenu.sectionId) {
|
||||||
|
handleEditSectionInTab(contextMenu.trackId, contextMenu.sectionId);
|
||||||
|
}
|
||||||
|
closeContextMenu();
|
||||||
|
};
|
||||||
|
|
||||||
// Close context menu on any click outside
|
// Close context menu on any click outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
@@ -9701,7 +9757,29 @@ const App = () => {
|
|||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
"data-lucide": "layout-dashboard",
|
"data-lucide": "layout-dashboard",
|
||||||
className: "w-3 h-3"
|
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,
|
key: st.id,
|
||||||
className: "flex items-stretch"
|
className: "flex items-stretch"
|
||||||
}, /*#__PURE__*/React.createElement("button", {
|
}, /*#__PURE__*/React.createElement("button", {
|
||||||
@@ -10748,7 +10826,7 @@ const App = () => {
|
|||||||
className: "flex-1 flex flex-col overflow-hidden min-w-0"
|
className: "flex-1 flex flex-col overflow-hidden min-w-0"
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
}, /*#__PURE__*/React.createElement("div", {
|
||||||
className: "flex-1 flex overflow-hidden"
|
className: "flex-1 flex overflow-hidden"
|
||||||
}, activeTab === 'main' ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
}, activeTab === 'main' || sessionTabs.some(s => s.id === activeTab) ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
||||||
ref: tcpContainerRef,
|
ref: tcpContainerRef,
|
||||||
onScroll: handleTCPScroll,
|
onScroll: handleTCPScroll,
|
||||||
className: "w-[300px] shrink-0 z-20 bg-[#262626] overflow-hidden flex flex-col border-r border-zinc-900",
|
className: "w-[300px] shrink-0 z-20 bg-[#262626] overflow-hidden flex flex-col border-r border-zinc-900",
|
||||||
@@ -10765,7 +10843,7 @@ const App = () => {
|
|||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
"data-lucide": "sliders",
|
"data-lucide": "sliders",
|
||||||
className: "w-3.5 h-3.5 text-cyan-400"
|
className: "w-3.5 h-3.5 text-cyan-400"
|
||||||
})), "TRACKS (", tracks.length, ")"), /*#__PURE__*/React.createElement("button", {
|
})), "TRACKS (", activeTracks.length, ")"), /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: addNewTrack,
|
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"
|
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", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
@@ -10797,7 +10875,7 @@ const App = () => {
|
|||||||
className: "text-xs text-zinc-500"
|
className: "text-xs text-zinc-500"
|
||||||
}, "BPM")))), /*#__PURE__*/React.createElement("div", {
|
}, "BPM")))), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "flex-1 flex flex-col divide-y divide-[#141414] bg-[#262626]"
|
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"
|
className: "p-6 text-center text-zinc-400 flex flex-col items-center justify-center space-y-3"
|
||||||
}, /*#__PURE__*/React.createElement("span", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
className: "inline-flex items-center shrink-0"
|
className: "inline-flex items-center shrink-0"
|
||||||
@@ -10814,7 +10892,7 @@ const App = () => {
|
|||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
"data-lucide": "plus",
|
"data-lucide": "plus",
|
||||||
className: "w-3.5 h-3.5"
|
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;
|
const isSelected = selectedTrackId === track.id;
|
||||||
return /*#__PURE__*/React.createElement("div", {
|
return /*#__PURE__*/React.createElement("div", {
|
||||||
key: track.id,
|
key: track.id,
|
||||||
@@ -11092,7 +11170,7 @@ const App = () => {
|
|||||||
scrollLeft: scrollLeft
|
scrollLeft: scrollLeft
|
||||||
}))), /*#__PURE__*/React.createElement("div", {
|
}))), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "flex-1 flex flex-col divide-y divide-[#141414] relative bg-[#111111] min-h-full"
|
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;
|
const isSelected = selectedTrackId === track.id;
|
||||||
return /*#__PURE__*/React.createElement("div", {
|
return /*#__PURE__*/React.createElement("div", {
|
||||||
key: track.id,
|
key: track.id,
|
||||||
@@ -11128,6 +11206,7 @@ const App = () => {
|
|||||||
activeTool: activeTool,
|
activeTool: activeTool,
|
||||||
onSplitTrackAtTime: handleSplitTrackAtTime,
|
onSplitTrackAtTime: handleSplitTrackAtTime,
|
||||||
onEditClipInSubTab: handleEditClipInSubTab,
|
onEditClipInSubTab: handleEditClipInSubTab,
|
||||||
|
onEditSectionInTab: handleEditSectionInTab,
|
||||||
snapValue: snapValue,
|
snapValue: snapValue,
|
||||||
bpm: bpm,
|
bpm: bpm,
|
||||||
selectionMode: selectionMode,
|
selectionMode: selectionMode,
|
||||||
@@ -11856,7 +11935,19 @@ const App = () => {
|
|||||||
top: contextMenu.y
|
top: contextMenu.y
|
||||||
},
|
},
|
||||||
onClick: e => e.stopPropagation()
|
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,
|
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"
|
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", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user