feat: allow multiple simultaneous solo tracks

Replace single-track solo (radio) with multi-solo toggle.
No longer clears solo on other tracks when toggling one track.
This commit is contained in:
2026-07-31 07:10:32 +07:00
parent bd296c63f1
commit 59aa9a32ff
2 changed files with 21 additions and 12 deletions
+15 -12
View File
@@ -9198,8 +9198,7 @@ const App = () => {
const [saveProjectModalOpen, setSaveProjectModalOpen] = useState(false);
const [saveAsModalOpen, setSaveAsModalOpen] = useState(false);
const [openProjectModalOpen, setOpenProjectModalOpen] = useState(false);
const soloedTrack = tracks.find(t => t.solo);
const soloedTrackId = soloedTrack ? soloedTrack.id : null;
const hasAnySolo = tracks.some(t => t.solo);
const [toastMessage, setToastMessage] = useState(null);
const [audioDevices, setAudioDevices] = useState([]);
const [midiDevices, setMidiDevices] = useState([]);
@@ -12985,12 +12984,16 @@ const App = () => {
// If selection cleared by user, play linearly (don't loop)
if (!selectionCleared && isLoopingSelection && selLeft !== null && selRight !== null) {
if (selRight > selLeft && updatedTime >= selRight) {
if (soloedTrackId !== null || selectionMode === 'local') {
if (hasAnySolo || selectionMode === 'local') {
stopAllPlayback();
startOffsetTimeRef.current = selLeft;
startAudioTimeRef.current = context.currentTime;
const soloTid = soloedTrackId !== null ? soloedTrackId : localSelectionTrackId;
startLocalTrackPlayback(soloTid, selLeft);
if (hasAnySolo) {
const soloed = tracks.filter(t => t.solo);
soloed.forEach(t => startLocalTrackPlayback(t.id, selLeft));
} else {
startLocalTrackPlayback(localSelectionTrackId, selLeft);
}
setCurrentTime(selLeft);
setIsPlaying(true);
} else {
@@ -13164,9 +13167,9 @@ const App = () => {
const startTrackPlayback = offsetTime => {
const context = getAudioContext();
const hasSolo = activeTracks.some(t => t.solo) || soloedTrackId !== null;
const hasSolo = activeTracks.some(t => t.solo);
activeTracks.forEach(track => {
const isPlayable = hasSolo ? track.id === soloedTrackId || track.solo : !track.muted;
const isPlayable = hasSolo ? track.solo : !track.muted;
if (!isPlayable) return;
const gainNode = getOrCreateTrackNode(track, context);
@@ -20022,8 +20025,8 @@ const App = () => {
toggleTrackSoloEvaluate(track.id);
},
title: "Solo",
className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${soloedTrackId === track.id || track.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
}, /*#__PURE__*/React.createElement("i", { "data-lucide": soloedTrackId === track.id || track.solo ? "headphones" : "headphone-off", className: "w-3 h-3" })), /*#__PURE__*/React.createElement("button", {
className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
}, /*#__PURE__*/React.createElement("i", { "data-lucide": track.solo ? "headphones" : "headphone-off", className: "w-3 h-3" })), /*#__PURE__*/React.createElement("button", {
onClick: e => {
e.stopPropagation();
toggleTrackDrum(track.id);
@@ -20548,7 +20551,7 @@ const App = () => {
e.stopPropagation();
toggleTrackSoloEvaluate(vTrack.id);
},
className: `px-1.5 py-0.5 text-xs rounded font-mono font-bold border ${soloedTrackId === vTrack.id || vTrack.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
className: `px-1.5 py-0.5 text-xs rounded font-mono font-bold border ${vTrack.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
}, "S"))), /*#__PURE__*/React.createElement("div", {
className: "flex flex-col gap-2.5 text-[14px] mb-3"
}, /*#__PURE__*/React.createElement("div", {
@@ -20992,9 +20995,9 @@ const App = () => {
className: "text-amber-400 font-semibold uppercase text-xs"
}, "Local Sel"), selectionMode === 'global' && /*#__PURE__*/React.createElement("span", {
className: "text-purple-400 font-semibold uppercase text-xs"
}, "Global Sel"), soloedTrackId && /*#__PURE__*/React.createElement("span", {
}, "Global Sel"), hasAnySolo && /*#__PURE__*/React.createElement("span", {
className: "text-amber-500 font-semibold"
}, "Solo: ID ", soloedTrackId), isLoopingSelection && selectionMode === 'local' && /*#__PURE__*/React.createElement("span", {
}, "Solo: ", tracks.filter(t => t.solo).length, " track(s)"), isLoopingSelection && selectionMode === 'local' && /*#__PURE__*/React.createElement("span", {
className: "text-emerald-400 font-semibold uppercase text-xs"
}, "Solo Loop"), isLoopingSelection && selectionMode !== 'local' && /*#__PURE__*/React.createElement("span", {
className: "text-cyan-400 font-semibold uppercase text-xs"