import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Services.UPower import qs.Commons import qs.Modules.Bar.Extras import qs.Services.Hardware import qs.Services.Networking import qs.Services.UI import qs.Widgets Item { id: root property ShellScreen screen // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section]; if (widgets && sectionWidgetIndex < widgets.length) { return widgets[sectionWidgetIndex]; } } return {}; } readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode readonly property real warningThreshold: widgetSettings.warningThreshold !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold // Only show low battery warning if device is ready (prevents false positive during initialization) readonly property bool isLowBattery: isReady && !charging && percent <= warningThreshold // Test mode readonly property bool testMode: false readonly property int testPercent: 35 readonly property bool testCharging: false readonly property string deviceNativePath: widgetSettings.deviceNativePath || "" function findBatteryDevice(nativePath) { if (!nativePath || !UPower.devices) { return UPower.displayDevice; } var devices = UPower.devices.values || []; for (var i = 0; i < devices.length; i++) { var device = devices[i]; if (device && device.nativePath === nativePath && device.type !== UPowerDeviceType.LinePower && device.percentage !== undefined) { return device; } } return UPower.displayDevice; } 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 devices = BluetoothService.devices.values || []; for (var i = 0; i < devices.length; i++) { var device = devices[i]; if (device && device.address && device.address.toUpperCase() === macAddress) { return device; } } return null; } 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 === true property bool initializationComplete: false Timer { interval: 500 running: true onTriggered: root.initializationComplete = true } readonly property bool isDevicePresent: { if (testMode) return true; if (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) { return (battery.type === UPowerDeviceType.Battery && battery.isPresent !== undefined) ? battery.isPresent : (battery.ready && battery.percentage !== undefined); } return false; } readonly property bool isReady: testMode ? true : (initializationComplete && battery && battery.ready && isDevicePresent && (battery.percentage !== undefined || hasBluetoothBattery)) readonly property real percent: testMode ? testPercent : (isReady ? (hasBluetoothBattery ? (bluetoothDevice.battery * 100) : (battery.percentage * 100)) : 0) readonly property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) property bool hasNotifiedLowBattery: false implicitWidth: pill.width implicitHeight: pill.height function maybeNotify(currentPercent, isCharging) { if (!isCharging && !hasNotifiedLowBattery && currentPercent <= warningThreshold) { hasNotifiedLowBattery = true; ToastService.showWarning(I18n.tr("toast.battery.low"), I18n.tr("toast.battery.low-desc", { "percent": Math.round(currentPercent) })); } else if (hasNotifiedLowBattery && (isCharging || currentPercent > warningThreshold + 5)) { hasNotifiedLowBattery = false; } } function getCurrentPercent() { return hasBluetoothBattery ? (bluetoothDevice.battery * 100) : (battery ? battery.percentage * 100 : 0); } Connections { target: battery function onPercentageChanged() { if (battery) { maybeNotify(getCurrentPercent(), battery.state === UPowerDeviceState.Charging); } } function onStateChanged() { if (battery) { if (battery.state === UPowerDeviceState.Charging) { hasNotifiedLowBattery = false; } maybeNotify(getCurrentPercent(), battery.state === UPowerDeviceState.Charging); } } } Connections { target: bluetoothDevice function onBatteryChanged() { if (bluetoothDevice && hasBluetoothBattery) { maybeNotify(bluetoothDevice.battery * 100, battery ? battery.state === UPowerDeviceState.Charging : false); } } } NPopupContextMenu { id: contextMenu model: [ { "label": I18n.tr("context-menu.widget-settings"), "action": "widget-settings", "icon": "settings" }, ] onTriggered: action => { var popupMenuWindow = PanelService.getPopupMenuWindow(screen); if (popupMenuWindow) { popupMenuWindow.close(); } if (action === "widget-settings") { BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); } } } BarPill { id: pill screen: root.screen density: Settings.data.bar.density oppositeDirection: BarService.getPillDirection(root) icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady) text: (isReady || testMode) ? Math.round(percent) : "-" suffix: "%" autoHide: false forceOpen: isReady && displayMode === "alwaysShow" forceClose: displayMode === "alwaysHide" || (initializationComplete && !isReady) customBackgroundColor: !initializationComplete ? Color.transparent : (charging ? Color.mPrimary : (isLowBattery ? Color.mError : Color.transparent)) customTextIconColor: !initializationComplete ? Color.transparent : (charging ? Color.mOnPrimary : (isLowBattery ? Color.mOnError : Color.transparent)) tooltipText: { let lines = []; if (testMode) { lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(12345)}.`); return lines.join("\n"); } if (!isReady || !isDevicePresent) { return I18n.tr("battery.no-battery-detected"); } if (battery.timeToEmpty > 0) { lines.push(I18n.tr("battery.time-left", { "time": Time.formatVagueHumanReadableDuration(battery.timeToEmpty) })); } if (battery.timeToFull > 0) { lines.push(I18n.tr("battery.time-until-full", { "time": Time.formatVagueHumanReadableDuration(battery.timeToFull) })); } if (battery.changeRate !== undefined) { const rate = battery.changeRate; if (rate > 0) { lines.push(charging ? I18n.tr("battery.charging-rate", { "rate": rate.toFixed(2) }) : I18n.tr("battery.discharging-rate", { "rate": rate.toFixed(2) })); } else if (rate < 0) { lines.push(I18n.tr("battery.discharging-rate", { "rate": Math.abs(rate).toFixed(2) })); } else { // Rate is 0 - check if plugged in (charging state) or idle lines.push(charging ? I18n.tr("battery.plugged-in") : I18n.tr("battery.idle")); } } else { lines.push(charging ? I18n.tr("battery.charging") : I18n.tr("battery.discharging")); } if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) { lines.push(I18n.tr("battery.health", { "percent": Math.round(battery.healthPercentage) })); } return lines.join("\n"); } onClicked: PanelService.getPanel("batteryPanel", screen)?.toggle(this) onRightClicked: { var popupMenuWindow = PanelService.getPopupMenuWindow(screen); if (popupMenuWindow) { popupMenuWindow.showContextMenu(contextMenu); const pos = BarService.getContextMenuPosition(pill, contextMenu.implicitWidth, contextMenu.implicitHeight); contextMenu.openAtItem(pill, pos.x, pos.y); } } } }