import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kcmutils as KCM
import "Lambent.js" as L
KCM.SimpleKCM {
id: configGeneral
property alias cfg_serverUrl: serverUrlField.text
property string cfg_circles: "[]"
// Connection test: a socket of our own, only dialled on request
property bool isTesting: false
property string testResult: ""
property bool testSuccess: false
property var devices: ({})
property var places: []
readonly property string base: L.normalizeBase(serverUrlField.text)
readonly property var circles: {
try {
var parsed = JSON.parse(cfg_circles || "[]")
return Array.isArray(parsed) ? parsed : []
} catch (e) {
return []
}
}
readonly property var deviceMacs: Object.keys(devices).sort(function(a, b) {
return L.deviceLabel(devices[a], a).localeCompare(L.deviceLabel(devices[b], b))
})
PhoenixSocket {
id: probe
url: ""
onOpened: probe.join("devices", {})
onMessage: function(topic, event, payload) {
if (topic !== "devices" || event !== "devices") return
var merged = Object.assign({}, configGeneral.devices, payload.devices || {})
configGeneral.devices = merged
var set = {}
for (var mac in merged) if (merged[mac].place) set[merged[mac].place] = true
configGeneral.places = Object.keys(set).sort()
// MQTT devices land a beat after the snapshot; give them a moment
settle.restart()
}
onConnStateChanged: {
if (probe.connState === "offline" && configGeneral.isTesting) {
configGeneral.finishTest(false, i18n("Could not reach %1", configGeneral.base)
+ (probe.lastError ? " — " + probe.lastError : ""))
}
}
}
Timer {
id: settle
interval: 800
onTriggered: {
var n = Object.keys(configGeneral.devices).length
configGeneral.finishTest(true, i18n("Connected — %1 in %2",
i18np("%1 device", "%1 devices", n),
i18np("%1 place", "%1 places", configGeneral.places.length)))
}
}
Timer {
id: giveUp
interval: 8000
onTriggered: configGeneral.finishTest(false, probe.live ? i18n("Connected, but no devices were reported")
: i18n("Timed out reaching %1", configGeneral.base))
}
function testConnection() {
if (!probe.available) {
finishTest(false, probe.unavailableReason)
return
}
if (base === "") {
finishTest(false, i18n("Enter the address of your LambentEx box"))
return
}
isTesting = true
testResult = ""
devices = {}
places = []
giveUp.restart()
var url = L.socketUrl(base)
if (probe.url === url) probe.reconnect()
else probe.url = url
}
function finishTest(ok, message) {
if (!isTesting) return
isTesting = false
giveUp.stop()
settle.stop()
testResult = message
testSuccess = ok
// keep the device list, drop the socket
probe.url = ""
}
// ── circle list editing ─────────────────────────────────────────────
function save(list) {
cfg_circles = JSON.stringify(list)
}
function hasPlace(name) {
return circles.some(function(c) { return c.kind === "place" && c.name === name })
}
function hasDevice(mac) {
return circles.some(function(c) { return c.kind === "device" && c.mac === mac })
}
function addPlace(name) {
if (!name || hasPlace(name)) return
save(circles.concat([{ kind: "place", name: name }]))
}
function addDevice(mac) {
if (!mac || hasDevice(mac)) return
var d = devices[mac]
// keep a label so the list still reads when the box is unreachable
save(circles.concat([{ kind: "device", mac: mac, label: L.deviceLabel(d, mac) }]))
}
function removeAt(index) {
var list = circles.slice()
list.splice(index, 1)
save(list)
}
function move(index, delta) {
var target = index + delta
if (target < 0 || target >= circles.length) return
var list = circles.slice()
var item = list.splice(index, 1)[0]
list.splice(target, 0, item)
save(list)
}
function addManual() {
var text = manualField.text.trim()
if (!text) return
// a MAC is a device; anything else is a place name
if (/^([0-9a-f]{2}:){5}[0-9a-f]{2}$/i.test(text)) addDevice(text.toLowerCase())
else addPlace(text)
manualField.text = ""
}
Component.onCompleted: if (base !== "" && probe.available) Qt.callLater(testConnection)
ColumnLayout {
spacing: Kirigami.Units.largeSpacing
// ── connection ──────────────────────────────────────────────────
Kirigami.FormLayout {
Layout.fillWidth: true
QQC2.TextField {
id: serverUrlField
Kirigami.FormData.label: i18n("LambentEx address:")
placeholderText: i18n("172.16.13.5 or http://lambent.local:8033")
Layout.fillWidth: true
onTextChanged: testResult = ""
onAccepted: testConnection()
}
QQC2.Label {
visible: configGeneral.base !== "" && configGeneral.base !== serverUrlField.text.trim()
Layout.fillWidth: true
font: Kirigami.Theme.smallFont
opacity: 0.7
text: i18n("Connects to %1", configGeneral.base)
}
RowLayout {
Kirigami.FormData.label: i18n("Connection:")
QQC2.Button {
text: i18n("Test Connection")
icon.name: "network-connect"
enabled: !isTesting && configGeneral.base !== ""
onClicked: testConnection()
}
QQC2.BusyIndicator {
visible: isTesting
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
}
}
QQC2.Label {
visible: testResult !== "" || !probe.available
text: probe.available ? testResult : probe.unavailableReason
color: testSuccess && probe.available ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.negativeTextColor
wrapMode: Text.WordWrap
Layout.fillWidth: true
font: Kirigami.Theme.smallFont
}
QQC2.Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
font: Kirigami.Theme.smallFont
color: Kirigami.Theme.disabledTextColor
text: i18n("The pairing QR in the web UI (the ⬛ icon, top right of any page) shows the address. A bare host is fine: port 8033 and plain http are assumed. There's nothing to log into — the socket is open on the LAN, same as the web UI.")
}
}
// ── circles ─────────────────────────────────────────────────────
Kirigami.Heading {
level: 3
text: i18n("Circles in the panel")
}
QQC2.Label {
visible: circles.length === 0
Layout.fillWidth: true
wrapMode: Text.WordWrap
opacity: 0.7
text: i18n("Nothing chosen: one circle per place, in alphabetical order (or one per device until any has a place). Add places or devices below to pick and order them yourself.")
}
Repeater {
model: circles
delegate: RowLayout {
required property var modelData
required property int index
readonly property bool isPlace: modelData.kind === "place"
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
Kirigami.Icon {
source: isPlace ? "go-home" : "lighttable"
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
}
QQC2.Label {
Layout.fillWidth: true
elide: Text.ElideRight
text: isPlace ? modelData.name
: (configGeneral.devices[modelData.mac] ? L.deviceLabel(configGeneral.devices[modelData.mac], modelData.mac)
: (modelData.label || modelData.mac))
+ " (" + modelData.mac + ")"
}
QQC2.ToolButton {
icon.name: "go-up"
enabled: index > 0
onClicked: move(index, -1)
}
QQC2.ToolButton {
icon.name: "go-down"
enabled: index < circles.length - 1
onClicked: move(index, 1)
}
QQC2.ToolButton {
icon.name: "list-remove"
onClicked: removeAt(index)
QQC2.ToolTip.text: i18n("Remove this circle")
QQC2.ToolTip.visible: hovered
}
}
}
RowLayout {
Layout.fillWidth: true
QQC2.TextField {
id: manualField
Layout.fillWidth: true
placeholderText: i18n("A place name, or a device's MAC address")
onAccepted: addManual()
}
QQC2.Button {
text: i18n("Add")
icon.name: "list-add"
enabled: manualField.text.trim() !== ""
onClicked: addManual()
}
}
// ── what the box knows about ────────────────────────────────────
Kirigami.Heading {
level: 3
text: i18n("On the box")
}
QQC2.Label {
visible: deviceMacs.length === 0
Layout.fillWidth: true
wrapMode: Text.WordWrap
opacity: 0.7
text: i18n("Test the connection to list places and devices.")
}
QQC2.Label {
visible: places.length > 0
text: i18n("Places")
font.bold: true
}
Repeater {
model: places
delegate: RowLayout {
required property var modelData
Layout.fillWidth: true
Kirigami.Icon {
source: "go-home"
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
}
QQC2.Label {
Layout.fillWidth: true
elide: Text.ElideRight
text: {
var n = 0
for (var mac in configGeneral.devices) if (configGeneral.devices[mac].place === modelData) n++
return modelData + " " + i18np("(%1 device)", "(%1 devices)", n)
}
}
QQC2.Button {
text: hasPlace(modelData) ? i18n("Shown") : i18n("Add")
icon.name: hasPlace(modelData) ? "checkmark" : "list-add"
enabled: !hasPlace(modelData)
onClicked: addPlace(modelData)
}
}
}
QQC2.Label {
visible: deviceMacs.length > 0
text: i18n("Devices")
font.bold: true
}
Repeater {
model: deviceMacs
delegate: RowLayout {
required property var modelData
readonly property var device: configGeneral.devices[modelData]
Layout.fillWidth: true
Kirigami.Icon {
source: "lighttable"
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
QQC2.Label {
Layout.fillWidth: true
elide: Text.ElideRight
text: L.deviceLabel(device, modelData) + (device && device.place ? " · " + device.place : "")
}
QQC2.Label {
Layout.fillWidth: true
elide: Text.ElideRight
font: Kirigami.Theme.smallFont
opacity: 0.7
text: [modelData, device ? device.ip || "" : "", device ? device.type || "" : ""]
.filter(function(s) { return s !== "" }).join(" · ")
}
}
QQC2.Button {
text: hasDevice(modelData) ? i18n("Shown") : i18n("Add")
icon.name: hasDevice(modelData) ? "checkmark" : "list-add"
enabled: !hasDevice(modelData)
onClicked: addDevice(modelData)
}
}
}
}
}
neiam
/lambent-ex-plasmoid
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
13.9 KiB · text
History
cc97953