Battery: some fixes & cleanup

This commit is contained in:
notiant
2026-01-29 10:43:43 +01:00
committed by GitHub
parent 27bee0033d
commit 77a92d604c
5 changed files with 101 additions and 177 deletions
+40 -54
View File
@@ -40,10 +40,9 @@ Item {
readonly property real warningThreshold: widgetSettings.warningThreshold !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold
readonly property bool hideIfNotDetected: widgetSettings.hideIfNotDetected !== undefined ? widgetSettings.hideIfNotDetected : widgetMetadata.hideIfNotDetected
readonly property bool hideIfIdle: widgetSettings.hideIfIdle !== undefined ? widgetSettings.hideIfIdle : widgetMetadata.hideIfIdle
// Only show low battery warning if device is ready (prevents false positive during initialization)
readonly property bool isLowBattery: isReady && (!isCharging && !isPluggedIn) && percent <= warningThreshold
// Visibility: show if hideIfNotDetected is false, or if battery is ready (after initialization)
// Visibility: show if hideIfNotDetected is false, or if battery is ready
readonly property bool shouldShow: !hideIfNotDetected || (isReady && (hideIfIdle ? (!isCharging && !isPluggedIn) : true))
// Test mode
@@ -51,78 +50,65 @@ Item {
readonly property int testPercent: 35
readonly property bool testCharging: false
readonly property bool testPluggedIn: false
readonly property string deviceNativePath: widgetSettings.deviceNativePath || ""
readonly property string deviceNativePath: widgetSettings.deviceNativePath || "__default__"
readonly property var battery: BatteryService.findUPowerDevice(deviceNativePath)
readonly property var bluetoothDevice: deviceNativePath ? BatteryService.findBluetoothDevice(deviceNativePath) : null
readonly property var device: {
if (deviceNativePath)
return bluetoothDevice || battery;
readonly property var selectedBattery: BatteryService.findUPowerDevice(deviceNativePath)
readonly property var selectedBluetoothDevice: BatteryService.findBluetoothDevice(deviceNativePath)
readonly property var selectedDevice: {
if (BatteryService.isDevicePresent(selectedBluetoothDevice)) {
return selectedBluetoothDevice;
}
if (BatteryService.isDevicePresent(selectedBattery)) {
return selectedBattery;
}
return BatteryService.primaryDevice;
}
readonly property bool hasBluetoothBattery: BatteryService.isBluetoothDevice(device)
readonly property bool isReady: testMode ? true : (initializationComplete && BatteryService.isDeviceReady(device))
readonly property real percent: testMode ? testPercent : (isReady ? BatteryService.getPercentage(device) : 0)
readonly property bool isCharging: testMode ? testCharging : (isReady ? BatteryService.isCharging(device) : false)
readonly property bool isPluggedIn: testMode ? testPluggedIn : (isReady ? BatteryService.isPluggedIn(device) : false)
// Check if selected device is actually present/connected
readonly property bool isPresent: testMode ? true : BatteryService.isDevicePresent(selectedDevice)
readonly property bool isReady: testMode ? true: BatteryService.isDeviceReady(selectedDevice)
readonly property real percent: testMode ? testPercent : (isReady ? Math.round(BatteryService.getPercentage(selectedDevice)) : -1)
readonly property bool isCharging: testMode ? testCharging : (isReady ? BatteryService.isCharging(selectedDevice) : false)
readonly property bool isPluggedIn: testMode ? testPluggedIn : (isReady ? BatteryService.isPluggedIn(selectedDevice) : false)
property bool initializationComplete: false
property bool hasNotifiedLowBattery: false
visible: shouldShow
opacity: shouldShow ? 1.0 : 0.0
Timer {
interval: 500
running: true
onTriggered: root.initializationComplete = true
}
readonly property bool isDevicePresent: {
if (testMode)
return true;
return BatteryService.isDevicePresent(device);
}
implicitWidth: pill.width
implicitHeight: pill.height
function maybeNotify(currentPercent, charging, pluggedIn, isReady) {
if (isReady && (!charging && !pluggedIn) && !hasNotifiedLowBattery && currentPercent <= warningThreshold) {
hasNotifiedLowBattery = true;
ToastService.showWarning(I18n.tr("toast.battery.low"), I18n.tr("toast.battery.low-desc", {
"percent": Math.round(currentPercent)
}), "battery-exclamation", "warning", 4000, "", null);
ToastService.showWarning(I18n.tr("toast.battery.low"), I18n.tr("toast.battery.low-desc", {"percent": Math.round(currentPercent)}), "battery-exclamation");
} else if (hasNotifiedLowBattery && (charging || pluggedIn || currentPercent > warningThreshold + 5)) {
hasNotifiedLowBattery = false;
}
}
function getCurrentPercent() {
return BatteryService.getPercentage(device);
}
Connections {
target: device
target: selectedDevice?.type === UPowerDeviceType.Battery ? selectedDevice : null
function onPercentageChanged() {
if (device) {
maybeNotify(getCurrentPercent(), isCharging, isPluggedIn, isReady);
}
maybeNotify(BatteryService.getPercentage(selectedBattery), isCharging, isPluggedIn, isReady);
}
function onStateChanged() {
if (device) {
if (isCharging || isPluggedIn) {
hasNotifiedLowBattery = false;
}
maybeNotify(getCurrentPercent(), isCharging, isPluggedIn, isReady);
if (isCharging || isPluggedIn) {
hasNotifiedLowBattery = false;
}
maybeNotify(BatteryService.getPercentage(selectedBattery), isCharging, isPluggedIn, isReady);
}
}
Connections {
target: (device && BatteryService.isBluetoothDevice(device)) ? device : null
target: selectedDevice?.batteryAvailable ? selectedDevice : null
function onBatteryChanged() {
maybeNotify(BatteryService.getPercentage(selectedBluetoothDevice), isCharging, isPluggedIn, isReady);
}
}
NPopupContextMenu {
@@ -155,9 +141,9 @@ Item {
suffix: "%"
autoHide: false
forceOpen: isReady && displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide" || (initializationComplete && !isReady)
customBackgroundColor: !initializationComplete ? "transparent" : (isCharging ? Color.mPrimary : (isLowBattery ? Color.mError : "transparent"))
customTextIconColor: !initializationComplete ? "transparent" : (isCharging ? Color.mOnPrimary : (isLowBattery ? Color.mOnError : "transparent"))
forceClose: displayMode === "alwaysHide" || !isReady
customBackgroundColor: isCharging ? Color.mPrimary : (isLowBattery ? Color.mError : "transparent")
customTextIconColor: isCharging ? Color.mOnPrimary : (isLowBattery ? Color.mOnError : "transparent")
tooltipText: {
let lines = [];
@@ -165,25 +151,25 @@ Item {
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345) + ".");
return lines.join("\n");
}
if (!isReady || !isDevicePresent) {
if (!isReady || !isPresent) {
return I18n.tr("battery.no-battery-detected");
}
const isInternal = device === BatteryService.primaryDevice && BatteryService.isLaptopBattery;
const isInternal = selectedDevice === BatteryService.primaryDevice && BatteryService.isLaptopBattery;
if (isInternal) {
let timeText = BatteryService.getTimeRemainingText(device);
let timeText = BatteryService.getTimeRemainingText(selectedDevice);
if (timeText && timeText !== I18n.tr("common.idle") && timeText !== I18n.tr("battery.no-battery-detected") && timeText !== I18n.tr("battery.plugged-in")) {
lines.push(timeText);
}
let rateText = BatteryService.getRateText(device);
let rateText = BatteryService.getRateText(selectedDevice);
if (rateText) {
lines.push(rateText);
}
} else if (device) {
} else if (selectedDevice) {
// External / Peripheral Device (Phone, Keyboard, Mouse, Gamepad, Headphone etc.)
let name = BatteryService.getDeviceName(device);
let pct = Math.round(BatteryService.getPercentage(device));
let name = BatteryService.getDeviceName(selectedDevice);
let pct = Math.round(percent);
lines.push(name + ": " + pct + suffix);
}
+1 -1
View File
@@ -78,7 +78,7 @@ Loader {
Item {
id: batteryIndicator
property bool isReady: BatteryService.ready && BatteryService.batteryReady
property bool isReady: BatteryService.batteryReady
property real percent: BatteryService.batteryPercentage
property bool charging: BatteryService.batteryCharging
property bool pluggedIn: BatteryService.batteryPluggedIn
+13 -41
View File
@@ -25,33 +25,26 @@ SmartPanel {
id: panelContent
property real contentPreferredHeight: mainLayout.implicitHeight + Style.marginL * 2
// Get device selection from Battery widget settings (check right section first, then any Battery widget)
function getBatteryDevicePath() {
var widget = BarService.lookupWidget("Battery");
if (widget !== undefined && widget.deviceNativePath !== undefined) {
return widget.deviceNativePath;
}
return "";
}
readonly property string deviceNativePath: getBatteryDevicePath()
readonly property string deviceNativePath: resolveWidgetSetting("deviceNativePath", "__default__")
readonly property var selectedBattery: BatteryService.findUPowerDevice(deviceNativePath)
readonly property var selectedBluetoothDevice: deviceNativePath ? BatteryService.findBluetoothDevice(deviceNativePath) : null
readonly property var selectedBluetoothDevice: BatteryService.findBluetoothDevice(deviceNativePath)
readonly property var selectedDevice: {
var dev = selectedBluetoothDevice || selectedBattery;
if (BatteryService.isDevicePresent(dev))
return dev;
return allDevices.length > 0 ? allDevices[0] : null;
if (BatteryService.isDevicePresent(selectedBluetoothDevice)) {
return selectedBluetoothDevice;
}
if (BatteryService.isDevicePresent(selectedBattery)) {
return selectedBattery;
}
return BatteryService.primaryDevice;
}
// Check if selected device is actually present/connected
readonly property bool isDevicePresent: BatteryService.isDevicePresent(selectedDevice)
readonly property bool isPresent: BatteryService.isDevicePresent(selectedDevice)
readonly property bool isReady: BatteryService.isDeviceReady(selectedDevice)
readonly property int percent: isReady ? Math.round(BatteryService.getPercentage(selectedDevice)) : -1
readonly property bool isCharging: BatteryService.isCharging(selectedDevice)
readonly property bool isPluggedIn: BatteryService.isPluggedIn(selectedDevice)
readonly property bool isCharging: isReady ? BatteryService.isCharging(selectedDevice) : false
readonly property bool isPluggedIn: isReady ? BatteryService.isPluggedIn(selectedDevice) : false
readonly property bool healthAvailable: (isReady && selectedBattery && selectedBattery.healthSupported) || (selectedBattery && BatteryService.healthAvailable)
readonly property int healthPercent: (isReady && selectedBattery && selectedBattery.healthSupported) ? Math.round(selectedBattery.healthPercentage) : BatteryService.healthPercent
@@ -101,27 +94,6 @@ SmartPanel {
readonly property var laptopBatteries: allDevices.filter(d => !BatteryService.isBluetoothDevice(d))
readonly property var otherDevices: allDevices.filter(d => BatteryService.isBluetoothDevice(d))
readonly property string timeText: {
if (!isReady || !isDevicePresent) {
return I18n.tr("battery.no-battery-detected");
}
if (isPluggedIn) {
return I18n.tr("battery.plugged-in");
}
if (selectedDevice) {
if (selectedDevice.timeToFull > 0) {
return I18n.tr("battery.time-until-full", {
"time": Time.formatVagueHumanReadableDuration(selectedDevice.timeToFull)
});
}
if (selectedDevice.timeToEmpty > 0) {
return I18n.tr("battery.time-left", {
"time": Time.formatVagueHumanReadableDuration(selectedDevice.timeToEmpty)
});
}
}
return I18n.tr("common.idle");
}
readonly property string iconName: BatteryService.getIcon(percent, isCharging, isPluggedIn, isReady)
property var batteryWidgetInstance: BarService.lookupWidget("Battery", screen ? screen.name : null)
@@ -207,7 +179,7 @@ SmartPanel {
}
NText {
text: timeText
text: BatteryService.getTimeRemainingText(selectedDevice)
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
wrapMode: Text.Wrap
@@ -24,8 +24,6 @@ ColumnLayout {
property bool valueHideIfNotDetected: widgetData.hideIfNotDetected !== undefined ? widgetData.hideIfNotDetected : widgetMetadata.hideIfNotDetected
property bool valueHideIfIdle: widgetData.hideIfIdle !== undefined ? widgetData.hideIfIdle : widgetMetadata.hideIfIdle
property var deviceModel: BatteryService.devicesModel
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
if (widgetData && widgetData.id) {
@@ -55,7 +53,7 @@ ColumnLayout {
label: I18n.tr("bar.battery.device-label")
description: I18n.tr("bar.battery.device-description")
minimumWidth: 200
model: root.deviceModel
model: BatteryService.deviceModel
currentKey: root.valueDeviceNativePath
onSelected: key => {
root.valueDeviceNativePath = key;
@@ -63,18 +61,9 @@ ColumnLayout {
}
}
// Update currentKey when model changes to ensure selection is preserved
Connections {
target: root
function onDeviceModelChanged() {
// Force update of currentKey to trigger selection update
deviceComboBox.currentKey = root.valueDeviceNativePath;
}
}
NIconButton {
icon: "refresh"
tooltipText: I18n.tr("common.refresh")
tooltipText: I18n.tr("tooltips.refresh-devices")
onClicked: BatteryService.devicesModel = BatteryService.buildDeviceModel()
}
}
+45 -68
View File
@@ -26,8 +26,9 @@ Singleton {
property int healthPercent: -1
readonly property var _laptopBattery: {
if (!UPower.devices)
return UPower.displayDevice;
if (!UPower.devices) {
return UPower.displayDevice;
}
var devices = UPower.devices.values || [];
@@ -59,36 +60,15 @@ Singleton {
}
readonly property var _bluetoothBattery: {
if (externalBatteries.length > 0)
return externalBatteries[0];
return null;
}
// MARK: resolveDevice
function resolveDevice(nativePath) {
if (!nativePath || nativePath === "") {
return primaryDevice;
if (externalBatteries.length > 0) {
return externalBatteries[0];
}
// Check for DisplayDevice explicitly (Literal key OR actual native path)
if ((nativePath === "DisplayDevice" || (UPower.displayDevice && nativePath === UPower.displayDevice.nativePath)) && UPower.displayDevice) {
return UPower.displayDevice;
}
var upowerDev = findUPowerDevice(nativePath);
if (upowerDev)
return upowerDev;
var btDev = findBluetoothDevice(nativePath);
if (btDev)
return btDev;
return null;
}
// MARK: findUPowerDevice
function findUPowerDevice(nativePath) {
if (!nativePath || nativePath === "" || nativePath === "DisplayDevice") {
if (!nativePath || nativePath === "__default__" || nativePath === "DisplayDevice") {
return _laptopBattery;
}
@@ -134,8 +114,9 @@ Singleton {
// MARK: isDevicePresent
function isDevicePresent(device) {
if (!device)
if (!device) {
return false;
}
// Handle Bluetooth devices (identified by having batteryAvailable property)
if (device.batteryAvailable !== undefined) {
@@ -147,30 +128,28 @@ Singleton {
if (device.type === UPowerDeviceType.Battery && device.isPresent !== undefined) {
return device.isPresent === true;
}
// Fallback for non-battery UPower devices or if isPresent is missing
return device.ready && device.percentage !== undefined;
}
return false;
}
// MARK: isDeviceReady
function isDeviceReady(device) {
if (!isDevicePresent(device))
if (!isDevicePresent(device)) {
return false;
}
if (device.batteryAvailable !== undefined) {
return device.battery !== undefined;
}
return device.ready && device.percentage !== undefined;
}
// MARK: getPercentage
function getPercentage(device) {
if (!device)
return 0;
if (!device) {
return -1;
}
if (device.batteryAvailable !== undefined) {
return (device.battery || 0) * 100;
}
@@ -179,9 +158,10 @@ Singleton {
// MARK: isCharging
function isCharging(device) {
if (!device || isBluetoothDevice(device))
if (!device || isBluetoothDevice(device)) {
// Tracking bluetooth devices can charge or not is a loop hole, none of my devices has it, even if it possible?!
return false; // Assuming not charging until someone/quickshell brings a way to do pretty unlikely.
}
if (device.state !== undefined) {
return device.state === UPowerDeviceState.Charging;
}
@@ -190,9 +170,10 @@ Singleton {
// MARK: isPluggedIn
function isPluggedIn(device) {
if (!device || isBluetoothDevice(device))
if (!device || isBluetoothDevice(device)) {
// Tracking bluetooth devices can charge or not is a loop hole, none of my devices has it, even if it possible?!
return false; // Assuming not charging until someone/quickshell brings a way to do pretty unlikely.
}
if (device.state !== undefined) {
return device.state === UPowerDeviceState.FullyCharged || device.state === UPowerDeviceState.PendingCharge;
}
@@ -206,8 +187,9 @@ Singleton {
// MARK: getDeviceName
function getDeviceName(device) {
if (!isDeviceReady(device))
if (!isDeviceReady(device)) {
return "";
}
// Don't show name for laptop batteries
if (!isBluetoothDevice(device) && device.isLaptopBattery) {
@@ -245,9 +227,9 @@ Singleton {
stdout: SplitParser {
onRead: function (data) {
var line = data.trim();
if (line === "")
if (line === "") {
return;
}
var capacityMatch = line.match(/^\s*capacity:\s*(\d+(?:\.\d+)?)\s*%/i);
if (capacityMatch) {
root.healthPercent = Math.round(parseFloat(capacityMatch[1]));
@@ -300,21 +282,18 @@ Singleton {
// MARK: Battery
// MARK: getRateText
function getRateText(device) {
if (!device || device.changeRate === undefined)
if (!device || device.changeRate === undefined) {
return "";
}
const rate = Math.abs(device.changeRate);
if (isPluggedIn(device)) {
return I18n.tr("battery.plugged-in");
} else if (isCharging(device)) {
return I18n.tr("battery.charging-rate", {
"rate": rate.toFixed(2)
});
} else {
return I18n.tr("battery.discharging-rate", {
"rate": rate.toFixed(2)
});
} else if (device.timeToFull > 0) {
return I18n.tr("battery.charging-rate", {"rate": rate.toFixed(2)});
} else if (device.timeToEmpty > 0) {
return I18n.tr("battery.discharging-rate", {"rate": rate.toFixed(2)});
}
return I18n.tr("common.idle");
}
// MARK: BatteryPanel
@@ -337,34 +316,24 @@ Singleton {
}
if (isPluggedIn(device)) {
return I18n.tr("battery.plugged-in");
}
if (device) {
if (device.timeToFull > 0) {
return I18n.tr("battery.time-until-full", {
"time": Time.formatVagueHumanReadableDuration(device.timeToFull)
});
}
if (device.timeToEmpty > 0) {
return I18n.tr("battery.time-left", {
"time": Time.formatVagueHumanReadableDuration(device.timeToEmpty)
});
}
} else if (device.timeToFull > 0) {
return I18n.tr("battery.time-until-full", {"time": Time.formatVagueHumanReadableDuration(device.timeToFull)});
} else if (device.timeToEmpty > 0) {
return I18n.tr("battery.time-left", {"time": Time.formatVagueHumanReadableDuration(device.timeToEmpty)});
}
return I18n.tr("common.idle");
}
// MARK: BatterySettings
property var devicesModel: buildDeviceModel()
property var deviceModel: buildDeviceModel()
function buildDeviceModel() {
var model = [
{
"key": UPower.devices.DisplayDevice || "" // It was capital D and i spend an hour to figure out [why tf this do absolutely nothing] XD (I hate my left shift it sticks)
,
"key": "__default__",
"name": I18n.tr("bar.battery.device-default")
}
];
// UPower Devices
if (UPower.devices && UPower.devices.values) {
var deviceArray = UPower.devices.values;
@@ -392,8 +361,8 @@ Singleton {
onTriggered: {
var newModel = buildDeviceModel();
// Simple change detection to avoid unnecessary bindings updates
if (JSON.stringify(newModel) !== JSON.stringify(devicesModel)) {
devicesModel = newModel;
if (JSON.stringify(newModel) !== JSON.stringify(deviceModel)) {
deviceModel = newModel;
}
}
}
@@ -402,7 +371,15 @@ Singleton {
target: UPower.devices
function onValuesChanged() {
modelUpdateTimer.restart();
devicesModel = buildDeviceModel();
deviceModel = buildDeviceModel();
}
}
Connections {
target: BluetoothService
function onConnectedDevicesChanged() {
modelUpdateTimer.restart();
deviceModel = buildDeviceModel();
}
}
}