- 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
284 lines
9.3 KiB
QML
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()
|
|
}
|