diff --git a/quickshell/Modules/Settings/BatteryTab.qml b/quickshell/Modules/Settings/BatteryTab.qml index 415a44961..277afa7f2 100644 --- a/quickshell/Modules/Settings/BatteryTab.qml +++ b/quickshell/Modules/Settings/BatteryTab.qml @@ -8,6 +8,14 @@ import qs.Modules.Settings.Widgets Item { id: root + readonly property color batteryStatusColor: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) + return Theme.error; + if (BatteryService.isCharging || BatteryService.isPluggedIn) + return Theme.primary; + return Theme.surfaceText; + } + Process { id: applyLimitProcess command: ["pkexec", "sh", "-c", " @@ -47,8 +55,6 @@ done // 1. Information Card SettingsCard { width: parent.width - iconName: "battery_charging_full" - title: I18n.tr("Status") settingKey: "batteryStatusCard" tags: ["battery", "status", "charge", "health"] @@ -57,108 +63,131 @@ done x: Theme.spacingM spacing: Theme.spacingM - SettingsDivider {} - Row { width: parent.width - StyledText { - text: I18n.tr("Power source") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - width: parent.width / 2 - horizontalAlignment: Text.AlignLeft - } - StyledText { - text: BatteryService.isPluggedIn ? I18n.tr("AC Adapter (Plugged In)") : I18n.tr("Battery Power") - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width / 2 - horizontalAlignment: Text.AlignLeft - } - } + spacing: Theme.spacingM - SettingsDivider {} - - Row { - width: parent.width - StyledText { - text: I18n.tr("Charge Level") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - width: parent.width / 2 - horizontalAlignment: Text.AlignLeft + DankIcon { + name: BatteryService.getBatteryIcon() + size: Theme.iconSizeLarge + color: root.batteryStatusColor + anchors.verticalCenter: parent.verticalCenter } - StyledText { - text: `${BatteryService.batteryLevel}%` - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width / 2 - horizontalAlignment: Text.AlignLeft - } - } - SettingsDivider {} - - Row { - width: parent.width - StyledText { - text: I18n.tr("Status") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - width: parent.width / 2 - horizontalAlignment: Text.AlignLeft - } - StyledText { - text: BatteryService.batteryStatus - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width / 2 - horizontalAlignment: Text.AlignLeft + Column { + spacing: Theme.spacingXS + width: parent.width - Theme.iconSizeLarge - Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + + Row { + spacing: Theme.spacingS + width: parent.width + + StyledText { + text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : I18n.tr("Power") + font.pixelSize: Theme.fontSizeXLarge + font.weight: Font.Bold + color: root.batteryStatusColor + } + + StyledText { + text: BatteryService.batteryAvailable ? BatteryService.batteryStatus : I18n.tr("Management") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + width: Math.max(0, parent.width - 100) + } + } + + StyledText { + text: BatteryService.isPluggedIn ? I18n.tr("Plugged In (AC)") : I18n.tr("Battery Power") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + width: parent.width + elide: Text.ElideRight + } } } - SettingsDivider {} - - Row { + Item { width: parent.width - StyledText { - text: I18n.tr("Estimated Time") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - width: parent.width / 2 - horizontalAlignment: Text.AlignLeft + height: 4 + + Rectangle { + anchors.fill: parent + radius: height / 2 + color: Theme.withAlpha(Theme.primary, 0.16) } - StyledText { - text: BatteryService.formatTimeRemaining() - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width / 2 - horizontalAlignment: Text.AlignLeft + + Rectangle { + width: parent.width * Math.max(0, Math.min(1, BatteryService.batteryLevel / 100)) + height: parent.height + radius: height / 2 + color: root.batteryStatusColor + visible: BatteryService.batteryAvailable } } - SettingsDivider {} - Row { width: parent.width - StyledText { - text: I18n.tr("Battery Health") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - width: parent.width / 2 - horizontalAlignment: Text.AlignLeft + spacing: Theme.spacingM + + Item { + width: (parent.width - Theme.spacingM) / 2 + height: timeColumn.implicitHeight + + Column { + id: timeColumn + width: parent.width + spacing: Theme.spacingXXS + + StyledText { + text: I18n.tr("Estimated time") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + + StyledText { + text: { + const remaining = BatteryService.formatTimeRemaining(); + const estimated = BatteryService.formatEstimatedTime(); + return estimated ? `${remaining} (${estimated})` : remaining; + } + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + elide: Text.ElideRight + } + } } - StyledText { - text: BatteryService.batteryHealth - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width / 2 - horizontalAlignment: Text.AlignLeft + + Item { + width: (parent.width - Theme.spacingM) / 2 + height: healthColumn.implicitHeight + + Column { + id: healthColumn + width: parent.width + spacing: Theme.spacingXXS + + StyledText { + text: I18n.tr("Battery health") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + + StyledText { + text: BatteryService.batteryHealth + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + elide: Text.ElideRight + } + } } } } diff --git a/quickshell/Services/BatteryService.qml b/quickshell/Services/BatteryService.qml index 1f0d98030..a142b4447 100644 --- a/quickshell/Services/BatteryService.qml +++ b/quickshell/Services/BatteryService.qml @@ -405,23 +405,40 @@ Singleton { return btDevices; } - // Format time remaining for charge/discharge - function formatTimeRemaining() { + function estimatedSeconds() { if (!batteryAvailable) { - return "Unknown"; + return 0; } const rate = _smoothedChangeRate > 0 ? _smoothedChangeRate : changeRate; const totalTime = (isCharging) ? ((batteryCapacity - batteryEnergy) / rate) : (batteryEnergy / rate); - const avgTime = Math.abs(totalTime * 3600); - if (!avgTime || avgTime <= 0 || avgTime > 86400) + const seconds = Math.abs(totalTime * 3600); + if (!seconds || seconds <= 0 || seconds > 86400) + return 0; + return seconds; + } + + // Format time remaining for charge/discharge + function formatTimeRemaining() { + const seconds = estimatedSeconds(); + if (!seconds) return "Unknown"; - const hours = Math.floor(avgTime / 3600); - const minutes = Math.floor((avgTime % 3600) / 60); + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; } + function formatEstimatedTime() { + const seconds = estimatedSeconds(); + if (!seconds) + return ""; + + const target = new Date(Date.now() + seconds * 1000); + const use24Hour = SettingsData.use24HourClock !== false; + return target.toLocaleTimeString(Qt.locale(), use24Hour ? "HH:mm" : "h:mm AP"); + } + function getBatteryIcon() { if (!batteryAvailable) { return "power"; diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 4be4b577f..8f28107b0 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -11141,6 +11141,26 @@ ], "description": "Show a notification when battery reaches the charge limit." }, + { + "section": "batteryStatusCard", + "label": "Power", + "tabIndex": 42, + "category": "Power & Security", + "keywords": [ + "battery", + "charge", + "health", + "hibernate", + "power", + "reboot", + "restart", + "security", + "shutdown", + "sleep", + "status", + "suspend" + ] + }, { "section": "_tab_42", "label": "Power & Security", @@ -11233,21 +11253,6 @@ "icon": "tune", "description": "Limit the maximum battery charge level to extend lifespan." }, - { - "section": "batteryStatusCard", - "label": "Status", - "tabIndex": 42, - "category": "Power & Security", - "keywords": [ - "battery", - "charge", - "health", - "power", - "security", - "status" - ], - "icon": "battery_charging_full" - }, { "section": "_tab_43", "label": "Widgets & Notifications",