fix: wire all settings to backend — master_volume/routing use dedicated endpoints, all others persist via POST /api/settings to config.yaml
This commit is contained in:
@@ -36,6 +36,7 @@ const API = {
|
|||||||
getAudioProfile:() => api("GET", "/api/audio/profile"),
|
getAudioProfile:() => api("GET", "/api/audio/profile"),
|
||||||
getRouting: () => api("GET", "/api/routing"),
|
getRouting: () => api("GET", "/api/routing"),
|
||||||
setRouting: (mode, breakpoint) => api("POST", "/api/routing", { routing_mode: mode, routing_breakpoint: breakpoint }),
|
setRouting: (mode, breakpoint) => api("POST", "/api/routing", { routing_mode: mode, routing_breakpoint: breakpoint }),
|
||||||
|
saveSettings: (settings) => api("POST", "/api/settings", settings),
|
||||||
searchModels: (q) => api("GET", `/api/tonehub/search?q=${encodeURIComponent(q)}`),
|
searchModels: (q) => api("GET", `/api/tonehub/search?q=${encodeURIComponent(q)}`),
|
||||||
searchIrs: (q) => api("GET", `/api/tonehub/search/irs?q=${encodeURIComponent(q)}`),
|
searchIrs: (q) => api("GET", `/api/tonehub/search/irs?q=${encodeURIComponent(q)}`),
|
||||||
installModel: (url, name) => api("POST", "/api/models/tonedownload/install", { download_url: url, name }),
|
installModel: (url, name) => api("POST", "/api/models/tonedownload/install", { download_url: url, name }),
|
||||||
@@ -1015,6 +1016,7 @@ export default function App(){
|
|||||||
onMasterVolume={(v)=>API.masterVolume(v).catch(()=>{})}
|
onMasterVolume={(v)=>API.masterVolume(v).catch(()=>{})}
|
||||||
onAudioProfile={(rate, period)=>API.setAudioProfile(rate, period).catch(()=>{})}
|
onAudioProfile={(rate, period)=>API.setAudioProfile(rate, period).catch(()=>{})}
|
||||||
onRoutingChange={(mode, bp)=>API.setRouting(mode, bp).catch(()=>{})}
|
onRoutingChange={(mode, bp)=>API.setRouting(mode, bp).catch(()=>{})}
|
||||||
|
onSaveSettings={(s)=>API.saveSettings(s).catch(()=>{})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+67
-198
@@ -6,7 +6,6 @@ const T = {
|
|||||||
green: "#3AB87A", red: "#C84040", textPrimary: "#F0EDE6", textSec: "#8888A0", textDim: "#444458",
|
green: "#3AB87A", red: "#C84040", textPrimary: "#F0EDE6", textSec: "#8888A0", textDim: "#444458",
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── API calls (injected via props or fetched internally) ──
|
|
||||||
async function api(method, path, body) {
|
async function api(method, path, body) {
|
||||||
const opts = { method, headers: { "Content-Type": "application/json" } };
|
const opts = { method, headers: { "Content-Type": "application/json" } };
|
||||||
if (body) opts.body = JSON.stringify(body);
|
if (body) opts.body = JSON.stringify(body);
|
||||||
@@ -15,10 +14,10 @@ async function api(method, path, body) {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Settings definitions ──
|
|
||||||
const SETTINGS = {
|
const SETTINGS = {
|
||||||
"Audio": [
|
"Audio": [
|
||||||
{ key: "master_volume", label: "Master Volume", type: "slider", min: 0, max: 100, def: 72, unit: "%", icon: "🔊" },
|
{ key: "master_volume", label: "Master Volume", type: "slider", min: 0, max: 100, def: 72, unit: "%", icon: "🔊",
|
||||||
|
via: "volume" },
|
||||||
{ key: "sample_rate", label: "Sample Rate", type: "select", def: "48000",
|
{ key: "sample_rate", label: "Sample Rate", type: "select", def: "48000",
|
||||||
options: [
|
options: [
|
||||||
{ value: "22050", label: "22.05 kHz" },
|
{ value: "22050", label: "22.05 kHz" },
|
||||||
@@ -26,7 +25,7 @@ const SETTINGS = {
|
|||||||
{ value: "48000", label: "48 kHz" },
|
{ value: "48000", label: "48 kHz" },
|
||||||
{ value: "88200", label: "88.2 kHz" },
|
{ value: "88200", label: "88.2 kHz" },
|
||||||
{ value: "96000", label: "96 kHz" },
|
{ value: "96000", label: "96 kHz" },
|
||||||
], icon: "📊" },
|
], icon: "📊", via: "audio_profile" },
|
||||||
{ key: "buffer_size", label: "Buffer Size", type: "select", def: "512",
|
{ key: "buffer_size", label: "Buffer Size", type: "select", def: "512",
|
||||||
options: [
|
options: [
|
||||||
{ value: "64", label: "64 (1.3ms)" },
|
{ value: "64", label: "64 (1.3ms)" },
|
||||||
@@ -34,14 +33,14 @@ const SETTINGS = {
|
|||||||
{ value: "256", label: "256 (5.3ms)" },
|
{ value: "256", label: "256 (5.3ms)" },
|
||||||
{ value: "512", label: "512 (10.6ms)" },
|
{ value: "512", label: "512 (10.6ms)" },
|
||||||
{ value: "1024",label: "1024 (21.3ms)" },
|
{ value: "1024",label: "1024 (21.3ms)" },
|
||||||
], icon: "📦" },
|
], icon: "📦", via: "audio_profile" },
|
||||||
{ key: "input_pad", label: "Input Pad", type: "select", def: "off",
|
{ key: "input_pad", label: "Input Pad", type: "select", def: "off",
|
||||||
options: [
|
options: [
|
||||||
{ value: "off", label: "Off (0 dB)" },
|
{ value: "off", label: "Off (0 dB)" },
|
||||||
{ value: "-6", label: "-6 dB" },
|
{ value: "-6", label: "-6 dB" },
|
||||||
{ value: "-12", label: "-12 dB" },
|
{ value: "-12", label: "-12 dB" },
|
||||||
{ value: "-18", label: "-18 dB" },
|
{ value: "-18", label: "-18 dB" },
|
||||||
], icon: "📥" },
|
], icon: "📥", via: "settings" },
|
||||||
{ key: "input_impedance", label: "Input Impedance", type: "select", def: "1m",
|
{ key: "input_impedance", label: "Input Impedance", type: "select", def: "1m",
|
||||||
options: [
|
options: [
|
||||||
{ value: "1m", label: "1 MΩ" },
|
{ value: "1m", label: "1 MΩ" },
|
||||||
@@ -49,14 +48,14 @@ const SETTINGS = {
|
|||||||
{ value: "250k", label: "250 kΩ" },
|
{ value: "250k", label: "250 kΩ" },
|
||||||
{ value: "150k", label: "150 kΩ" },
|
{ value: "150k", label: "150 kΩ" },
|
||||||
{ value: "68k", label: "68 kΩ" },
|
{ value: "68k", label: "68 kΩ" },
|
||||||
], icon: "⚡" },
|
], icon: "⚡", via: "settings" },
|
||||||
],
|
],
|
||||||
"Routing": [
|
"Routing": [
|
||||||
{ key: "routing_mode", label: "Routing Mode", type: "select", def: "mono",
|
{ key: "routing_mode", label: "Routing Mode", type: "select", def: "mono",
|
||||||
options: [
|
options: [
|
||||||
{ value: "mono", label: "Mono" },
|
{ value: "mono", label: "Mono" },
|
||||||
{ value: "4cm", label: "4CM (Stereo FX Loop)" },
|
{ value: "4cm", label: "4CM (Stereo FX Loop)" },
|
||||||
], icon: "🔀" },
|
], icon: "🔀", via: "routing" },
|
||||||
{ key: "routing_breakpoint", label: "FX Loop Breakpoint", type: "select", def: "7",
|
{ key: "routing_breakpoint", label: "FX Loop Breakpoint", type: "select", def: "7",
|
||||||
options: [
|
options: [
|
||||||
{ value: "1", label: "After block 1" },
|
{ value: "1", label: "After block 1" },
|
||||||
@@ -67,7 +66,7 @@ const SETTINGS = {
|
|||||||
{ value: "6", label: "After block 6" },
|
{ value: "6", label: "After block 6" },
|
||||||
{ value: "7", label: "After block 7" },
|
{ value: "7", label: "After block 7" },
|
||||||
{ value: "8", label: "After block 8" },
|
{ value: "8", label: "After block 8" },
|
||||||
], icon: "↔" },
|
], icon: "↔", via: "routing" },
|
||||||
],
|
],
|
||||||
"Audio Interface": [
|
"Audio Interface": [
|
||||||
{ key: "input_device", label: "Input Device", type: "select", def: "hw:0,0",
|
{ key: "input_device", label: "Input Device", type: "select", def: "hw:0,0",
|
||||||
@@ -75,19 +74,19 @@ const SETTINGS = {
|
|||||||
{ value: "hw:0,0", label: "Default (hw:0,0)" },
|
{ value: "hw:0,0", label: "Default (hw:0,0)" },
|
||||||
{ value: "hw:1,0", label: "USB Audio (hw:1,0)" },
|
{ value: "hw:1,0", label: "USB Audio (hw:1,0)" },
|
||||||
{ value: "hw:2,0", label: "HDMI Audio (hw:2,0)" },
|
{ value: "hw:2,0", label: "HDMI Audio (hw:2,0)" },
|
||||||
], icon: "🎤" },
|
], icon: "🎤", via: "settings" },
|
||||||
{ key: "output_device", label: "Output Device", type: "select", def: "hw:0,0",
|
{ key: "output_device", label: "Output Device", type: "select", def: "hw:0,0",
|
||||||
options: [
|
options: [
|
||||||
{ value: "hw:0,0", label: "Default (hw:0,0)" },
|
{ value: "hw:0,0", label: "Default (hw:0,0)" },
|
||||||
{ value: "hw:1,0", label: "USB Audio (hw:1,0)" },
|
{ value: "hw:1,0", label: "USB Audio (hw:1,0)" },
|
||||||
{ value: "hw:2,0", label: "HDMI Audio (hw:2,0)" },
|
{ value: "hw:2,0", label: "HDMI Audio (hw:2,0)" },
|
||||||
], icon: "🔈" },
|
], icon: "🔈", via: "settings" },
|
||||||
{ key: "channel_mode", label: "Channel Mode", type: "select", def: "mono",
|
{ key: "channel_mode", label: "Channel Mode", type: "select", def: "mono",
|
||||||
options: [
|
options: [
|
||||||
{ value: "mono", label: "Mono" },
|
{ value: "mono", label: "Mono" },
|
||||||
{ value: "dual-mono", label: "Dual Mono" },
|
{ value: "dual-mono", label: "Dual Mono" },
|
||||||
{ value: "stereo_4cm", label: "Stereo (4CM)" },
|
{ value: "stereo_4cm", label: "Stereo (4CM)" },
|
||||||
], icon: "🔊" },
|
], icon: "🔊", via: "settings" },
|
||||||
],
|
],
|
||||||
"MIDI": [
|
"MIDI": [
|
||||||
{ key: "midi_channel", label: "MIDI Channel", type: "select", def: "omni",
|
{ key: "midi_channel", label: "MIDI Channel", type: "select", def: "omni",
|
||||||
@@ -96,13 +95,13 @@ const SETTINGS = {
|
|||||||
...Array.from({ length: 16 }, (_, i) => ({
|
...Array.from({ length: 16 }, (_, i) => ({
|
||||||
value: String(i + 1), label: `Channel ${i + 1}`
|
value: String(i + 1), label: `Channel ${i + 1}`
|
||||||
})),
|
})),
|
||||||
], icon: "🎹" },
|
], icon: "🎹", via: "settings" },
|
||||||
{ key: "midi_clock", label: "MIDI Clock", type: "toggle", def: false, icon: "⏱" },
|
{ key: "midi_clock", label: "MIDI Clock", type: "toggle", def: false, via: "settings", icon: "⏱" },
|
||||||
{ key: "midi_thru", label: "MIDI Thru", type: "toggle", def: true, icon: "↔" },
|
{ key: "midi_thru", label: "MIDI Thru", type: "toggle", def: true, via: "settings", icon: "↔" },
|
||||||
],
|
],
|
||||||
"Display": [
|
"Display": [
|
||||||
{ key: "brightness", label: "Display Brightness", type: "slider", min: 1, max: 10, def: 8, unit: "", icon: "☀️" },
|
{ key: "brightness", label: "Display Brightness", type: "slider", min: 1, max: 10, def: 8, unit: "", icon: "☀️", via: "settings" },
|
||||||
{ key: "auto_dim", label: "Auto Dim", type: "toggle", def: true, icon: "🌙" },
|
{ key: "auto_dim", label: "Auto Dim", type: "toggle", def: true, via: "settings", icon: "🌙" },
|
||||||
{ key: "screen_saver", label: "Screen Saver", type: "select", def: "5m",
|
{ key: "screen_saver", label: "Screen Saver", type: "select", def: "5m",
|
||||||
options: [
|
options: [
|
||||||
{ value: "off", label: "Off" },
|
{ value: "off", label: "Off" },
|
||||||
@@ -110,10 +109,10 @@ const SETTINGS = {
|
|||||||
{ value: "5m", label: "5 min" },
|
{ value: "5m", label: "5 min" },
|
||||||
{ value: "15m", label: "15 min" },
|
{ value: "15m", label: "15 min" },
|
||||||
{ value: "30m", label: "30 min" },
|
{ value: "30m", label: "30 min" },
|
||||||
], icon: "💤" },
|
], icon: "💤", via: "settings" },
|
||||||
],
|
],
|
||||||
"Tempo": [
|
"Tempo": [
|
||||||
{ key: "tap_tempo", label: "Tap Tempo", type: "bpm", def: 120, icon: "🔄" },
|
{ key: "tap_tempo", label: "Tap Tempo", type: "bpm", def: 120, via: "settings", icon: "🔄" },
|
||||||
{ key: "tempo_division", label: "Tempo Division", type: "select", def: "quarter",
|
{ key: "tempo_division", label: "Tempo Division", type: "select", def: "quarter",
|
||||||
options: [
|
options: [
|
||||||
{ value: "half", label: "½ Note" },
|
{ value: "half", label: "½ Note" },
|
||||||
@@ -122,12 +121,11 @@ const SETTINGS = {
|
|||||||
{ value: "triplet", label: "⅛ Triplet" },
|
{ value: "triplet", label: "⅛ Triplet" },
|
||||||
{ value: "sixteenth", label: "¹⁄₁₆ Note" },
|
{ value: "sixteenth", label: "¹⁄₁₆ Note" },
|
||||||
{ value: "dotted_eighth", label: "Dotted ⅛" },
|
{ value: "dotted_eighth", label: "Dotted ⅛" },
|
||||||
], icon: "🎶" },
|
], icon: "🎶", via: "settings" },
|
||||||
{ key: "tap_tempo_mute", label: "Mute on Tap", type: "toggle", def: false, icon: "🔇" },
|
{ key: "tap_tempo_mute", label: "Mute on Tap", type: "toggle", def: false, via: "settings", icon: "🔇" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Build defaults from SETTINGS definitions ──
|
|
||||||
function buildDefaults(overrides = {}) {
|
function buildDefaults(overrides = {}) {
|
||||||
const init = {};
|
const init = {};
|
||||||
for (const group of Object.values(SETTINGS)) {
|
for (const group of Object.values(SETTINGS)) {
|
||||||
@@ -138,7 +136,8 @@ function buildDefaults(overrides = {}) {
|
|||||||
return { ...init, ...overrides };
|
return { ...init, ...overrides };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Slider Widget ──
|
// ── Widgets ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function SettingsSlider({ label, value, min, max, unit, onChange }) {
|
function SettingsSlider({ label, value, min, max, unit, onChange }) {
|
||||||
const trackRef = useRef(null);
|
const trackRef = useRef(null);
|
||||||
const dragRef = useRef(false);
|
const dragRef = useRef(false);
|
||||||
@@ -196,45 +195,22 @@ function SettingsSlider({ label, value, min, max, unit, onChange }) {
|
|||||||
position: "relative", height: 32, display: "flex", alignItems: "center",
|
position: "relative", height: 32, display: "flex", alignItems: "center",
|
||||||
cursor: "pointer", touchAction: "none",
|
cursor: "pointer", touchAction: "none",
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{ width: "100%", height: 6, background: T.border, borderRadius: 3, position: "relative", overflow: "hidden" }}>
|
||||||
width: "100%", height: 6, background: T.border, borderRadius: 3,
|
<div style={{ height: "100%", borderRadius: 3, position: "absolute", top: 0, left: 0, width: `${pct}%`, background: T.amber, boxShadow: `0 0 6px ${T.amber}44`, transition: "width .03s" }} />
|
||||||
position: "relative", overflow: "hidden",
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
height: "100%", borderRadius: 3, position: "absolute", top: 0, left: 0,
|
|
||||||
width: `${pct}%`, background: T.amber,
|
|
||||||
boxShadow: `0 0 6px ${T.amber}44`,
|
|
||||||
transition: "width .03s",
|
|
||||||
}} />
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{ width: 24, height: 24, borderRadius: "50%", position: "absolute", top: "50%", left: `${pct}%`, transform: "translate(-50%,-50%)", background: T.amber, border: `3px solid ${T.bg}`, boxShadow: `0 1px 6px rgba(0,0,0,.5)`, pointerEvents: "none", transition: "left .03s" }} />
|
||||||
width: 24, height: 24, borderRadius: "50%", position: "absolute",
|
|
||||||
top: "50%", left: `${pct}%`,
|
|
||||||
transform: "translate(-50%,-50%)",
|
|
||||||
background: T.amber, border: `3px solid ${T.bg}`,
|
|
||||||
boxShadow: `0 1px 6px rgba(0,0,0,.5)`,
|
|
||||||
pointerEvents: "none",
|
|
||||||
transition: "left .03s",
|
|
||||||
}} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Select Widget ──
|
function SettingsSelect({ label, value, options, onChange }) {
|
||||||
function SettingsSelect({ label, value, options, onChange, icon }) {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 6 }}>
|
||||||
display: "flex", justifyContent: "space-between", alignItems: "center",
|
|
||||||
marginBottom: 6,
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: 11, fontWeight: 500, color: T.textSec }}>{label}</span>
|
<span style={{ fontSize: 11, fontWeight: 500, color: T.textSec }}>{label}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{ display: "flex", gap: 4, flexWrap: "wrap", background: T.panel, borderRadius: 6, padding: 4 }}>
|
||||||
display: "flex", gap: 4, flexWrap: "wrap",
|
|
||||||
background: T.panel, borderRadius: 6, padding: 4,
|
|
||||||
}}>
|
|
||||||
{options.map(opt => (
|
{options.map(opt => (
|
||||||
<button key={opt.value}
|
<button key={opt.value}
|
||||||
onClick={() => onChange?.(opt.value)}
|
onClick={() => onChange?.(opt.value)}
|
||||||
@@ -244,25 +220,18 @@ function SettingsSelect({ label, value, options, onChange, icon }) {
|
|||||||
background: value === opt.value ? T.amber : "transparent",
|
background: value === opt.value ? T.amber : "transparent",
|
||||||
color: value === opt.value ? "#000" : T.textSec,
|
color: value === opt.value ? "#000" : T.textSec,
|
||||||
transition: "all .1s", letterSpacing: ".03em",
|
transition: "all .1s", letterSpacing: ".03em",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap", minWidth: 0,
|
||||||
minWidth: 0,
|
|
||||||
}}
|
}}
|
||||||
>
|
>{opt.label}</button>
|
||||||
{opt.label}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Toggle Widget ──
|
|
||||||
function SettingsToggle({ label, value, onChange }) {
|
function SettingsToggle({ label, value, onChange }) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "4px 0" }}>
|
||||||
display: "flex", justifyContent: "space-between", alignItems: "center",
|
|
||||||
padding: "4px 0",
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: 11, fontWeight: 500, color: T.textSec }}>{label}</span>
|
<span style={{ fontSize: 11, fontWeight: 500, color: T.textSec }}>{label}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => onChange?.(!value)}
|
onClick={() => onChange?.(!value)}
|
||||||
@@ -285,7 +254,6 @@ function SettingsToggle({ label, value, onChange }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── BPM/Tap Tempo Widget ──
|
|
||||||
function BpmWidget({ label, value, onChange }) {
|
function BpmWidget({ label, value, onChange }) {
|
||||||
const tapTimes = useRef([]);
|
const tapTimes = useRef([]);
|
||||||
|
|
||||||
@@ -305,55 +273,27 @@ function BpmWidget({ label, value, onChange }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 6 }}>
|
||||||
display: "flex", justifyContent: "space-between", alignItems: "center",
|
|
||||||
marginBottom: 6,
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: 11, fontWeight: 500, color: T.textSec }}>{label}</span>
|
<span style={{ fontSize: 11, fontWeight: 500, color: T.textSec }}>{label}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||||
display: "flex", gap: 8, alignItems: "center",
|
<button onClick={handleTap} style={{ padding: "8px 20px", borderRadius: 8, border: `1px solid ${T.amber}66`, background: `${T.amber}15`, color: T.amber, fontSize: 12, fontWeight: 700, cursor: "pointer", letterSpacing: ".05em" }}>TAP</button>
|
||||||
}}>
|
<div style={{ flex: 1, display: "flex", alignItems: "center", gap: 4, background: T.surface, borderRadius: 6, border: `1px solid ${T.border}`, padding: "6px 10px" }}>
|
||||||
<button onClick={handleTap}
|
<button onClick={() => onChange?.(Math.max(20, value - 1))} style={{ background: "none", border: "none", color: T.textDim, fontSize: 14, cursor: "pointer", padding: "0 4px" }}>−</button>
|
||||||
style={{
|
<span style={{ flex: 1, textAlign: "center", fontFamily: "'JetBrains Mono', monospace", fontSize: 16, fontWeight: 700, color: T.amber }}>{value} BPM</span>
|
||||||
padding: "8px 20px", borderRadius: 8, border: `1px solid ${T.amber}66`,
|
<button onClick={() => onChange?.(Math.min(300, value + 1))} style={{ background: "none", border: "none", color: T.textDim, fontSize: 14, cursor: "pointer", padding: "0 4px" }}>+</button>
|
||||||
background: `${T.amber}15`, color: T.amber,
|
|
||||||
fontSize: 12, fontWeight: 700, cursor: "pointer",
|
|
||||||
letterSpacing: ".05em",
|
|
||||||
}}>
|
|
||||||
TAP
|
|
||||||
</button>
|
|
||||||
<div style={{
|
|
||||||
flex: 1, display: "flex", alignItems: "center", gap: 4,
|
|
||||||
background: T.surface, borderRadius: 6, border: `1px solid ${T.border}`,
|
|
||||||
padding: "6px 10px",
|
|
||||||
}}>
|
|
||||||
<button onClick={() => onChange?.(Math.max(20, value - 1))}
|
|
||||||
style={{
|
|
||||||
background: "none", border: "none", color: T.textDim,
|
|
||||||
fontSize: 14, cursor: "pointer", padding: "0 4px",
|
|
||||||
}}>−</button>
|
|
||||||
<span style={{
|
|
||||||
flex: 1, textAlign: "center",
|
|
||||||
fontFamily: "'JetBrains Mono', monospace", fontSize: 16, fontWeight: 700, color: T.amber,
|
|
||||||
}}>{value} BPM</span>
|
|
||||||
<button onClick={() => onChange?.(Math.min(300, value + 1))}
|
|
||||||
style={{
|
|
||||||
background: "none", border: "none", color: T.textDim,
|
|
||||||
fontSize: 14, cursor: "pointer", padding: "0 4px",
|
|
||||||
}}>+</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Global Settings ──
|
// ── Global Settings ─────────────────────────────────────────────────────────
|
||||||
export default function GlobalSettings({ onClose, onMasterVolume, onAudioProfile, onRoutingChange }) {
|
|
||||||
|
export default function GlobalSettings({ onClose, onMasterVolume, onAudioProfile, onRoutingChange, onSaveSettings }) {
|
||||||
const [values, setValues] = useState(buildDefaults());
|
const [values, setValues] = useState(buildDefaults());
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
// ── Load current settings from API on mount ──
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
@@ -363,18 +303,15 @@ export default function GlobalSettings({ onClose, onMasterVolume, onAudioProfile
|
|||||||
]).then(([state, audioProfile, routing]) => {
|
]).then(([state, audioProfile, routing]) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const overrides = {};
|
const overrides = {};
|
||||||
// From /api/state
|
|
||||||
if (state) {
|
if (state) {
|
||||||
if (state.master_volume != null) overrides.master_volume = Math.round(state.master_volume * 100);
|
if (state.master_volume != null) overrides.master_volume = Math.round(state.master_volume * 100);
|
||||||
if (state.sample_rate != null) overrides.sample_rate = String(state.sample_rate);
|
if (state.sample_rate != null) overrides.sample_rate = String(state.sample_rate);
|
||||||
if (state.channel_mode) overrides.channel_mode = state.channel_mode;
|
if (state.channel_mode) overrides.channel_mode = state.channel_mode;
|
||||||
}
|
}
|
||||||
// From /api/audio/profile (more accurate for rate/period)
|
|
||||||
if (audioProfile) {
|
if (audioProfile) {
|
||||||
if (audioProfile.rate != null) overrides.sample_rate = String(audioProfile.rate);
|
if (audioProfile.rate != null) overrides.sample_rate = String(audioProfile.rate);
|
||||||
if (audioProfile.period != null) overrides.buffer_size = String(audioProfile.period);
|
if (audioProfile.period != null) overrides.buffer_size = String(audioProfile.period);
|
||||||
}
|
}
|
||||||
// From /api/routing
|
|
||||||
if (routing) {
|
if (routing) {
|
||||||
if (routing.routing_mode) overrides.routing_mode = routing.routing_mode;
|
if (routing.routing_mode) overrides.routing_mode = routing.routing_mode;
|
||||||
if (routing.routing_breakpoint != null) overrides.routing_breakpoint = String(routing.routing_breakpoint);
|
if (routing.routing_breakpoint != null) overrides.routing_breakpoint = String(routing.routing_breakpoint);
|
||||||
@@ -391,123 +328,55 @@ export default function GlobalSettings({ onClose, onMasterVolume, onAudioProfile
|
|||||||
setValues(prev => {
|
setValues(prev => {
|
||||||
const next = { ...prev, [key]: val };
|
const next = { ...prev, [key]: val };
|
||||||
|
|
||||||
// Fire external callbacks
|
// Route via dedicated endpoints
|
||||||
if (key === "master_volume") {
|
if (key === "master_volume") onMasterVolume?.(val);
|
||||||
onMasterVolume?.(val);
|
|
||||||
}
|
else if (key === "sample_rate" || key === "buffer_size") {
|
||||||
if (key === "sample_rate" || key === "buffer_size") {
|
|
||||||
const rate = key === "sample_rate" ? val : (next.sample_rate || "48000");
|
const rate = key === "sample_rate" ? val : (next.sample_rate || "48000");
|
||||||
const period = key === "buffer_size" ? val : (next.buffer_size || "512");
|
const period = key === "buffer_size" ? val : (next.buffer_size || "512");
|
||||||
onAudioProfile?.(parseInt(rate), parseInt(period));
|
onAudioProfile?.(parseInt(rate), parseInt(period));
|
||||||
}
|
}
|
||||||
if (key === "routing_mode" || key === "routing_breakpoint") {
|
|
||||||
|
else if (key === "routing_mode" || key === "routing_breakpoint") {
|
||||||
const mode = key === "routing_mode" ? val : (next.routing_mode || "mono");
|
const mode = key === "routing_mode" ? val : (next.routing_mode || "mono");
|
||||||
const bp = key === "routing_breakpoint" ? val : (next.routing_breakpoint || "7");
|
const bp = key === "routing_breakpoint" ? val : (next.routing_breakpoint || "7");
|
||||||
onRoutingChange?.(mode, parseInt(bp));
|
onRoutingChange?.(mode, parseInt(bp));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
// All other settings → POST /api/settings (generic persistence)
|
||||||
|
onSaveSettings?.({ [key]: val });
|
||||||
|
}
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, [onMasterVolume, onAudioProfile, onRoutingChange]);
|
}, [onMasterVolume, onAudioProfile, onRoutingChange, onSaveSettings]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{ position: "fixed", inset: 0, zIndex: 998, display: "flex", flexDirection: "column", background: T.bg, color: T.textPrimary, fontFamily: "'Inter', sans-serif" }}>
|
||||||
position: "fixed", inset: 0, zIndex: 998,
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "10px 14px", borderBottom: `1px solid ${T.border}`, background: T.panel, flexShrink: 0 }}>
|
||||||
display: "flex", flexDirection: "column",
|
|
||||||
background: T.bg, color: T.textPrimary,
|
|
||||||
fontFamily: "'Inter', sans-serif",
|
|
||||||
}}>
|
|
||||||
{/* Header */}
|
|
||||||
<div style={{
|
|
||||||
display: "flex", alignItems: "center", justifyContent: "space-between",
|
|
||||||
padding: "10px 14px",
|
|
||||||
borderBottom: `1px solid ${T.border}`,
|
|
||||||
background: T.panel, flexShrink: 0,
|
|
||||||
}}>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||||
<button onClick={onClose}
|
<button onClick={onClose} style={{ width: 34, height: 34, borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", background: T.surface, border: `1px solid ${T.border}`, color: T.textPrimary, fontSize: 16, cursor: "pointer" }}>✕</button>
|
||||||
style={{
|
|
||||||
width: 34, height: 34, borderRadius: 8,
|
|
||||||
display: "flex", alignItems: "center", justifyContent: "center",
|
|
||||||
background: T.surface, border: `1px solid ${T.border}`,
|
|
||||||
color: T.textPrimary, fontSize: 16, cursor: "pointer",
|
|
||||||
}}>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 15, fontWeight: 600 }}>Global Settings</div>
|
<div style={{ fontSize: 15, fontWeight: 600 }}>Settings</div>
|
||||||
{!loaded && <span style={{ fontSize: 10, color: T.textDim }}>Loading current values…</span>}
|
{!loaded && <span style={{ fontSize: 10, color: T.textDim }}>Loading…</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Body */}
|
<div style={{ flex: 1, overflowY: "auto", overflowX: "hidden", padding: "6px 14px 20px" }}>
|
||||||
<div style={{
|
|
||||||
flex: 1, overflowY: "auto", overflowX: "hidden",
|
|
||||||
padding: "6px 14px 20px",
|
|
||||||
}}>
|
|
||||||
{groups.map(([groupName, settings]) => (
|
{groups.map(([groupName, settings]) => (
|
||||||
<div key={groupName}>
|
<div key={groupName}>
|
||||||
{/* Group header */}
|
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: ".1em", textTransform: "uppercase", color: T.textDim, padding: "14px 0 8px" }}>{groupName}</div>
|
||||||
<div style={{
|
<div style={{ display: "flex", flexDirection: "column", gap: 12, background: T.surface, borderRadius: 8, border: `1px solid ${T.border}`, padding: "12px 14px" }}>
|
||||||
fontSize: 10, fontWeight: 700, letterSpacing: ".1em",
|
|
||||||
textTransform: "uppercase", color: T.textDim,
|
|
||||||
padding: "14px 0 8px",
|
|
||||||
}}>
|
|
||||||
{groupName}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Settings */}
|
|
||||||
<div style={{
|
|
||||||
display: "flex", flexDirection: "column", gap: 12,
|
|
||||||
background: T.surface, borderRadius: 8,
|
|
||||||
border: `1px solid ${T.border}`,
|
|
||||||
padding: "12px 14px",
|
|
||||||
}}>
|
|
||||||
{settings.map(s => (
|
{settings.map(s => (
|
||||||
<div key={s.key} style={{
|
<div key={s.key} style={{ display: "flex", alignItems: "flex-start", gap: 10 }}>
|
||||||
display: "flex", alignItems: "flex-start", gap: 10,
|
<div style={{ width: 28, height: 28, borderRadius: 6, flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center", background: T.panel, fontSize: 13 }}>{s.icon}</div>
|
||||||
}}>
|
|
||||||
{/* Icon */}
|
|
||||||
<div style={{
|
|
||||||
width: 28, height: 28, borderRadius: 6, flexShrink: 0,
|
|
||||||
display: "flex", alignItems: "center", justifyContent: "center",
|
|
||||||
background: T.panel, fontSize: 13,
|
|
||||||
}}>
|
|
||||||
{s.icon}
|
|
||||||
</div>
|
|
||||||
{/* Control */}
|
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
{s.type === "slider" && (
|
{s.type === "slider" && <SettingsSlider label={s.label} value={values[s.key] ?? s.def} min={s.min} max={s.max} unit={s.unit} onChange={v => update(s.key, v)} />}
|
||||||
<SettingsSlider
|
{s.type === "select" && <SettingsSelect label={s.label} value={values[s.key] ?? s.def} options={s.options} onChange={v => update(s.key, v)} />}
|
||||||
label={s.label}
|
{s.type === "toggle" && <SettingsToggle label={s.label} value={values[s.key] ?? s.def} onChange={v => update(s.key, v)} />}
|
||||||
value={values[s.key] ?? s.def}
|
{s.type === "bpm" && <BpmWidget label={s.label} value={values[s.key] ?? s.def} onChange={v => update(s.key, v)} />}
|
||||||
min={s.min} max={s.max} unit={s.unit}
|
|
||||||
onChange={v => update(s.key, v)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{s.type === "select" && (
|
|
||||||
<SettingsSelect
|
|
||||||
label={s.label}
|
|
||||||
value={values[s.key] ?? s.def}
|
|
||||||
options={s.options}
|
|
||||||
onChange={v => update(s.key, v)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{s.type === "toggle" && (
|
|
||||||
<SettingsToggle
|
|
||||||
label={s.label}
|
|
||||||
value={values[s.key] ?? s.def}
|
|
||||||
onChange={v => update(s.key, v)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{s.type === "bpm" && (
|
|
||||||
<BpmWidget
|
|
||||||
label={s.label}
|
|
||||||
value={values[s.key] ?? s.def}
|
|
||||||
onChange={v => update(s.key, v)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user