mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
287 lines
9.1 KiB
QML
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|