401 lines
14 KiB
QML
401 lines
14 KiB
QML
/*
|
|
* SPDX-FileCopyrightText: 2026 enne2
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import org.kde.plasma.plasmoid
|
|
import org.kde.plasma.core as PlasmaCore
|
|
import org.kde.plasma.components as PlasmaComponents3
|
|
import org.kde.plasma.extras as PlasmaExtras
|
|
import org.kde.plasma.plasma5support as P5Support
|
|
import org.kde.kirigami as Kirigami
|
|
|
|
PlasmoidItem {
|
|
id: root
|
|
|
|
// ---------------------------------------------------------------- percorsi
|
|
readonly property string pkgPath: Qt.resolvedUrl("..").toString().replace("file://", "")
|
|
readonly property string scriptPath: pkgPath + "/scripts/caffeine.sh"
|
|
readonly property string unitSrcPath: pkgPath + "/systemd"
|
|
readonly property string iconOnPath: Qt.resolvedUrl("../icons/caffeine-on.svg").toString()
|
|
readonly property string iconOffPath: Qt.resolvedUrl("../icons/caffeine-off.svg").toString()
|
|
readonly property string statusCommand: scriptPath + " status"
|
|
|
|
// ------------------------------------------------------------------ stato
|
|
property string backendState: "off" // on | off
|
|
property string backendMode: "none" // screen | sleep | none
|
|
property string backend: "unknown" // systemd | unsupported | unknown
|
|
property int deadlineEpoch: 0
|
|
property int remainingSec: 0
|
|
property bool unitsReady: false
|
|
property string lastError: ""
|
|
|
|
readonly property bool active: backendState === "on"
|
|
readonly property bool timerActive: active && remainingSec > 0
|
|
readonly property bool keepScreenOn: plasmoid.configuration.keepScreenOn
|
|
readonly property string startMode: keepScreenOn ? "screen" : "sleep"
|
|
|
|
// ------------------------------------------------------------ presentazione
|
|
Plasmoid.icon: active ? iconOnPath : iconOffPath
|
|
Plasmoid.status: active ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.PassiveStatus
|
|
toolTipMainText: active ? "Caffeine attivo" : "Caffeine inattivo"
|
|
toolTipTextFormat: Text.PlainText
|
|
toolTipSubText: {
|
|
let text = modeText()
|
|
if (timerActive) {
|
|
text += "\nSi disattiva automaticamente tra " + formatDuration(remainingSec)
|
|
}
|
|
text += "\nClic sinistro: attiva/disattiva · clic centrale: opzioni"
|
|
return text
|
|
}
|
|
|
|
// un clic sinistro non deve aprire il popup: lo gestiamo noi
|
|
activationTogglesExpanded: false
|
|
|
|
// ---------------------------------------------------------------- funzioni
|
|
function formatDuration(seconds) {
|
|
const total = Math.max(0, Math.floor(seconds))
|
|
const hours = Math.floor(total / 3600)
|
|
const minutes = Math.floor((total % 3600) / 60)
|
|
const secs = total % 60
|
|
const pad = value => (value < 10 ? "0" : "") + value
|
|
if (hours > 0) {
|
|
return hours + "h " + pad(minutes) + "m"
|
|
}
|
|
if (minutes > 0) {
|
|
return minutes + "m " + pad(secs) + "s"
|
|
}
|
|
return secs + "s"
|
|
}
|
|
|
|
function modeText() {
|
|
if (!active) {
|
|
return "Nessuna inibizione attiva: schermo e sospensione gestiti automaticamente"
|
|
}
|
|
if (backendMode === "sleep") {
|
|
return "Sospensione automatica bloccata · lo schermo può spegnersi"
|
|
}
|
|
return "Schermo, blocco schermo e sospensione automatica bloccati"
|
|
}
|
|
|
|
function applyStatus(output) {
|
|
const values = {}
|
|
String(output || "").split("\n").forEach(function(line) {
|
|
const index = line.indexOf("=")
|
|
if (index > 0) {
|
|
values[line.substring(0, index)] = line.substring(index + 1).trim()
|
|
}
|
|
})
|
|
if (values["backend"] !== undefined) {
|
|
backend = values["backend"]
|
|
}
|
|
if (values["state"] !== undefined) {
|
|
backendState = values["state"]
|
|
}
|
|
if (values["mode"] !== undefined) {
|
|
backendMode = values["mode"]
|
|
}
|
|
const deadline = parseInt(values["deadline"] || "0", 10)
|
|
deadlineEpoch = isNaN(deadline) ? 0 : deadline
|
|
if (deadlineEpoch > 0) {
|
|
const remaining = deadlineEpoch - Math.floor(Date.now() / 1000)
|
|
remainingSec = remaining > 0 ? remaining : 0
|
|
if (remaining <= 0) {
|
|
deactivate(true)
|
|
return
|
|
}
|
|
} else {
|
|
remainingSec = 0
|
|
}
|
|
}
|
|
|
|
function run(command) {
|
|
if (backend === "unsupported") {
|
|
return
|
|
}
|
|
const source = scriptPath + " " + command
|
|
actionSource.disconnectSource(source)
|
|
actionSource.connectSource(source)
|
|
}
|
|
|
|
function refresh() {
|
|
statusSource.disconnectSource(statusCommand)
|
|
statusSource.connectSource(statusCommand)
|
|
}
|
|
|
|
function activate() {
|
|
const minutes = plasmoid.configuration.autoOffMinutes
|
|
run("on " + startMode + " " + minutes)
|
|
remainingSec = minutes > 0 ? minutes * 60 : 0
|
|
refreshDelay.restart()
|
|
}
|
|
|
|
function deactivate(fromTimer) {
|
|
const notify = (fromTimer === true && plasmoid.configuration.notifyOnTimerEnd) ? " notify" : ""
|
|
run("off" + notify)
|
|
remainingSec = 0
|
|
deadlineEpoch = 0
|
|
refreshDelay.restart()
|
|
}
|
|
|
|
function toggle() {
|
|
if (active) {
|
|
deactivate(false)
|
|
} else {
|
|
activate()
|
|
}
|
|
}
|
|
|
|
function changeMode(keepScreen) {
|
|
if (!active) {
|
|
return
|
|
}
|
|
run("switch " + (keepScreen ? "screen" : "sleep"))
|
|
refreshDelay.restart()
|
|
}
|
|
|
|
function changeTimer(minutes) {
|
|
if (!active) {
|
|
return
|
|
}
|
|
if (minutes > 0) {
|
|
remainingSec = minutes * 60
|
|
run("on " + startMode + " " + minutes)
|
|
} else {
|
|
remainingSec = 0
|
|
run("switch " + startMode)
|
|
}
|
|
refreshDelay.restart()
|
|
}
|
|
|
|
// ---------------------------------------------------------------- backend
|
|
// Stato: polling ogni 2 secondi (systemctl --user is-active).
|
|
P5Support.DataSource {
|
|
id: statusSource
|
|
engine: "executable"
|
|
interval: 2000
|
|
connectedSources: [root.statusCommand]
|
|
onNewData: function(sourceName, data) {
|
|
root.applyStatus(data ? data["stdout"] : "")
|
|
}
|
|
}
|
|
|
|
// Comandi una tantum (on/off/switch), un source per comando.
|
|
P5Support.DataSource {
|
|
id: actionSource
|
|
engine: "executable"
|
|
connectedSources: []
|
|
onNewData: function(sourceName, data) {
|
|
const output = data ? String(data["stdout"] || "") : ""
|
|
root.lastError = output.indexOf("error=") === 0 ? output : ""
|
|
if (root.lastError !== "") {
|
|
console.warn("enne2-caffeine:", root.lastError)
|
|
}
|
|
root.refresh()
|
|
}
|
|
}
|
|
|
|
// Installa/aggiorna le unità systemd utente al primo avvio.
|
|
P5Support.DataSource {
|
|
id: unitsSource
|
|
engine: "executable"
|
|
connectedSources: [root.scriptPath + " ensure-units " + root.unitSrcPath]
|
|
onNewData: function(sourceName, data) {
|
|
root.unitsReady = Boolean(data && Number(data["exit code"]) === 0
|
|
&& String(data["stdout"] || "").indexOf("ok=1") === 0)
|
|
root.refresh()
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: refreshDelay
|
|
interval: 300
|
|
repeat: false
|
|
onTriggered: root.refresh()
|
|
}
|
|
|
|
// Countdown del timer di disattivazione automatica.
|
|
Timer {
|
|
interval: 1000
|
|
repeat: true
|
|
running: root.timerActive
|
|
onTriggered: {
|
|
const remaining = root.deadlineEpoch - Math.floor(Date.now() / 1000)
|
|
root.remainingSec = remaining > 0 ? remaining : 0
|
|
if (remaining <= 0) {
|
|
root.deactivate(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------- rappresentazione compatta
|
|
compactRepresentation: Item {
|
|
implicitWidth: compactRow.implicitWidth
|
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
|
|
|
RowLayout {
|
|
id: compactRow
|
|
anchors.centerIn: parent
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
Kirigami.Icon {
|
|
source: root.active ? root.iconOnPath : root.iconOffPath
|
|
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
|
}
|
|
|
|
PlasmaComponents3.Label {
|
|
visible: root.timerActive
|
|
text: Math.ceil(root.remainingSec / 60) + "m"
|
|
font: Kirigami.Theme.smallFont
|
|
color: Kirigami.Theme.textColor
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: function(mouse) {
|
|
if (mouse.button === Qt.MiddleButton) {
|
|
root.expanded = !root.expanded
|
|
} else {
|
|
root.toggle()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------- popup opzioni
|
|
fullRepresentation: PlasmaExtras.Representation {
|
|
Layout.minimumWidth: Kirigami.Units.gridUnit * 20
|
|
Layout.minimumHeight: Kirigami.Units.gridUnit * 13
|
|
Layout.preferredWidth: Kirigami.Units.gridUnit * 20
|
|
Layout.preferredHeight: popupColumn.implicitHeight + Kirigami.Units.gridUnit * 2
|
|
collapseMarginsHint: true
|
|
|
|
ColumnLayout {
|
|
id: popupColumn
|
|
anchors.fill: parent
|
|
anchors.margins: Kirigami.Units.largeSpacing
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
Kirigami.Icon {
|
|
source: root.active ? root.iconOnPath : root.iconOffPath
|
|
implicitWidth: Kirigami.Units.iconSizes.medium
|
|
implicitHeight: Kirigami.Units.iconSizes.medium
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 0
|
|
|
|
Kirigami.Heading {
|
|
Layout.fillWidth: true
|
|
level: 3
|
|
text: root.active ? "Caffeine attivo" : "Caffeine inattivo"
|
|
}
|
|
|
|
PlasmaComponents3.Label {
|
|
Layout.fillWidth: true
|
|
text: root.modeText()
|
|
wrapMode: Text.WordWrap
|
|
opacity: 0.7
|
|
}
|
|
}
|
|
}
|
|
|
|
PlasmaComponents3.Label {
|
|
Layout.fillWidth: true
|
|
visible: root.timerActive
|
|
text: "Disattivazione automatica tra " + root.formatDuration(root.remainingSec)
|
|
color: Kirigami.Theme.positiveTextColor
|
|
}
|
|
|
|
Kirigami.Separator {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
}
|
|
|
|
PlasmaComponents3.CheckBox {
|
|
Layout.fillWidth: true
|
|
text: "Impedisci anche lo spegnimento dello schermo"
|
|
checked: root.keepScreenOn
|
|
onToggled: {
|
|
plasmoid.configuration.keepScreenOn = checked
|
|
root.changeMode(checked)
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
PlasmaComponents3.Label {
|
|
text: "Disattiva dopo:"
|
|
}
|
|
|
|
PlasmaComponents3.ComboBox {
|
|
id: timerCombo
|
|
Layout.fillWidth: true
|
|
model: ["Mai", "5 minuti", "15 minuti", "30 minuti", "1 ora", "2 ore", "4 ore"]
|
|
readonly property var minutes: [0, 5, 15, 30, 60, 120, 240]
|
|
currentIndex: Math.max(0, minutes.indexOf(plasmoid.configuration.autoOffMinutes))
|
|
onActivated: function(index) {
|
|
const value = minutes[index]
|
|
plasmoid.configuration.autoOffMinutes = value
|
|
root.changeTimer(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
PlasmaComponents3.CheckBox {
|
|
Layout.fillWidth: true
|
|
visible: plasmoid.configuration.autoOffMinutes > 0
|
|
text: "Notifica quando scade il timer"
|
|
checked: plasmoid.configuration.notifyOnTimerEnd
|
|
onToggled: plasmoid.configuration.notifyOnTimerEnd = checked
|
|
}
|
|
|
|
PlasmaComponents3.Label {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
visible: root.backend === "unsupported" || root.lastError !== ""
|
|
color: Kirigami.Theme.negativeTextColor
|
|
wrapMode: Text.WordWrap
|
|
text: root.backend === "unsupported"
|
|
? "Backend non disponibile: serve systemd con gestore utente attivo."
|
|
: "Errore dal backend: " + root.lastError
|
|
}
|
|
|
|
Item {
|
|
Layout.fillHeight: true
|
|
}
|
|
|
|
PlasmaComponents3.Button {
|
|
Layout.fillWidth: true
|
|
text: root.active ? "Disattiva Caffeine" : "Attiva Caffeine"
|
|
icon.name: root.active ? "media-playback-stop" : "media-playback-start"
|
|
onClicked: root.toggle()
|
|
}
|
|
|
|
PlasmaComponents3.Label {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
horizontalAlignment: Text.AlignHCenter
|
|
font: Kirigami.Theme.smallFont
|
|
opacity: 0.6
|
|
wrapMode: Text.WordWrap
|
|
text: "Clic sinistro sull'icona: attiva/disattiva\nClic centrale: queste opzioni"
|
|
}
|
|
}
|
|
}
|
|
}
|