feat: control panel with sidetone, EQ, and headset controls

This commit is contained in:
loki committed 2026-08-31 12:33:16 +02:00
1 parent 55329ec190
commit eb20480321
1 file changed
+199
+199
View File
@@ -0,0 +1,199 @@
local hsc = require("./lib/hsc.luau")
local state = {
open = false,
device = nil,
battery = { status = "BATTERY_UNAVAILABLE", level = -1 },
chatmix = 0,
caps = {},
equalizerPresets = 0,
sidetone = nil,
}
local INACTIVE_VALUES = { 0, 1, 2, 5, 10, 15, 20, 30, 45, 60, 90 }
local function syncState()
local v = noctalia.state.get("device")
if v then state.device = v end
v = noctalia.state.get("battery")
if v then state.battery = v end
v = noctalia.state.get("chatmix")
if v ~= nil then state.chatmix = v end
v = noctalia.state.get("caps")
if v then state.caps = v end
v = noctalia.state.get("equalizerPresets")
if v then state.equalizerPresets = v end
v = noctalia.state.get("sidetone")
if v ~= nil then state.sidetone = v end
end
local function send(flag, value, onOk)
hsc.sendCommand({ flag, value }, function(res)
if res.exitCode ~= 0 then
noctalia.notifyError(noctalia.tr("error.command_failed"), string.sub(res.stderr or "", 1, 200))
return
end
if onOk then onOk() end
render()
end)
end
function onSidetone(value) -- live drag update
local v = tonumber(value)
if v then state.sidetone = v end
render()
end
function onSidetoneEnd(value)
local v = tonumber(value)
if v then
send("-s", tostring(v), function()
noctalia.state.set("sidetone", v)
end)
end
end
function onSidetoneOff()
send("-s", "0", function()
state.sidetone = 0
noctalia.state.set("sidetone", 0)
end)
end
function onEqPreset(idx)
local i = tonumber(idx) or 0
send("-p", tostring(i))
end
function onInactive(idx)
local i = tonumber(idx) or 0
local value = INACTIVE_VALUES[i + 1]
if value ~= nil then
send("-i", tostring(value))
end
end
local function noiseButtons()
local label = noctalia.tr("panel.noise_filter") .. ":"
return ui.row({ gap = 8, align = "center" }, {
ui.label({ text = label, width = 120 }),
ui.button({ text = noctalia.tr("panel.off"), flexGrow = 1, onClick = function() send("--noise-filter", "0") end }),
ui.button({ text = noctalia.tr("panel.low"), flexGrow = 1, onClick = function() send("--noise-filter", "1") end }),
ui.button({ text = noctalia.tr("panel.high"), flexGrow = 1, onClick = function() send("--noise-filter", "2") end }),
})
end
local function render()
if not state.open then return end
syncState()
local caps = state.caps
local rows = {}
table.insert(rows, ui.row({ gap = 8, align = "center" }, {
ui.glyph({ name = "headset", size = 20 }),
ui.label({
text = (state.device and state.device.name) or noctalia.tr("panel.no_device"),
fontWeight = "bold",
flexGrow = 1,
}),
ui.button({ glyph = "x", onClick = "onCloseClicked" }),
}))
local pct = state.battery.level
local progress = (pct >= 0 and pct or 0) / 100
table.insert(rows, ui.row({ gap = 8, align = "center" }, {
ui.label({ text = noctalia.tr("panel.battery") .. ":", width = 120 }),
ui.progress({ progress = progress, flexGrow = 1 }),
ui.label({ text = pct >= 0 and (pct .. "%") or "--", width = 40, textAlign = "end" }),
}))
if caps.chatmix then
table.insert(rows, ui.row({ gap = 8, align = "center" }, {
ui.label({ text = noctalia.tr("panel.chatmix") .. ":", width = 120 }),
ui.label({ text = hsc.chatmixText(state.chatmix), flexGrow = 1 }),
}))
end
if caps.sidetone then
local st = state.sidetone or noctalia.getConfig("sidetone_default") or 64
table.insert(rows, ui.row({ gap = 8, align = "center" }, {
ui.label({ text = noctalia.tr("panel.sidetone") .. ":", width = 120 }),
ui.slider({
min = 0, max = 128, step = 1, value = st,
flexGrow = 1, onChange = "onSidetone", onDragEnd = "onSidetoneEnd",
}),
ui.label({ text = tostring(st), width = 40, textAlign = "end" }),
ui.button({ text = noctalia.tr("panel.off"), onClick = "onSidetoneOff" }),
}))
end
if caps.equalizer_preset and state.equalizerPresets > 0 then
local opts = {}
for i = 0, state.equalizerPresets - 1 do
table.insert(opts, noctalia.tr("panel.preset") .. " " .. tostring(i))
end
table.insert(rows, ui.row({ gap = 8, align = "center" }, {
ui.label({ text = noctalia.tr("panel.equalizer") .. ":", width = 120 }),
ui.select({ options = opts, selectedIndex = 0, flexGrow = 1, onChange = "onEqPreset" }),
}))
end
if caps.inactive_time then
local opts = { noctalia.tr("panel.never"), "1", "2", "5", "10", "15", "20", "30", "45", "60", "90" }
table.insert(rows, ui.row({ gap = 8, align = "center" }, {
ui.label({ text = noctalia.tr("panel.inactive_time") .. ":", width = 120 }),
ui.select({ options = opts, selectedIndex = 0, flexGrow = 1, onChange = "onInactive" }),
}))
end
if caps.noise_filter then
table.insert(rows, noiseButtons())
end
if caps.voice_prompts then
table.insert(rows, ui.row({ gap = 8, align = "center", justify = "space_between" }, {
ui.label({ text = noctalia.tr("panel.voice_prompts") }),
ui.toggle({ checked = false, onChange = "onVoicePrompts" }),
}))
end
if caps.volume_limiter then
table.insert(rows, ui.row({ gap = 8, align = "center", justify = "space_between" }, {
ui.label({ text = noctalia.tr("panel.volume_limiter") }),
ui.toggle({ checked = false, onChange = "onVolumeLimiter" }),
}))
end
if #rows == 0 then
table.insert(rows, ui.label({ text = noctalia.tr("panel.no_caps"), opacity = 0.6 }))
end
panel.render(ui.column({ gap = 12, padding = 16 }, rows))
end
function onVoicePrompts(value)
send("-v", value == "true" and "1" or "0")
end
function onVolumeLimiter(value)
send("--volume-limiter", value == "true" and "1" or "0")
end
function onOpen(_context)
state.open = true
render()
end
function onClose()
state.open = false
end
function onCloseClicked()
panel.close()
end
noctalia.state.watch("battery", function() render() end)
noctalia.state.watch("chatmix", function() render() end)
noctalia.state.watch("caps", function() render() end)
noctalia.state.watch("equalizerPresets", function() render() end)
noctalia.state.watch("device", function() render() end)