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.Networking import qs.Widgets SmartPanel { id: root preferredWidth: Math.round(360 * Style.uiScaleRatio) preferredHeight: Math.round(460 * Style.uiScaleRatio) // Get device selection from Battery widget settings (check right section first, then any Battery widget) function getBatteryDevicePath() { // Check right section first (most common location for Battery widget) var rightWidgets = Settings.data.bar.widgets.right || []; for (var i = 0; i < rightWidgets.length; i++) { if (rightWidgets[i].id === "Battery" && rightWidgets[i].deviceNativePath) { return rightWidgets[i].deviceNativePath; } } // Check other sections var sections = ["left", "center"]; for (var s = 0; s < sections.length; s++) { var widgets = Settings.data.bar.widgets[sections[s]] || []; for (var j = 0; j < widgets.length; j++) { if (widgets[j].id === "Battery" && widgets[j].deviceNativePath) { return widgets[j].deviceNativePath; } } } return ""; } // Helper function to find battery device by nativePath function findBatteryDevice(nativePath) { if (!nativePath || nativePath === "") { return UPower.displayDevice; } if (!UPower.devices) { return UPower.displayDevice; } var deviceArray = UPower.devices.values || []; for (var i = 0; i < deviceArray.length; i++) { var device = deviceArray[i]; if (device && device.nativePath === nativePath) { if (device.type === UPowerDeviceType.LinePower) { continue; } if (device.percentage !== undefined) { return device; } } } return UPower.displayDevice; } // Helper function to find Bluetooth device by MAC address from nativePath function findBluetoothDevice(nativePath) { if (!nativePath || !BluetoothService.devices) { return null; } var macMatch = nativePath.match(/([0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2})/); if (!macMatch) { return null; } var macAddress = macMatch[1].toUpperCase(); var deviceArray = BluetoothService.devices.values || []; for (var i = 0; i < deviceArray.length; i++) { var device = deviceArray[i]; if (device && device.address && device.address.toUpperCase() === macAddress) { return device; } } return null; } readonly property string deviceNativePath: getBatteryDevicePath() readonly property var battery: findBatteryDevice(deviceNativePath) readonly property var bluetoothDevice: deviceNativePath ? findBluetoothDevice(deviceNativePath) : null readonly property bool hasBluetoothBattery: bluetoothDevice && bluetoothDevice.batteryAvailable && bluetoothDevice.battery !== undefined readonly property bool isBluetoothConnected: bluetoothDevice && bluetoothDevice.connected !== undefined ? bluetoothDevice.connected : false // Check if device is actually present/connected readonly property bool isDevicePresent: { if (deviceNativePath && deviceNativePath !== "") { if (bluetoothDevice) { return isBluetoothConnected; } if (battery && battery.nativePath === deviceNativePath) { if (battery.type === UPowerDeviceType.Battery && battery.isPresent !== undefined) { return battery.isPresent; } return battery.ready && battery.percentage !== undefined && (battery.percentage > 0 || battery.state === UPowerDeviceState.Charging); } return false; } if (battery) { if (battery.type === UPowerDeviceType.Battery && battery.isPresent !== undefined) { return battery.isPresent; } return battery.ready && battery.percentage !== undefined; } return false; } readonly property bool isReady: battery && battery.ready && isDevicePresent && (battery.percentage !== undefined || hasBluetoothBattery) readonly property int percent: isReady ? Math.round(hasBluetoothBattery ? (bluetoothDevice.battery * 100) : (battery.percentage * 100)) : -1 readonly property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false readonly property bool healthAvailable: isReady && battery.healthSupported readonly property int healthPercent: healthAvailable ? Math.round(battery.healthPercentage) : -1 function getDeviceName() { if (!isReady) { return ""; } // Don't show name for laptop batteries if (battery && battery.isLaptopBattery) { return ""; } if (bluetoothDevice && bluetoothDevice.name) { return bluetoothDevice.name; } if (battery && battery.model) { return battery.model; } return ""; } readonly property string deviceName: getDeviceName() readonly property string panelTitle: deviceName ? `${I18n.tr("battery.panel-title")} - ${deviceName}` : I18n.tr("battery.panel-title") readonly property string timeText: { if (!isReady || !isDevicePresent) 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) 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: root.charging ? Color.mPrimary : Color.mOnSurface icon: iconName } ColumnLayout { spacing: Style.marginXXS Layout.fillWidth: true NText { text: root.panelTitle pointSize: Style.fontSizeL font.weight: Style.fontWeightBold color: Color.mOnSurface Layout.fillWidth: true elide: Text.ElideRight } 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 height: chargeLayout.implicitHeight + Style.marginL * 2 visible: isReady ColumnLayout { id: chargeLayout anchors.fill: parent anchors.margins: Style.marginL spacing: Style.marginS RowLayout { Layout.fillWidth: true spacing: Style.marginS ColumnLayout { NText { text: I18n.tr("battery.battery-level") color: Color.mOnSurface pointSize: Style.fontSizeS } 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 } } } NText { text: percent >= 0 ? `${percent}%` : "--" color: Color.mOnSurface pointSize: Style.fontSizeS font.weight: Style.fontWeightBold } } 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 } } } } } } }