Files
noctalia-shell/Modules/Panels/Battery/BatteryPanel.qml
T

287 lines
9.1 KiB
QML

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: Math.min(Style.radiusL, 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
}
}
}
}
}
}
}