freebsd: gate audio and battery polling on consumer refs instead of popout bindings

This commit is contained in:
bbedward
2026-10-05 22:23:34 -04:00
parent ff50b492b1
commit 4265ea1620
10 changed files with 182 additions and 191 deletions
@@ -16,6 +16,9 @@ FocusScope {
required property var host
property bool live: Window.window?.visible ?? false
property bool audioRefHeld: false
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
@@ -75,7 +78,23 @@ FocusScope {
implicitHeight: targetImplicitHeight
focus: true
Component.onCompleted: WidgetUtils.ensureEditButton()
function syncAudioRef(wanted) {
if (wanted === audioRefHeld)
return;
audioRefHeld = wanted;
if (wanted) {
AudioService.addRef();
return;
}
AudioService.removeRef();
}
onLiveChanged: syncAudioRef(live)
Component.onCompleted: {
WidgetUtils.ensureEditButton();
syncAudioRef(live);
}
Component.onDestruction: syncAudioRef(false)
onPlacedWidgetIdsChanged: WidgetUtils.ensureEditButton()
function navigateTo(section) {
@@ -7,6 +7,8 @@ import qs.Widgets
CcTile {
id: root
property bool live: Window.window?.visible ?? false
property bool batteryRefHeld: false
readonly property bool available: BatteryService.batteryAvailable
readonly property bool profileMode: !available && PowerProfileWatcher.available
@@ -28,6 +30,21 @@ CcTile {
return `${BatteryService.batteryLevel}%`;
}
dockedText: available || profileMode ? subtitle : title
function syncBatteryRef(wanted) {
if (wanted === batteryRefHeld)
return;
batteryRefHeld = wanted;
if (wanted) {
BatteryService.addRef();
return;
}
BatteryService.removeRef();
}
onLiveChanged: syncBatteryRef(live)
Component.onCompleted: syncBatteryRef(live)
Component.onDestruction: syncBatteryRef(false)
active: available && (BatteryService.isCharging || BatteryService.isPluggedIn)
opensPage: !profileMode
showExpand: profileMode
@@ -13,6 +13,7 @@ Item {
id: root
property bool active: false
property bool batteryRefHeld: false
property real contentPadding: PopoutMetrics.contentPadding
readonly property var historyDevice: BatteryService.usePreferred ? BatteryService.preferredDevice : BatteryService.device
readonly property bool multipleBatteries: !BatteryService.usePreferred && BatteryService.batteries.length > 1
@@ -72,10 +73,24 @@ Item {
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
function syncBatteryRef(wanted) {
if (wanted === batteryRefHeld)
return;
batteryRefHeld = wanted;
if (wanted) {
BatteryService.addRef();
return;
}
BatteryService.removeRef();
}
onActiveChanged: {
syncBatteryRef(active);
if (active)
focusTimer.restart();
}
Component.onCompleted: syncBatteryRef(active)
Component.onDestruction: syncBatteryRef(false)
Keys.onEscapePressed: event => {
dismissRequested();
event.accepted = true;
@@ -9,6 +9,9 @@ BasePill {
id: battery
readonly property var log: Log.scoped("Battery")
Component.onCompleted: BatteryService.addRef()
Component.onDestruction: BatteryService.removeRef()
property bool batteryPopupVisible: false
property var popoutTarget: null
property var widgetData: null
@@ -14,6 +14,7 @@ Item {
readonly property real stableLength: MprisController.activePlayerStableLength
property var allPlayers: MprisController.availablePlayers
property bool live: Window.window?.visible ?? false
property bool audioRefHeld: false
property string entryId: "media"
readonly property var options: DashRegistry.resolvedOptions(entryId)
readonly property string playerStyle: options?.playerStyle ?? MediaOptions.defaultPlayerStyle
@@ -125,7 +126,22 @@ Item {
onTriggered: root.activePlayer?.positionChanged()
}
function syncAudioRef(wanted) {
if (wanted === audioRefHeld)
return;
audioRefHeld = wanted;
if (wanted) {
AudioService.addRef();
return;
}
AudioService.removeRef();
}
Component.onCompleted: syncAudioRef(live)
Component.onDestruction: syncAudioRef(false)
onLiveChanged: {
syncAudioRef(live);
if (live) {
restoreLyrics();
return;
@@ -9,6 +9,7 @@ Card {
id: root
property bool live: Window.window?.visible ?? false
property bool batteryRefHeld: false
readonly property bool wide: width >= DashMetrics.gridRowUnit * 2
readonly property bool tall: height >= DashMetrics.gridRowUnit * 2
@@ -20,6 +21,21 @@ Card {
entryId: "battery"
tone: options.tone ?? ""
function syncBatteryRef(wanted) {
if (wanted === batteryRefHeld)
return;
batteryRefHeld = wanted;
if (wanted) {
BatteryService.addRef();
return;
}
BatteryService.removeRef();
}
onLiveChanged: syncBatteryRef(live)
Component.onCompleted: syncBatteryRef(live)
Component.onDestruction: syncBatteryRef(false)
Row {
anchors.left: parent.left
anchors.right: parent.right
+2
View File
@@ -85,10 +85,12 @@ Item {
}
Component.onCompleted: {
AudioService.addRef();
hiddenOutputDeviceNames = SessionData.hiddenOutputDeviceNames ?? [];
hiddenInputDeviceNames = SessionData.hiddenInputDeviceNames ?? [];
updateDeviceList();
}
Component.onDestruction: AudioService.removeRef()
Connections {
target: AudioService
@@ -26,6 +26,9 @@ Item {
property bool chargeLimitSupported: Qt.platform.os === "linux"
Component.onCompleted: BatteryService.addRef()
Component.onDestruction: BatteryService.removeRef()
Process {
id: thresholdProbe
running: Qt.platform.os === "linux"
+49 -47
View File
@@ -31,18 +31,22 @@ Singleton {
readonly property bool pipewireReady: pipewireBackend?.ready ?? false
readonly property var pipewireNodes: pipewireBackend?.nodes ?? []
readonly property var allNodes: freebsdAudio ? nativeSinks.concat(nativeSources) : pipewireNodes
readonly property var sink: freebsdAudio ? nativeSink : (pipewireBackend?.defaultSink ?? null)
readonly property var source: freebsdAudio ? nativeSource : (pipewireBackend?.defaultSource ?? null)
readonly property var sink: freebsdAudio ? nativeSink : (pipewireBackend?.defaultSink ?? null)
readonly property var source: freebsdAudio ? nativeSource : (pipewireBackend?.defaultSource ?? null)
readonly property bool freeBsdPollingRequested:
(PopoutService.controlCenterPopout?.shouldBeVisible ?? false)
|| (PopoutService.dankDashPopout?.shouldBeVisible ?? false)
|| (PopoutService.settingsModal?.visible ?? false)
property int refCount: 0
function isPipewireVideoSource(node) {
return !freebsdAudio
&& (pipewireBackend?.isVideoSource(node) ?? false);
}
function addRef() {
refCount++;
}
function removeRef() {
refCount = Math.max(0, refCount - 1);
}
function isPipewireVideoSource(node) {
return !freebsdAudio && (pipewireBackend?.isVideoSource(node) ?? false);
}
Loader {
id: pipewireBackendLoader
@@ -79,11 +83,11 @@ Singleton {
property bool isSink: true
property bool isStream: false
property var properties: ({
"media.class": isSink ? "Audio/Sink" : "Audio/Source",
"device.api": "oss",
"device.name": pcmName,
"device.description": description
})
"media.class": isSink ? "Audio/Sink" : "Audio/Source",
"device.api": "oss",
"device.name": pcmName,
"device.description": description
})
property QtObject audio: QtObject {
property real volume: 0.0
property bool muted: false
@@ -275,7 +279,8 @@ Singleton {
function refreshSinkPorts(callback) {
if (freebsdAudio) {
sinkPorts = ({});
if (callback) callback();
if (callback)
callback();
return;
}
// ensure that parsed labels are in English
@@ -297,7 +302,8 @@ Singleton {
function setSinkPort(sinkName, portName, callback) {
if (freebsdAudio) {
if (callback) callback(false, I18n.tr("Port switching is not available with the FreeBSD native audio backend"));
if (callback)
callback(false, I18n.tr("Port switching is not available with the FreeBSD native audio backend"));
return;
}
Proc.runCommand("audio-set-sink-port", ["env", "LC_ALL=C", "pactl", "set-sink-port", sinkName, portName], (output, exitCode) => {
@@ -384,7 +390,8 @@ Singleton {
function refreshCards(callback) {
if (freebsdAudio) {
cards = [];
if (callback) callback();
if (callback)
callback();
return;
}
Proc.runCommand("audio-list-cards", ["env", "LC_ALL=C", "pactl", "list", "cards"], (output, exitCode) => {
@@ -534,7 +541,6 @@ Singleton {
return profile.split("+").filter(part => part.startsWith("output:")).map(part => prefix + part.substring(7));
}
function collectSwitchableOutputPorts(cards, hiddenSinkNames) {
const entries = [];
for (const card of cards || []) {
@@ -576,7 +582,8 @@ Singleton {
function activateOutputPort(entry, callback) {
if (freebsdAudio) {
if (callback) callback(false, I18n.tr("Output profile switching is not available with the FreeBSD native audio backend"));
if (callback)
callback(false, I18n.tr("Output profile switching is not available with the FreeBSD native audio backend"));
return;
}
if (!entry?.cardName || !entry.profile)
@@ -966,7 +973,6 @@ EOFCONFIG
}, 0);
}
function selectSoundTheme(themeName) {
if (!themeName) {
SettingsData.set("useSystemSoundTheme", false);
@@ -988,7 +994,6 @@ EOFCONFIG
}, 0);
}
function refreshSoundThemes() {
if (!soundThemeSupported)
return;
@@ -1207,7 +1212,6 @@ EOFCONFIG
return audio.muted || audio.volume === 0;
}
function volumeIcon(volume, muted) {
if (muted)
return "volume_off";
@@ -1381,7 +1385,6 @@ EOFCONFIG
return "";
}
function detectAudioBackend() {
Proc.runCommand("audio-detect-os", ["uname", "-s"], (output, exitCode) => {
const osName = exitCode === 0 ? (output || "").trim() : "";
@@ -1538,7 +1541,10 @@ EOFCONFIG
const percent = Math.max(0, Math.min(100, Math.round(volume * 100)));
const key = node.mixerDevice + "|" + node.mixerControl;
const writes = Object.assign({}, pendingFreeBsdVolumeWrites);
writes[key] = { node: node, percent: percent };
writes[key] = {
node: node,
percent: percent
};
pendingFreeBsdVolumeWrites = writes;
lastFreeBsdLocalChangeMs = Date.now();
freeBsdVolumeWriteTimer.restart();
@@ -1550,12 +1556,10 @@ EOFCONFIG
for (const key of Object.keys(writes)) {
const entry = writes[key];
const node = entry.node;
Proc.runCommand("audio-freebsd-volume-" + node.pcmName + "-" + node.mixerControl,
["mixer", "-f", node.mixerDevice, node.mixerControl + ".volume=" + entry.percent + "%"],
(output, exitCode) => {
if (exitCode !== 0)
log.error("FreeBSD mixer volume update failed:", output);
}, 0);
Proc.runCommand("audio-freebsd-volume-" + node.pcmName + "-" + node.mixerControl, ["mixer", "-f", node.mixerDevice, node.mixerControl + ".volume=" + entry.percent + "%"], (output, exitCode) => {
if (exitCode !== 0)
log.error("FreeBSD mixer volume update failed:", output);
}, 0);
}
}
@@ -1563,12 +1567,10 @@ EOFCONFIG
if (!node)
return;
lastFreeBsdLocalChangeMs = Date.now();
Proc.runCommand("audio-freebsd-mute-" + node.pcmName + "-" + node.mixerControl,
["mixer", "-f", node.mixerDevice, node.mixerControl + ".mute=" + (muted ? "1" : "0")],
(output, exitCode) => {
if (exitCode !== 0)
log.error("FreeBSD mixer mute update failed:", output);
}, 0);
Proc.runCommand("audio-freebsd-mute-" + node.pcmName + "-" + node.mixerControl, ["mixer", "-f", node.mixerDevice, node.mixerControl + ".mute=" + (muted ? "1" : "0")], (output, exitCode) => {
if (exitCode !== 0)
log.error("FreeBSD mixer mute update failed:", output);
}, 0);
}
function setFreeBsdDefaultDevice(node) {
@@ -1768,17 +1770,17 @@ EOFCONFIG
return `Microphone volume decreased to ${newVolume}%`;
}
onFreeBsdPollingRequestedChanged: {
if (root.freebsdAudio && root.freeBsdPollingRequested)
root.refreshFreeBsdDevices();
}
onRefCountChanged: {
if (root.refCount === 1)
root.refreshFreeBsdDevices();
}
Timer {
interval: 5000
repeat: true
running: root.freebsdAudio && root.freeBsdPollingRequested
onTriggered: root.refreshFreeBsdDevices()
}
Timer {
interval: 5000
repeat: true
running: root.freebsdAudio && root.refCount > 0
onTriggered: root.refreshFreeBsdDevices()
}
IpcHandler {
target: "audio"
+41 -143
View File
@@ -12,9 +12,6 @@ Singleton {
property bool suppressSound: true
property bool previousPluggedState: false
// Consumers such as the battery widget and battery-related popouts hold a
// reference while they need fresh battery information. This keeps the
// service independent from UI singletons and avoids creation-time cycles.
property int refCount: 0
function addRef() {
@@ -100,9 +97,7 @@ Singleton {
const acMatch = text.match(/__DMS_AC__:(\d+)/);
root.freebsdBatteryAvailable = blocks.length > 0;
root.freebsdBatteryLevel = blocks.length > 0
? Math.max(0, Math.min(100, weight > 0 ? weightedPct / weight : (pctCount > 0 ? pctSum / pctCount : 0)))
: 0;
root.freebsdBatteryLevel = blocks.length > 0 ? Math.max(0, Math.min(100, weight > 0 ? weightedPct / weight : (pctCount > 0 ? pctSum / pctCount : 0))) : 0;
root.freebsdIsCharging = charging;
root.freebsdPluggedIn = acMatch ? acMatch[1] === "1" : charging;
root.freebsdChangeRate = totalRateW;
@@ -117,17 +112,14 @@ Singleton {
const script = 'units=$(sysctl -n hw.acpi.battery.units 2>/dev/null || echo 0); i=0; while [ "$i" -lt "$units" ]; do echo "__DMS_BATTERY_${i}__"; acpiconf -i "$i" 2>/dev/null; i=$((i+1)); done; printf "__DMS_AC__:"; sysctl -n hw.acpi.acline 2>/dev/null || echo 1';
Proc.runCommand("battery-freebsd-acpi",
["nice", "-n", "10", "sh", "-c", script],
(output, exitCode) => {
root.freebsdBatteryProbeComplete = true;
Proc.runCommand("battery-freebsd-acpi", ["nice", "-n", "10", "sh", "-c", script], (output, exitCode) => {
root.freebsdBatteryProbeComplete = true;
if (exitCode === 0)
root.applyFreeBsdBatteryState(output);
else
root.freebsdBatteryAvailable = false;
},
0);
if (exitCode === 0)
root.applyFreeBsdBatteryState(output);
else
root.freebsdBatteryAvailable = false;
}, 0);
}
function updateFreeBsdBatteryFallback() {
@@ -154,10 +146,7 @@ Singleton {
Timer {
interval: 15000
repeat: true
running: root.isBSD
&& root.batteries.length === 0
&& root.freebsdBatteryAvailable
&& root.refCount > 0
running: root.isBSD && root.batteries.length === 0 && root.freebsdBatteryAvailable && root.refCount > 0
onTriggered: root.refreshFreeBsdBattery()
}
@@ -186,9 +175,7 @@ Singleton {
if (!batteryAvailable)
return;
const profileValue = isPluggedIn
? SettingsData.acProfileName
: SettingsData.batteryProfileName;
const profileValue = isPluggedIn ? SettingsData.acProfileName : SettingsData.batteryProfileName;
if (profileValue === "")
return;
@@ -266,10 +253,7 @@ Singleton {
return Math.min(100, Math.round((energy * 100) / cap));
}
return Math.min(100, Math.round(
chargeBatteries.reduce((sum, b) => sum + _chargePercent(b), 0)
/ chargeBatteries.length
));
return Math.min(100, Math.round(chargeBatteries.reduce((sum, b) => sum + _chargePercent(b), 0) / chargeBatteries.length));
}
if (_hasUsableCharge(UPower.displayDevice))
@@ -370,14 +354,10 @@ Singleton {
return live > 0 ? live : _lastBatteryLevel;
}
readonly property bool isCharging: (isBSD && freebsdBatteryAvailable)
? freebsdIsCharging
: (_hasKnownChargingState ? _currentIsCharging : _lastIsCharging)
readonly property bool isCharging: (isBSD && freebsdBatteryAvailable) ? freebsdIsCharging : (_hasKnownChargingState ? _currentIsCharging : _lastIsCharging)
// Is the system plugged in (Is not running on battery)
readonly property bool isPluggedIn: (isBSD && freebsdBatteryAvailable)
? freebsdPluggedIn
: !UPower.onBattery
readonly property bool isPluggedIn: (isBSD && freebsdBatteryAvailable) ? freebsdPluggedIn : !UPower.onBattery
readonly property bool hasBatteryReading: batteryAvailable && batteryLevel > 0
readonly property bool isLowBattery: hasBatteryReading && batteryLevel <= SettingsData.batteryLowThreshold
@@ -431,21 +411,9 @@ Singleton {
// urgency: "critical" (red), "warning" (orange/important), or "info"
function sendAlert(title, message, urgency, icon, notificationType) {
if (notificationType === 1) {
const dbusUrgency = urgency === "critical"
? "critical"
: (urgency === "warning" ? "normal" : "low");
const dbusUrgency = urgency === "critical" ? "critical" : (urgency === "warning" ? "normal" : "low");
Quickshell.execDetached([
"notify-send",
"-u",
dbusUrgency,
"-a",
"DMS",
"-i",
icon,
title,
message
]);
Quickshell.execDetached(["notify-send", "-u", dbusUrgency, "-a", "DMS", "-i", icon, title, message]);
} else if (urgency === "critical") {
ToastService.showError(title, message, "", icon);
} else if (urgency === "warning") {
@@ -463,16 +431,7 @@ Singleton {
if (!_hasNotifiedChargeLimit && SettingsData.batteryNotifyChargeLimit) {
_hasNotifiedChargeLimit = true;
sendAlert(
I18n.tr("Charge Limit Reached"),
I18n.tr(
"Battery has charged to your set limit of %1%",
"charge limit notification body, %1 is the limit percentage"
).arg(SettingsData.batteryChargeLimit),
"info",
"material:battery_profile",
SettingsData.batteryChargeLimitNotificationType
);
sendAlert(I18n.tr("Charge Limit Reached"), I18n.tr("Battery has charged to your set limit of %1%", "charge limit notification body, %1 is the limit percentage").arg(SettingsData.batteryChargeLimit), "info", "material:battery_profile", SettingsData.batteryChargeLimitNotificationType);
}
} else if (!isCharging || batteryLevel < SettingsData.batteryChargeLimit - 2) {
_hasNotifiedChargeLimit = false;
@@ -489,16 +448,7 @@ Singleton {
if (!_hasNotifiedCriticalBattery && SettingsData.batteryNotifyCritical) {
_hasNotifiedCriticalBattery = true;
sendAlert(
I18n.tr("Critical Battery"),
I18n.tr(
"Battery is at %1% - Connect charger immediately!",
"critical battery notification body, %1 is the battery percentage"
).arg(batteryLevel),
"critical",
"material:battery_alert",
SettingsData.batteryCriticalNotificationType
);
sendAlert(I18n.tr("Critical Battery"), I18n.tr("Battery is at %1% - Connect charger immediately!", "critical battery notification body, %1 is the battery percentage").arg(batteryLevel), "critical", "material:battery_alert", SettingsData.batteryCriticalNotificationType);
}
return;
@@ -512,16 +462,7 @@ Singleton {
if (!_hasNotifiedLowBattery && SettingsData.batteryNotifyLow) {
_hasNotifiedLowBattery = true;
sendAlert(
I18n.tr("Low Battery"),
I18n.tr(
"Battery is at %1% - Consider charging soon",
"low battery notification body, %1 is the battery percentage"
).arg(batteryLevel),
"warning",
"material:battery_0_bar",
SettingsData.batteryLowNotificationType
);
sendAlert(I18n.tr("Low Battery"), I18n.tr("Battery is at %1% - Consider charging soon", "low battery notification body, %1 is the battery percentage").arg(batteryLevel), "warning", "material:battery_0_bar", SettingsData.batteryLowNotificationType);
}
if (SettingsData.batteryAutoPowerSaver && PowerProfileWatcher.available) {
@@ -536,13 +477,9 @@ Singleton {
onIsChargingChanged: {
// Reset average when switching states
_smoothedChangeRate = (_hasKnownChargingState && changeRate > 0)
? changeRate
: 0;
_smoothedChangeRate = (_hasKnownChargingState && changeRate > 0) ? changeRate : 0;
_lastRateSampleTime = _smoothedChangeRate > 0
? Date.now()
: 0;
_lastRateSampleTime = _smoothedChangeRate > 0 ? Date.now() : 0;
if (isCharging) {
_hasNotifiedLowBattery = false;
@@ -569,11 +506,9 @@ Singleton {
applyPowerProfile();
if (isPluggedIn) {
const dismissLow = SettingsData.batteryLowNotificationType === 1
&& SettingsData.notificationTimeoutNormal === 0;
const dismissLow = SettingsData.batteryLowNotificationType === 1 && SettingsData.notificationTimeoutNormal === 0;
const dismissCritical = SettingsData.batteryCriticalNotificationType === 1
&& SettingsData.notificationTimeoutCritical === 0;
const dismissCritical = SettingsData.batteryCriticalNotificationType === 1 && SettingsData.notificationTimeoutCritical === 0;
if (dismissLow || dismissCritical) {
const lowSummary = I18n.tr("Low Battery");
@@ -585,8 +520,7 @@ Singleton {
const summary = w.notification.summary;
if ((dismissLow && summary === lowSummary)
|| (dismissCritical && summary === criticalSummary)) {
if ((dismissLow && summary === lowSummary) || (dismissCritical && summary === criticalSummary)) {
NotificationService.dismissNotification(w);
}
}
@@ -646,9 +580,7 @@ Singleton {
return "";
const magnitude = Math.abs(rate);
const value = (compact || magnitude >= 10)
? Math.round(magnitude).toString()
: magnitude.toFixed(1);
const value = (compact || magnitude >= 10) ? Math.round(magnitude).toString() : magnitude.toFixed(1);
return `${rate > 0 ? "+" : "-"}${value}W`;
}
@@ -705,17 +637,12 @@ Singleton {
if (usePreferred && preferredDeviceReady && preferredDevice.healthSupported)
return `${Math.round(preferredDevice.healthPercentage)}%`;
const validBatteries = readyBatteries.filter(
b => b.healthSupported && b.healthPercentage > 0
);
const validBatteries = readyBatteries.filter(b => b.healthSupported && b.healthPercentage > 0);
if (validBatteries.length === 0)
return "N/A";
const avgHealth = validBatteries.reduce(
(sum, b) => sum + b.healthPercentage,
0
) / validBatteries.length;
const avgHealth = validBatteries.reduce((sum, b) => sum + b.healthPercentage, 0) / validBatteries.length;
return `${Math.round(avgHealth)}%`;
}
@@ -779,18 +706,14 @@ Singleton {
if (isCharging)
return I18n.tr("Charging", "battery status");
return isPluggedIn
? I18n.tr("Plugged in", "battery status")
: I18n.tr("Discharging", "battery status");
return isPluggedIn ? I18n.tr("Plugged in", "battery status") : I18n.tr("Discharging", "battery status");
}
if (stateKnownBatteries.length === 0) {
if (isCharging)
return I18n.tr("Charging", "battery status");
return isPluggedIn
? I18n.tr("Plugged in", "battery status")
: I18n.tr("Discharging", "battery status");
return isPluggedIn ? I18n.tr("Plugged in", "battery status") : I18n.tr("Discharging", "battery status");
}
if (isCharging && !stateKnownBatteries.some(b => b.changeRate > 0))
@@ -801,37 +724,21 @@ Singleton {
if (states.every(s => s === states[0]))
return translateBatteryState(states[0]);
return isCharging
? I18n.tr("Charging", "battery status")
: (isPluggedIn
? I18n.tr("Plugged in", "battery status")
: I18n.tr("Discharging", "battery status"));
return isCharging ? I18n.tr("Charging", "battery status") : (isPluggedIn ? I18n.tr("Plugged in", "battery status") : I18n.tr("Discharging", "battery status"));
}
readonly property bool suggestPowerSaver: false
readonly property var peripheralDevices: UPower.devices.values
.filter(dev => dev
&& dev.ready
&& !dev.isLaptopBattery
&& peripheralIcon(dev.type) !== "")
.map(dev => ({
"name": dev.model || UPowerDeviceType.toString(dev.type),
"percentage": Math.round(dev.percentage * 100),
"type": dev.type,
"icon": peripheralIcon(dev.type),
"charging": dev.state === UPowerDeviceState.Charging
}))
readonly property var peripheralDevices: UPower.devices.values.filter(dev => dev && dev.ready && !dev.isLaptopBattery && peripheralIcon(dev.type) !== "").map(dev => ({
"name": dev.model || UPowerDeviceType.toString(dev.type),
"percentage": Math.round(dev.percentage * 100),
"type": dev.type,
"icon": peripheralIcon(dev.type),
"charging": dev.state === UPowerDeviceState.Charging
}))
readonly property var bluetoothDevices: {
const bluetoothTypes = [
UPowerDeviceType.BluetoothGeneric,
UPowerDeviceType.Headphones,
UPowerDeviceType.Headset,
UPowerDeviceType.Keyboard,
UPowerDeviceType.Mouse,
UPowerDeviceType.Speakers
];
const bluetoothTypes = [UPowerDeviceType.BluetoothGeneric, UPowerDeviceType.Headphones, UPowerDeviceType.Headset, UPowerDeviceType.Keyboard, UPowerDeviceType.Mouse, UPowerDeviceType.Speakers];
return peripheralDevices.filter(dev => bluetoothTypes.includes(dev.type));
}
@@ -873,13 +780,9 @@ Singleton {
if (!batteryAvailable)
return 0;
const rate = _smoothedChangeRate > 0
? _smoothedChangeRate
: changeRate;
const rate = _smoothedChangeRate > 0 ? _smoothedChangeRate : changeRate;
const totalTime = isCharging
? ((batteryCapacity - batteryEnergy) / rate)
: (batteryEnergy / rate);
const totalTime = isCharging ? ((batteryCapacity - batteryEnergy) / rate) : (batteryEnergy / rate);
const seconds = Math.abs(totalTime * 3600);
@@ -893,9 +796,7 @@ Singleton {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return hours > 0
? I18n.tr("%1h %2m", "battery time remaining").arg(hours).arg(minutes)
: I18n.tr("%1m", "battery time remaining").arg(minutes);
return hours > 0 ? I18n.tr("%1h %2m", "battery time remaining").arg(hours).arg(minutes) : I18n.tr("%1m", "battery time remaining").arg(minutes);
}
function formatTimeRemaining() {
@@ -916,10 +817,7 @@ Singleton {
const target = new Date(Date.now() + seconds * 1000);
const use24Hour = SettingsData.use24HourClock !== false;
return target.toLocaleTimeString(
Qt.locale(),
use24Hour ? "HH:mm" : "h:mm AP"
);
return target.toLocaleTimeString(Qt.locale(), use24Hour ? "HH:mm" : "h:mm AP");
}
function getBatteryIcon() {