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
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user