Files
ambilight-plasmoid/package/org.enne2.ambilight/contents/ui/main.qml
T
enne2 a3b6bc66c9 Ambilight per Plasma 6: demone Adalight + plasmoide con slider
- backend/ambicapture.py: cattura Wayland via ScreenCast portal + PipeWire
- backend/ambilightd.py: demone, 47 zone sul bordo superiore, luminosita/contrasto/
  saturazione/banda, invio Adalight su seriale, servizio D-Bus org.enne2.Ambilight
- backend/ambilight-ctl: CLI JSON per il plasmoide
- backend/ambilight.service: avvio automatico (systemd --user)
- package/org.enne2.ambilight: applet vassoio con popup e slider
- install.sh / uninstall.sh senza root
2026-11-01 13:11:02 +01:00

284 lines
9.3 KiB
QML

import QtQuick
import QtQuick.Layouts
import org.kde.plasma.plasmoid
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.plasma5support as P5Support
import org.kde.kirigami as Kirigami
PlasmoidItem {
id: root
// Percorso dell'helper, sostituito da install.sh con il percorso assoluto
readonly property string ctl: "@CTL@"
property var st: ({ daemon: false, running: false, brightness: 1.0,
contrast: 1.0, saturation: 1.0, band: 0.12,
leds: 47, fps: 0.0, error: "" })
property bool busy: false
property string lastError: ""
preferredRepresentation: compactRepresentation
toolTipMainText: "Ambilight"
toolTipSubText: {
if (!st.daemon)
return i18n("Demone non raggiungibile")
if (!st.running)
return i18n("Fermo")
return i18n("%1 LED · %2 fps", st.leds, Math.round(st.fps))
}
function refresh() {
poll.connectSource(root.ctl + " state")
}
function run(args) {
if (root.busy)
return
root.busy = true
exec.connectSource(root.ctl + " " + args)
}
// ---- stato (polling)
P5Support.DataSource {
id: poll
engine: "executable"
interval: 2000
connectedSources: []
onNewData: function(source, data) {
poll.disconnectSource(source)
const out = String(data["stdout"] || "").trim()
if (!out)
return
try {
const s = JSON.parse(out)
root.st = s
root.lastError = s.error || ""
} catch (e) {
root.lastError = "stato illeggibile"
}
}
}
// ---- comandi (uno alla volta)
P5Support.DataSource {
id: exec
engine: "executable"
interval: 0
connectedSources: []
onNewData: function(source, data) {
exec.disconnectSource(source)
root.busy = false
const out = String(data["stdout"] || "").trim()
if (out !== "") {
try {
const r = JSON.parse(out)
root.lastError = (r.ok === false) ? (r.error || "errore") : ""
if (r.state)
root.st = r.state
} catch (e) {
root.lastError = ""
}
}
refresh()
}
}
// ---- icona nel vassoio
compactRepresentation: Item {
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
Layout.minimumWidth: implicitWidth
Layout.minimumHeight: implicitHeight
Layout.preferredWidth: implicitWidth
Layout.preferredHeight: implicitHeight
MouseArea {
id: compactMouse
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onClicked: root.expanded = !root.expanded
onContainsMouseChanged: {
if (containsMouse)
root.refresh()
}
Kirigami.Icon {
anchors.fill: parent
source: "video-television"
active: compactMouse.containsMouse
opacity: root.st.running ? 1.0 : 0.45
}
}
}
// ---- popup
fullRepresentation: Item {
implicitWidth: Kirigami.Units.gridUnit * 17
implicitHeight: col.implicitHeight + 2 * Kirigami.Units.largeSpacing
Layout.preferredWidth: implicitWidth
Layout.preferredHeight: implicitHeight
Layout.minimumWidth: Kirigami.Units.gridUnit * 15
Layout.minimumHeight: implicitHeight
ColumnLayout {
id: col
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.smallSpacing
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
Kirigami.Icon {
source: "video-television"
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
}
PC3.Label {
text: i18n("Ambilight")
font.bold: true
Layout.fillWidth: true
}
PC3.Switch {
id: powerSwitch
checked: root.st.running
enabled: root.st.daemon
onToggled: root.run(powerSwitch.checked ? "start" : "stop")
}
}
PC3.Label {
Layout.fillWidth: true
text: !root.st.daemon
? i18n("Demone non attivo")
: (root.st.running
? i18n("%1 LED · %2 fps · banda %3%",
root.st.leds, Math.round(root.st.fps),
Math.round(root.st.band * 100))
: i18n("Spento · %1 LED", root.st.leds))
opacity: 0.75
font: Kirigami.Theme.smallFont
color: root.st.daemon ? Kirigami.Theme.textColor : Kirigami.Theme.negativeTextColor
}
component ParamRow: RowLayout {
id: row
property string label
property real from: 0
property real to: 200
property real current: 100
property bool percent: true
property bool active: true
signal applied(real v)
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
PC3.Label {
text: row.label
Layout.preferredWidth: Kirigami.Units.gridUnit * 4
font: Kirigami.Theme.smallFont
}
PC3.Slider {
id: sl
Layout.fillWidth: true
from: row.from
to: row.to
stepSize: 1
enabled: row.active
value: row.current
onMoved: row.applied(value)
}
PC3.Label {
text: Math.round(sl.value) + (row.percent ? "%" : "")
Layout.preferredWidth: Kirigami.Units.gridUnit * 2.2
horizontalAlignment: Text.AlignRight
font: Kirigami.Theme.smallFont
opacity: 0.8
}
}
ParamRow {
label: i18n("Luminosità")
from: 5
to: 200
active: root.st.daemon
current: root.st.brightness * 100
onApplied: function(v) { root.run("set brightness " + (v / 100.0).toFixed(2)) }
}
ParamRow {
label: i18n("Contrasto")
from: 50
to: 200
active: root.st.daemon
current: root.st.contrast * 100
onApplied: function(v) { root.run("set contrast " + (v / 100.0).toFixed(2)) }
}
ParamRow {
label: i18n("Saturazione")
from: 0
to: 200
active: root.st.daemon
current: root.st.saturation * 100
onApplied: function(v) { root.run("set saturation " + (v / 100.0).toFixed(2)) }
}
ParamRow {
label: i18n("Banda")
from: 2
to: 50
active: root.st.daemon
current: root.st.band * 100
onApplied: function(v) { root.run("set band " + (v / 100.0).toFixed(2)) }
}
PC3.Label {
Layout.fillWidth: true
visible: root.lastError !== ""
text: root.lastError
color: Kirigami.Theme.negativeTextColor
font: Kirigami.Theme.smallFont
wrapMode: Text.Wrap
}
RowLayout {
Layout.fillWidth: true
PC3.Button {
text: i18n("Riavvia demone")
font: Kirigami.Theme.smallFont
onClicked: {
root.busy = true
restartSource.connectSource("systemctl --user restart ambilight.service")
}
}
Item { Layout.fillWidth: true }
PC3.Button {
text: i18n("Aggiorna")
font: Kirigami.Theme.smallFont
onClicked: root.refresh()
}
}
}
}
// sorgente dedicata al riavvio: nessun ritorno JSON, quindi handler separato
P5Support.DataSource {
id: restartSource
engine: "executable"
interval: 0
connectedSources: []
onNewData: function(source, data) {
restartSource.disconnectSource(source)
root.busy = false
root.refresh()
}
}
Component.onCompleted: root.refresh()
}