Add BatteryPanel with charge level, power profile settings, prevent sleep toggle, battery health (if available)

This commit is contained in:
art0rz
2025-11-22 13:14:51 +01:00
parent c3066e1dd5
commit 5cc71b4da2
15 changed files with 367 additions and 0 deletions
+1
View File
@@ -1,3 +1,4 @@
.qmlls.ini
.zed
Bin/battery-manager/uninstall-battery-manager.sh
.idea
+6
View File
@@ -381,14 +381,20 @@
}
},
"battery": {
"brightness": "Helligkeit",
"charge-level": "Ladestand",
"charging": "Wird geladen.",
"charging-rate": "Laderate: {rate} W.",
"discharging": "Wird entladen.",
"discharging-rate": "Entladerate: {rate} W.",
"health": "Zustand: {percent}%",
"inhibit-idle-description": "Hält das System wach.",
"inhibit-idle-label": "Ruhezustand verhindern",
"idle": "Leerlauf.",
"no-battery-detected": "Keine Batterie erkannt.",
"panel-title": "Akku",
"plugged-in": "Angeschlossen.",
"power-profile": "Energieprofil",
"time-left": "Verbleibende Zeit: {time}.",
"time-until-full": "Zeit bis vollständig geladen: {time}."
},
+6
View File
@@ -381,14 +381,20 @@
}
},
"battery": {
"brightness": "Brightness",
"charge-level": "Charge level",
"charging": "Charging",
"charging-rate": "Charging rate: {rate} W",
"discharging": "Discharging",
"discharging-rate": "Discharging rate: {rate} W",
"health": "Health: {percent}%",
"inhibit-idle-description": "Keeps the system awake.",
"inhibit-idle-label": "Prevent sleep",
"idle": "Idle",
"no-battery-detected": "No battery detected",
"panel-title": "Battery",
"plugged-in": "Plugged in",
"power-profile": "Power profile",
"time-left": "Time left: {time}",
"time-until-full": "Time until full: {time}"
},
+6
View File
@@ -381,14 +381,20 @@
}
},
"battery": {
"brightness": "Brillo",
"charge-level": "Nivel de carga",
"charging": "Cargando.",
"charging-rate": "Tasa de carga: {rate} W.",
"discharging": "Descargando.",
"discharging-rate": "Tasa de descarga: {rate} W.",
"health": "Salud: {percent}%",
"inhibit-idle-description": "Mantiene el sistema despierto.",
"inhibit-idle-label": "Evitar suspensión",
"idle": "Inactivo.",
"no-battery-detected": "No se detectó ninguna batería.",
"panel-title": "Batería",
"plugged-in": "Conectado.",
"power-profile": "Perfil de energía",
"time-left": "Tiempo restante: {time}.",
"time-until-full": "Tiempo hasta carga completa: {time}."
},
+6
View File
@@ -381,14 +381,20 @@
}
},
"battery": {
"brightness": "Luminosité",
"charge-level": "Niveau de charge",
"charging": "En charge.",
"charging-rate": "Taux de charge : {rate} W.",
"discharging": "En décharge.",
"discharging-rate": "Taux de décharge : {rate} W.",
"health": "État : {percent}%",
"inhibit-idle-description": "Maintient le système éveillé.",
"inhibit-idle-label": "Empêcher la veille",
"idle": "Inactif.",
"no-battery-detected": "Aucune batterie détectée.",
"panel-title": "Batterie",
"plugged-in": "Branché.",
"power-profile": "Profil dalimentation",
"time-left": "Temps restant : {time}.",
"time-until-full": "Temps jusqu'à charge complète : {time}."
},
+6
View File
@@ -381,14 +381,20 @@
}
},
"battery": {
"brightness": "Helderheid",
"charge-level": "Laadniveau",
"charging": "Opladen.",
"charging-rate": "Laadsnelheid: {rate} W.",
"discharging": "Ontladen.",
"discharging-rate": "Ontlaadsnelheid: {rate} W.",
"health": "Gezondheid: {percent}%",
"inhibit-idle-description": "Houdt het systeem wakker.",
"inhibit-idle-label": "Slaapstand voorkomen",
"idle": "In rust.",
"no-battery-detected": "Geen batterij gedetecteerd.",
"panel-title": "Batterij",
"plugged-in": "Op netstroom.",
"power-profile": "Energieprofiel",
"time-left": "Resterende tijd: {time}.",
"time-until-full": "Tijd tot vol: {time}."
},
+6
View File
@@ -381,14 +381,20 @@
}
},
"battery": {
"brightness": "Brilho",
"charge-level": "Nível de carga",
"charging": "Carregando.",
"charging-rate": "Taxa de carregamento: {rate} W.",
"discharging": "Descarregando.",
"discharging-rate": "Taxa de descarregamento: {rate} W.",
"health": "Saúde: {percent}%",
"inhibit-idle-description": "Mantém o sistema acordado.",
"inhibit-idle-label": "Impedir suspensão",
"idle": "Ocioso.",
"no-battery-detected": "Nenhuma bateria detectada.",
"panel-title": "Bateria",
"plugged-in": "Conectado.",
"power-profile": "Perfil de energia",
"time-left": "Tempo restante: {time}.",
"time-until-full": "Tempo até carga completa: {time}."
},
+6
View File
@@ -381,14 +381,20 @@
}
},
"battery": {
"brightness": "Яркость",
"charge-level": "Уровень заряда",
"charging": "Зарядка.",
"charging-rate": "Скорость зарядки: {rate} Вт.",
"discharging": "Разрядка.",
"discharging-rate": "Скорость разрядки: {rate} Вт.",
"health": "Здоровье: {percent}%",
"inhibit-idle-description": "Не дает системе уснуть.",
"inhibit-idle-label": "Запрет сна",
"idle": "Простой.",
"no-battery-detected": "Батарея не обнаружена.",
"panel-title": "Батарея",
"plugged-in": "Подключено.",
"power-profile": "Профиль питания",
"time-left": "Осталось времени: {time}.",
"time-until-full": "Время до полной зарядки: {time}."
},
+6
View File
@@ -381,14 +381,20 @@
}
},
"battery": {
"brightness": "Parlaklık",
"charge-level": "Şarj seviyesi",
"charging": "Şarj oluyor.",
"charging-rate": "Şarj oranı: {rate} W.",
"discharging": "Deşarj oluyor.",
"discharging-rate": "Deşarj oranı: {rate} W.",
"health": "Sağlık: {percent}%",
"inhibit-idle-description": "Sistemi uyanık tutar.",
"inhibit-idle-label": "Uykuya engelle",
"idle": "Boşta.",
"no-battery-detected": "Pil tespit edilmedi.",
"panel-title": "Pil",
"plugged-in": "Prize takılı.",
"power-profile": "Güç profili",
"time-left": "Kalan süre: {time}.",
"time-until-full": "Dolma süresi: {time}."
},
+6
View File
@@ -381,14 +381,20 @@
}
},
"battery": {
"brightness": "Яскравість",
"charge-level": "Рівень заряду",
"charging": "Зарядка.",
"charging-rate": "Швидкість зарядки: {rate} Вт.",
"discharging": "Розрядка.",
"discharging-rate": "Швидкість розрядки: {rate} Вт.",
"health": "Здоров'я: {percent}%",
"inhibit-idle-description": "Підтримує систему активною.",
"inhibit-idle-label": "Запобігати сну",
"idle": "Бездіяльність.",
"no-battery-detected": "Батарею не виявлено.",
"panel-title": "Батарея",
"plugged-in": "Підключено.",
"power-profile": "Профіль живлення",
"time-left": "Залишилось часу: {time}.",
"time-until-full": "Час до повного заряду: {time}."
},
+6
View File
@@ -381,14 +381,20 @@
}
},
"battery": {
"brightness": "亮度",
"charge-level": "电量",
"charging": "正在充电。",
"charging-rate": "充电速率:{rate} W。",
"discharging": "正在放电。",
"discharging-rate": "放电速率:{rate} W。",
"health": "健康状况:{percent}%",
"inhibit-idle-description": "保持系统唤醒。",
"inhibit-idle-label": "防止睡眠",
"idle": "空闲。",
"no-battery-detected": "未检测到电池。",
"panel-title": "电池",
"plugged-in": "已接通电源。",
"power-profile": "电源模式",
"time-left": "剩余时间:{time}。",
"time-until-full": "充满所需时间:{time}。"
},
+1
View File
@@ -168,6 +168,7 @@ Item {
}
return lines.join("\n");
}
onClicked: PanelService.getPanel("batteryPanel", screen)?.toggle(this)
onRightClicked: {
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);
if (popupMenuWindow) {
@@ -69,6 +69,13 @@ Item {
backgroundColor: panelBackgroundColor
}
// Battery
PanelBackground {
panel: root.windowRoot.batteryPanelPlaceholder
shapeContainer: backgroundsShape
backgroundColor: panelBackgroundColor
}
// Bluetooth
PanelBackground {
panel: root.windowRoot.bluetoothPanelPlaceholder
+9
View File
@@ -10,6 +10,7 @@ import qs.Commons
import qs.Modules.Bar
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Audio
import qs.Modules.Panels.Battery
import qs.Modules.Panels.Bluetooth
import qs.Modules.Panels.Brightness
import qs.Modules.Panels.Calendar
@@ -33,6 +34,7 @@ PanelWindow {
// Expose panels as readonly property aliases
readonly property alias audioPanel: audioPanel
readonly property alias batteryPanel: batteryPanel
readonly property alias bluetoothPanel: bluetoothPanel
readonly property alias brightnessPanel: brightnessPanel
readonly property alias calendarPanel: calendarPanel
@@ -49,6 +51,7 @@ PanelWindow {
// Expose panel placeholders for AllBackgrounds
readonly property var audioPanelPlaceholder: audioPanel.panelPlaceholder
readonly property var batteryPanelPlaceholder: batteryPanel.panelPlaceholder
readonly property var bluetoothPanelPlaceholder: bluetoothPanel.panelPlaceholder
readonly property var brightnessPanelPlaceholder: brightnessPanel.panelPlaceholder
readonly property var calendarPanelPlaceholder: calendarPanel.panelPlaceholder
@@ -168,6 +171,12 @@ PanelWindow {
screen: root.screen
}
BatteryPanel {
id: batteryPanel
objectName: "batteryPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
BluetoothPanel {
id: bluetoothPanel
objectName: "bluetoothPanel-" + (root.screen?.name || "unknown")
+289
View File
@@ -0,0 +1,289 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.UPower
import qs.Commons
import qs.Modules.MainScreen
import qs.Services.Hardware
import qs.Services.Power
import qs.Widgets
SmartPanel {
id: root
preferredWidth: Math.round(360 * Style.uiScaleRatio)
preferredHeight: Math.round(460 * Style.uiScaleRatio)
readonly property var battery: UPower.displayDevice
readonly property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent
readonly property int percent: isReady ? Math.round(battery.percentage * 100) : -1
readonly property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false
readonly property bool healthSupported: isReady && battery.healthSupported
readonly property bool healthAvailable: healthSupported
readonly property int healthPercent: healthAvailable ? Math.round(battery.healthPercentage) : -1
readonly property string timeText: {
if (!isReady)
return I18n.tr("battery.no-battery-detected");
if (charging && battery.timeToFull > 0) {
return I18n.tr("battery.time-until-full", {
"time": Time.formatVagueHumanReadableDuration(battery.timeToFull)
});
}
if (!charging && battery.timeToEmpty > 0) {
return I18n.tr("battery.time-left", {
"time": Time.formatVagueHumanReadableDuration(battery.timeToEmpty)
});
}
return I18n.tr("battery.idle");
}
readonly property string iconName: BatteryService.getIcon(percent, charging, isReady)
readonly property bool profilesAvailable: PowerProfileService.available
property int profileIndex: profileToIndex(PowerProfileService.profile)
property bool manualInhibitActive: manualInhibitorEnabled()
panelContent: Item {
property real contentPreferredHeight: mainLayout.implicitHeight + Style.marginL * 2
ColumnLayout {
id: mainLayout
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
// HEADER
NBox {
Layout.fillWidth: true
implicitHeight: headerRow.implicitHeight + (Style.marginM * 2)
RowLayout {
id: headerRow
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NIcon {
pointSize: Style.fontSizeXXL
color: Color.mPrimary
icon: iconName
}
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NText {
text: I18n.tr("battery.panel-title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NText {
text: timeText
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
wrapMode: Text.Wrap
Layout.fillWidth: true
}
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.close()
}
}
}
// Charge level + health/time
NBox {
Layout.fillWidth: true
implicitHeight: chargeLayout.implicitHeight + Style.marginM * 2
ColumnLayout {
id: chargeLayout
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginS
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: I18n.tr("battery.charge-level")
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeS
}
Item {
Layout.fillWidth: true
}
NText {
text: percent >= 0 ? `${percent}%` : "--"
color: Color.mOnSurface
pointSize: Style.fontSizeS
font.weight: Style.fontWeightBold
}
}
Rectangle {
Layout.fillWidth: true
height: Math.round(8 * Style.uiScaleRatio)
radius: height / 2
color: Color.mSurfaceVariant
Rectangle {
anchors.verticalCenter: parent.verticalCenter
height: parent.height
radius: parent.radius
width: {
var ratio = Math.max(0, Math.min(1, percent / 100));
return parent.width * ratio;
}
color: Color.mPrimary
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL
visible: healthAvailable
NText {
text: I18n.tr("battery.health", {"percent": healthPercent})
color: Color.mOnSurface
pointSize: Style.fontSizeS
font.weight: Style.fontWeightMedium
Layout.fillWidth: true
}
}
}
}
// Power profile and idle inhibit controls
NBox {
Layout.fillWidth: true
implicitHeight: controlsLayout.implicitHeight + Style.marginM * 2
ColumnLayout {
id: controlsLayout
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NIcon {
icon: PowerProfileService.getIcon(); pointSize: Style.fontSizeM; color: Color.mPrimary
}
NText {
text: I18n.tr("battery.power-profile")
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NText {
text: PowerProfileService.getName()
color: Color.mOnSurfaceVariant
}
}
NValueSlider {
Layout.fillWidth: true
from: 0
to: 2
stepSize: 1
snapAlways: true
value: profileIndex
enabled: profilesAvailable
onPressedChanged: function (pressed, v) {
if (!pressed) {
setProfileByIndex(Math.round(v));
}
}
onMoved: function (v) {
profileIndex = Math.round(v);
}
}
NToggle {
Layout.fillWidth: true
checked: manualInhibitActive
label: I18n.tr("battery.inhibit-idle-label")
description: I18n.tr("battery.inhibit-idle-description")
onToggled: function (checked) {
if (checked) {
IdleInhibitorService.addManualInhibitor(null);
} else {
IdleInhibitorService.removeManualInhibitor();
}
manualInhibitActive = checked;
}
}
}
}
}
}
function profileToIndex(p) {
switch (p) {
case PowerProfile.PowerSaver:
return 0;
case PowerProfile.Balanced:
return 1;
case PowerProfile.Performance:
return 2;
default:
return 1;
}
}
function indexToProfile(idx) {
switch (idx) {
case 0:
return PowerProfile.PowerSaver;
case 2:
return PowerProfile.Performance;
default:
return PowerProfile.Balanced;
}
}
function setProfileByIndex(idx) {
var prof = indexToProfile(idx);
profileIndex = idx;
PowerProfileService.setProfile(prof);
}
function manualInhibitorEnabled() {
return IdleInhibitorService.activeInhibitors && IdleInhibitorService.activeInhibitors.indexOf("manual") >= 0;
}
Connections {
target: IdleInhibitorService
function onIsInhibitedChanged() {
manualInhibitActive = manualInhibitorEnabled();
}
}
Timer {
id: inhibitorPoll
interval: 1000
repeat: true
running: true
onTriggered: manualInhibitActive = manualInhibitorEnabled();
}
Connections {
target: PowerProfileService
function onProfileChanged() {
profileIndex = profileToIndex(PowerProfileService.profile);
}
}
}