307 lines
9.1 KiB
Luau
307 lines
9.1 KiB
Luau
local hsc = require("./lib/hsc.luau")
|
|
|
|
hsc.init({
|
|
device = noctalia.getConfig("device"),
|
|
mock = noctalia.getConfig("mock"),
|
|
wake_sink = noctalia.getConfig("wake_sink"),
|
|
})
|
|
|
|
local state = {
|
|
open = false,
|
|
device = nil,
|
|
battery = { status = "BATTERY_UNAVAILABLE", level = -1 },
|
|
chatmix = 0,
|
|
caps = {},
|
|
equalizerPresets = 0,
|
|
sidetone = nil,
|
|
voicePrompts = false,
|
|
volumeLimiter = false,
|
|
eqPreset = 0,
|
|
inactive = 0,
|
|
waking = false,
|
|
connected = nil,
|
|
}
|
|
|
|
local INACTIVE_VALUES = { 0, 1, 2, 5, 10, 15, 20, 30, 45, 60, 90 }
|
|
local INACTIVE_LABELS = { noctalia.tr("panel.never"), "1", "2", "5", "10", "15", "20", "30", "45", "60", "90" }
|
|
local committedSidetone = noctalia.getConfig("sidetone_default") or 64
|
|
|
|
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
|
|
committedSidetone = v
|
|
end
|
|
end
|
|
|
|
local render -- forward declaration (send/refreshConnected/onWake call it before its definition)
|
|
|
|
local function send(flag, value, onOk, onFail)
|
|
hsc.sendCommand({ flag, value }, function(res)
|
|
if res.exitCode ~= 0 then
|
|
if onFail then onFail() end
|
|
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 = state.sidetone
|
|
if v == nil then return end
|
|
local prev = committedSidetone
|
|
send("-s", tostring(v), function()
|
|
committedSidetone = v
|
|
noctalia.state.set("sidetone", v)
|
|
end, function()
|
|
state.sidetone = prev
|
|
render()
|
|
end)
|
|
end
|
|
|
|
function onSidetoneOff()
|
|
local prev = committedSidetone
|
|
send("-s", "0", function()
|
|
state.sidetone = 0
|
|
committedSidetone = 0
|
|
noctalia.state.set("sidetone", 0)
|
|
end, function()
|
|
state.sidetone = prev
|
|
render()
|
|
end)
|
|
end
|
|
|
|
local function refreshConnected()
|
|
hsc.fetchConnected(function(connected)
|
|
state.connected = connected
|
|
state.waking = false
|
|
render()
|
|
end)
|
|
end
|
|
|
|
function onWake()
|
|
if state.waking then return end
|
|
state.waking = true
|
|
render()
|
|
hsc.wake(function(ok)
|
|
if not ok then
|
|
state.waking = false
|
|
noctalia.notifyError(noctalia.tr("error.command_failed"), noctalia.tr("panel.wake"))
|
|
render()
|
|
return
|
|
end
|
|
hsc.fetchStatus(function(status)
|
|
if status ~= nil then
|
|
noctalia.state.set("battery", hsc.normalizeBattery(status.battery))
|
|
noctalia.state.set("chatmix", status.chatmix or 0)
|
|
noctalia.state.set("lastUpdate", noctalia.nowMs())
|
|
end
|
|
refreshConnected()
|
|
end)
|
|
end)
|
|
end
|
|
|
|
function onEqPreset(idx)
|
|
local i = tonumber(idx) or 0
|
|
local prev = state.eqPreset
|
|
state.eqPreset = i
|
|
render()
|
|
send("-p", tostring(i), nil, function()
|
|
state.eqPreset = prev
|
|
render()
|
|
end)
|
|
end
|
|
|
|
function onInactive(idx)
|
|
local i = tonumber(idx) or 0
|
|
local value = INACTIVE_VALUES[i + 1]
|
|
if value == nil then return end
|
|
local prev = state.inactive
|
|
state.inactive = i
|
|
render()
|
|
send("-i", tostring(value), nil, function()
|
|
state.inactive = prev
|
|
render()
|
|
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
|
|
|
|
render = function()
|
|
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.label({
|
|
text = state.connected == nil and "" or (state.connected and noctalia.tr("panel.connected") or noctalia.tr("panel.disconnected")),
|
|
color = state.connected and "#3fb950" or "#6e7681",
|
|
fontSize = 11,
|
|
}),
|
|
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.battery then
|
|
table.insert(rows, ui.row({ gap = 8, align = "center" }, {
|
|
ui.label({ text = "", flexGrow = 1 }),
|
|
ui.button({ text = state.waking and noctalia.tr("panel.waking") or noctalia.tr("panel.wake"), enabled = not state.waking, onClick = "onWake" }),
|
|
}))
|
|
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 committedSidetone 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 = state.eqPreset, flexGrow = 1, onChange = "onEqPreset" }),
|
|
}))
|
|
end
|
|
|
|
if caps.inactive_time then
|
|
local opts = {}
|
|
for i, value in ipairs(INACTIVE_VALUES) do
|
|
table.insert(opts, INACTIVE_LABELS[i] or tostring(value))
|
|
end
|
|
table.insert(rows, ui.row({ gap = 8, align = "center" }, {
|
|
ui.label({ text = noctalia.tr("panel.inactive_time") .. ":", width = 120 }),
|
|
ui.select({ options = opts, selectedIndex = state.inactive, 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 = state.voicePrompts, 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 = state.volumeLimiter, onChange = "onVolumeLimiter" }),
|
|
}))
|
|
end
|
|
|
|
if not (caps.chatmix or caps.sidetone or caps.equalizer_preset or caps.inactive_time or caps.noise_filter or caps.voice_prompts or caps.volume_limiter) 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)
|
|
local newVal = value == "true"
|
|
local prev = state.voicePrompts
|
|
state.voicePrompts = newVal
|
|
render()
|
|
send("-v", newVal and "1" or "0", nil, function()
|
|
state.voicePrompts = prev
|
|
render()
|
|
end)
|
|
end
|
|
|
|
function onVolumeLimiter(value)
|
|
local newVal = value == "true"
|
|
local prev = state.volumeLimiter
|
|
state.volumeLimiter = newVal
|
|
render()
|
|
send("--volume-limiter", newVal and "1" or "0", nil, function()
|
|
state.volumeLimiter = prev
|
|
render()
|
|
end)
|
|
end
|
|
|
|
function onOpen(_context)
|
|
state.open = true
|
|
refreshConnected()
|
|
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)
|
|
noctalia.state.watch("sidetone", function(v)
|
|
if v ~= nil then
|
|
state.sidetone = v
|
|
committedSidetone = v
|
|
end
|
|
render()
|
|
end)
|