feat: Mixer Panel display-only with F7 toggle
Add MixerStrip component for display-only track strips (no audio routing). Mixer panel at bottom with drag resize handle (80-400px). F7 keyboard shortcut, status bar toggle button, View menu item. onUpdateTrack handles mute/solo/volume changes directly on tracks.
This commit is contained in:
+133
-3
@@ -371,6 +371,52 @@ const VolumeKnob = ({
|
|||||||
strokeLinecap: "round"
|
strokeLinecap: "round"
|
||||||
}))));
|
}))));
|
||||||
};
|
};
|
||||||
|
const MixerStrip = ({ track, index, onUpdateTrack }) => {
|
||||||
|
const dbLabel = (track.volumeDb == null || track.volumeDb <= -50) ? '-inf' : (track.volumeDb > 0 ? '+' : '') + (track.volumeDb || 0).toFixed(1) + 'dB';
|
||||||
|
const panLabel = track.pan === 0 ? 'C' : (track.pan < 0 ? 'L' + Math.round(Math.abs(track.pan)) : 'R' + Math.round(track.pan));
|
||||||
|
const isMuted = track.muted;
|
||||||
|
const isSoloed = track.solo;
|
||||||
|
const panDeg = (track.pan || 0) * 1.35;
|
||||||
|
return React.createElement("div", {
|
||||||
|
className: "flex flex-col items-stretch w-14 shrink-0 bg-[#2b2b2b] border border-black/70 overflow-hidden rounded-sm"
|
||||||
|
},
|
||||||
|
React.createElement("div", {
|
||||||
|
className: "flex items-center justify-between px-1 py-0.5 bg-[#222] border-b border-black/60 shrink-0"
|
||||||
|
}, React.createElement("span", {
|
||||||
|
className: "text-[9px] font-mono font-bold text-zinc-400"
|
||||||
|
}, (index + 1))),
|
||||||
|
React.createElement("div", {
|
||||||
|
className: "flex items-center justify-center gap-1 py-0.5 shrink-0"
|
||||||
|
}, React.createElement("button", {
|
||||||
|
onClick: e => { e.stopPropagation(); if (onUpdateTrack) onUpdateTrack(track.id, { muted: !track.muted }); },
|
||||||
|
title: "Mute",
|
||||||
|
className: "w-5 h-5 flex items-center justify-center rounded-sm text-[10px] font-mono font-bold border transition " + (isMuted ? 'bg-orange-500 text-black border-orange-400' : 'bg-[#3a3a3a] text-zinc-400 border-black/60 hover:text-zinc-100')
|
||||||
|
}, "M"), React.createElement("button", {
|
||||||
|
onClick: e => { e.stopPropagation(); if (onUpdateTrack) onUpdateTrack(track.id, { solo: !track.solo }); },
|
||||||
|
title: "Solo",
|
||||||
|
className: "w-5 h-5 flex items-center justify-center rounded-sm text-[10px] font-mono font-bold border transition " + (isSoloed ? 'bg-yellow-400 text-black border-yellow-300' : 'bg-[#3a3a3a] text-zinc-400 border-black/60 hover:text-zinc-100')
|
||||||
|
}, "S")),
|
||||||
|
React.createElement("div", {
|
||||||
|
className: "flex-1 flex items-end justify-center gap-1 px-1 py-1 min-h-0"
|
||||||
|
}, React.createElement("input", {
|
||||||
|
type: "range",
|
||||||
|
min: "-60",
|
||||||
|
max: "12",
|
||||||
|
step: "0.5",
|
||||||
|
value: track.volumeDb != null ? track.volumeDb : 0,
|
||||||
|
onChange: e => { if (onUpdateTrack) onUpdateTrack(track.id, { volumeDb: parseFloat(e.target.value) }); },
|
||||||
|
className: "w-3 bg-[#161616] rounded appearance-none cursor-pointer accent-orange-500 border border-black/60",
|
||||||
|
style: { writingMode: 'vertical-lr', direction: 'rtl' }
|
||||||
|
}), React.createElement("div", {
|
||||||
|
className: "w-2.5 rounded-sm bg-[#0d0d0d] border border-black/60"
|
||||||
|
})),
|
||||||
|
React.createElement("div", {
|
||||||
|
className: "text-center text-[9px] font-mono font-bold py-0.5 " + ((track.volumeDb || 0) > 0 ? 'text-orange-400' : 'text-zinc-300') + " bg-[#1c1c1c] border-t border-black/50 shrink-0"
|
||||||
|
}, dbLabel),
|
||||||
|
React.createElement("div", {
|
||||||
|
className: "text-[8px] font-mono text-zinc-400 truncate w-full text-center px-1 py-0.5 bg-[#222] border-t border-black/60 shrink-0"
|
||||||
|
}, track.name));
|
||||||
|
};
|
||||||
const WaveformLane = ({
|
const WaveformLane = ({
|
||||||
track,
|
track,
|
||||||
zoom,
|
zoom,
|
||||||
@@ -7174,6 +7220,13 @@ const App = () => {
|
|||||||
scrollBufferExtraRef.current = scrollBufferExtra;
|
scrollBufferExtraRef.current = scrollBufferExtra;
|
||||||
const [showFxRack, setShowFxRack] = useState(false);
|
const [showFxRack, setShowFxRack] = useState(false);
|
||||||
const [showMidiEvents, setShowMidiEvents] = useState(false);
|
const [showMidiEvents, setShowMidiEvents] = useState(false);
|
||||||
|
const [showMixer, setShowMixer] = useState(false);
|
||||||
|
const setShowMixerRef = useRef(setShowMixer);
|
||||||
|
setShowMixerRef.current = setShowMixer;
|
||||||
|
const [mixerHeight, setMixerHeight] = useState(function() {
|
||||||
|
var saved = localStorage.getItem('studio_mixer_height');
|
||||||
|
return saved ? parseInt(saved) : 200;
|
||||||
|
}());
|
||||||
const [rightSidebarWidth, setRightSidebarWidth] = useState(320);
|
const [rightSidebarWidth, setRightSidebarWidth] = useState(320);
|
||||||
const [tcpWidth, setTcpWidth] = useState(320);
|
const [tcpWidth, setTcpWidth] = useState(320);
|
||||||
const [mediaExplorerHeight, setMediaExplorerHeight] = useState(50);
|
const [mediaExplorerHeight, setMediaExplorerHeight] = useState(50);
|
||||||
@@ -8330,6 +8383,11 @@ const App = () => {
|
|||||||
handleSplitTrackRef.current(selectedTrackIdRef.current);
|
handleSplitTrackRef.current(selectedTrackIdRef.current);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (e.key === 'F7') {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowMixerRef.current(p => !p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', handler, { capture: true });
|
window.addEventListener('keydown', handler, { capture: true });
|
||||||
return () => window.removeEventListener('keydown', handler, { capture: true });
|
return () => window.removeEventListener('keydown', handler, { capture: true });
|
||||||
@@ -12426,6 +12484,10 @@ const App = () => {
|
|||||||
});
|
});
|
||||||
setTimeout(() => lucide.createIcons(), 50);
|
setTimeout(() => lucide.createIcons(), 50);
|
||||||
};
|
};
|
||||||
|
const updateTrackProp = (trackId, props) => {
|
||||||
|
setTracks(prev => prev.map(t => t.id === trackId ? { ...t, ...props } : t));
|
||||||
|
setTimeout(() => lucide.createIcons(), 50);
|
||||||
|
};
|
||||||
const toggleTrackDrum = trackId => {
|
const toggleTrackDrum = trackId => {
|
||||||
setTracks(prev => prev.map(t => t.id === trackId ? {
|
setTracks(prev => prev.map(t => t.id === trackId ? {
|
||||||
...t,
|
...t,
|
||||||
@@ -15314,7 +15376,7 @@ const App = () => {
|
|||||||
}, {
|
}, {
|
||||||
label: 'Mixer',
|
label: 'Mixer',
|
||||||
icon: 'sliders',
|
icon: 'sliders',
|
||||||
action: () => showToast('Mixer panel', 'info')
|
action: () => setShowMixer(p => !p)
|
||||||
}, {
|
}, {
|
||||||
label: 'Tempo Track',
|
label: 'Tempo Track',
|
||||||
icon: 'timer',
|
icon: 'timer',
|
||||||
@@ -17713,7 +17775,66 @@ const App = () => {
|
|||||||
})())), /*#__PURE__*/React.createElement("div", {
|
})())), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "w-1 cursor-ew-resize hover:bg-cyan-500/50 active:bg-cyan-400 transition-colors shrink-0 z-10",
|
className: "w-1 cursor-ew-resize hover:bg-cyan-500/50 active:bg-cyan-400 transition-colors shrink-0 z-10",
|
||||||
onMouseDown: startColResize
|
onMouseDown: startColResize
|
||||||
}), renderDock('right', 'Right')), renderDock('bottom', 'Bottom'));
|
}), renderDock('right', 'Right')), renderDock('bottom', 'Bottom'), showMixer && /*#__PURE__*/React.createElement("div", {
|
||||||
|
className: "flex flex-col border-t border-zinc-700 bg-slate-900 shrink-0 select-none",
|
||||||
|
style: { height: mixerHeight + 'px' }
|
||||||
|
}, /*#__PURE__*/React.createElement("div", {
|
||||||
|
className: "flex items-center justify-between px-2 py-0.5 bg-zinc-800/80 shrink-0 cursor-ns-resize",
|
||||||
|
onMouseDown: e => {
|
||||||
|
e.preventDefault();
|
||||||
|
var startY = e.clientY;
|
||||||
|
var startH = mixerHeight;
|
||||||
|
var onMove = function(ev) {
|
||||||
|
var newH = Math.max(80, Math.min(400, startH - (ev.clientY - startY)));
|
||||||
|
setMixerHeight(newH);
|
||||||
|
localStorage.setItem('studio_mixer_height', newH.toString());
|
||||||
|
};
|
||||||
|
var onUp = function() {
|
||||||
|
document.removeEventListener('mousemove', onMove);
|
||||||
|
document.removeEventListener('mouseup', onUp);
|
||||||
|
};
|
||||||
|
document.addEventListener('mousemove', onMove);
|
||||||
|
document.addEventListener('mouseup', onUp);
|
||||||
|
}
|
||||||
|
}, /*#__PURE__*/React.createElement("div", {
|
||||||
|
className: "flex items-center gap-2"
|
||||||
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
|
className: "inline-flex items-center shrink-0"
|
||||||
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
|
"data-lucide": "grip-horizontal",
|
||||||
|
className: "w-3 h-3 text-zinc-600"
|
||||||
|
})), /*#__PURE__*/React.createElement("span", {
|
||||||
|
className: "text-[10px] font-bold text-zinc-400 uppercase tracking-wider"
|
||||||
|
}, "MIXER")), /*#__PURE__*/React.createElement("button", {
|
||||||
|
onClick: () => setShowMixer(false),
|
||||||
|
className: "p-0.5 rounded text-zinc-500 hover:text-zinc-200 hover:bg-zinc-700 transition"
|
||||||
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
|
className: "inline-flex items-center shrink-0"
|
||||||
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
|
"data-lucide": "x",
|
||||||
|
className: "w-3 h-3"
|
||||||
|
})))), /*#__PURE__*/React.createElement("div", {
|
||||||
|
className: "flex-1 flex overflow-x-auto p-1.5 gap-1.5 items-stretch"
|
||||||
|
}, /*#__PURE__*/React.createElement("div", {
|
||||||
|
className: "flex flex-col items-center justify-between w-14 shrink-0 bg-[#2b2b2b] border border-black/70 overflow-hidden rounded-sm"
|
||||||
|
}, /*#__PURE__*/React.createElement("div", {
|
||||||
|
className: "text-[9px] font-bold text-zinc-300 uppercase tracking-wider w-full text-center py-0.5 bg-[#222] border-b border-black/60"
|
||||||
|
}, "MASTER"), /*#__PURE__*/React.createElement("div", {
|
||||||
|
className: "flex-1 flex items-end justify-center w-full px-1 py-1"
|
||||||
|
}, /*#__PURE__*/React.createElement("div", {
|
||||||
|
className: "w-2.5 rounded-sm bg-[#0d0d0d] border border-black/60"
|
||||||
|
})), /*#__PURE__*/React.createElement("div", {
|
||||||
|
className: "text-[8px] font-mono font-bold text-zinc-500 w-full text-center py-0.5 bg-[#222] border-t border-black/60"
|
||||||
|
}, "MAIN OUT")), activeTracks.length > 0 && /*#__PURE__*/React.createElement("div", {
|
||||||
|
className: "w-px bg-zinc-700 shrink-0 self-stretch mx-0.5"
|
||||||
|
}), activeTracks.map(function(track, idx) {
|
||||||
|
return /*#__PURE__*/React.createElement(MixerStrip, {
|
||||||
|
key: track.id,
|
||||||
|
track: track,
|
||||||
|
index: idx,
|
||||||
|
onUpdateTrack: updateTrackProp
|
||||||
|
});
|
||||||
|
}))));
|
||||||
})(), /*#__PURE__*/React.createElement("div", {
|
})(), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "h-6 bg-[#141414] border-t border-zinc-900 px-4 flex items-center justify-between text-xs text-zinc-500 select-none shrink-0"
|
className: "h-6 bg-[#141414] border-t border-zinc-900 px-4 flex items-center justify-between text-xs text-zinc-500 select-none shrink-0"
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
}, /*#__PURE__*/React.createElement("div", {
|
||||||
@@ -17789,7 +17910,16 @@ const App = () => {
|
|||||||
className: "w-3 h-3"
|
className: "w-3 h-3"
|
||||||
})), /*#__PURE__*/React.createElement("span", {
|
})), /*#__PURE__*/React.createElement("span", {
|
||||||
className: "text-[7px] opacity-60"
|
className: "text-[7px] opacity-60"
|
||||||
}, showMidiEvents ? (panelPositions.midi_events || 'bottom')[0].toUpperCase() : '')), /*#__PURE__*/React.createElement("span", {
|
}, showMidiEvents ? (panelPositions.midi_events || 'bottom')[0].toUpperCase() : '')), /*#__PURE__*/React.createElement("button", {
|
||||||
|
onClick: () => setShowMixer(p => !p),
|
||||||
|
className: `px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showMixer ? 'bg-indigo-900 text-indigo-300' : 'text-zinc-500 hover:text-zinc-300'}`,
|
||||||
|
title: "Mixer Panel (F7)"
|
||||||
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
|
className: "inline-flex items-center shrink-0"
|
||||||
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
|
"data-lucide": "sliders-horizontal",
|
||||||
|
className: "w-3 h-3"
|
||||||
|
}))), /*#__PURE__*/React.createElement("span", {
|
||||||
className: "w-[1px] h-3 bg-zinc-800 mx-1"
|
className: "w-[1px] h-3 bg-zinc-800 mx-1"
|
||||||
}), /*#__PURE__*/React.createElement("span", {
|
}), /*#__PURE__*/React.createElement("span", {
|
||||||
className: "flex items-center gap-1"
|
className: "flex items-center gap-1"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -670,3 +670,15 @@
|
|||||||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||||||
- **Ghi chú/Test (nếu có):** Build có lỗi parse pre-existing (Babel 8) không liên quan đến thay đổi này.
|
- **Ghi chú/Test (nếu có):** Build có lỗi parse pre-existing (Babel 8) không liên quan đến thay đổi này.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### [2026-07-29 09:40] Task: Reset to 33cf804 — clean slate for mixer reimplementation
|
||||||
|
- **Tóm tắt thay đổi:** Hard reset về commit 33cf804, xóa toàn bộ mixer code (MasterChannelStrip, TrackChannelStrip, MixerConsole, AudioMixerGraphManager, setOutputNode) và MIDI keyboard note sustain. Giữ lại TimelineRuler (tempo bar/timebar/lead-in/0-index). Backup mixer panel code lưu trong `mixer_panel_backup_*.patch`.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/services/audioMixerGraphManager.js`
|
||||||
|
- **Ghi chú/Test (nếu có):** `npm run build` — build passes. Sẵn sàng cài đặt lại mixer panel display-only.
|
||||||
|
---
|
||||||
|
|
||||||
|
### [2026-07-29 09:46] Task: Install Mixer Panel display-only with F7 toggle
|
||||||
|
- **Tóm tắt thay đổi:** Thêm MixerStrip component (display-only, không audio routing). Mixer panel bottom dock với drag handle resize (80-400px). Track strip hiển thị index, M/S buttons, fader slider, dB readout, track name. Master strip với "MAIN OUT" label. F7 toggle + status bar button + View menu item. onUpdateTrack thay đổi mute/solo/volumeDb trực tiếp.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||||||
|
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||||||
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user