|
|
|
@@ -2,6 +2,7 @@ import QtQuick
|
|
|
|
import QtQuick.Layouts
|
|
|
|
import QtQuick.Layouts
|
|
|
|
import Quickshell
|
|
|
|
import Quickshell
|
|
|
|
import Quickshell.Hyprland
|
|
|
|
import Quickshell.Hyprland
|
|
|
|
|
|
|
|
import Quickshell.Io
|
|
|
|
import Quickshell.Services.UPower
|
|
|
|
import Quickshell.Services.UPower
|
|
|
|
import "../../config"
|
|
|
|
import "../../config"
|
|
|
|
|
|
|
|
|
|
|
|
@@ -14,7 +15,7 @@ import "../../config"
|
|
|
|
// Nerd Font ones. The charge-threshold ("Holding"/limited-charge) concept from
|
|
|
|
// Nerd Font ones. The charge-threshold ("Holding"/limited-charge) concept from
|
|
|
|
// the original is hardware-specific (vendor charge-limit sysfs knobs) and has
|
|
|
|
// the original is hardware-specific (vendor charge-limit sysfs knobs) and has
|
|
|
|
// no equivalent in Quickshell's UPower binding, so it's dropped rather than
|
|
|
|
// no equivalent in Quickshell's UPower binding, so it's dropped rather than
|
|
|
|
// guessed at; "Battery health" (device.healthPercentage) is shown in its
|
|
|
|
// guessed at; "Battery health" (computed from sysfs energy_full/design) is shown in its
|
|
|
|
// place, using data UPower actually reports.
|
|
|
|
// place, using data UPower actually reports.
|
|
|
|
Item {
|
|
|
|
Item {
|
|
|
|
id: root
|
|
|
|
id: root
|
|
|
|
@@ -51,10 +52,162 @@ Item {
|
|
|
|
readonly property real fraction: root.batteryPresent ? Math.max(0, Math.min(1, root.device.percentage)) : 0
|
|
|
|
readonly property real fraction: root.batteryPresent ? Math.max(0, Math.min(1, root.device.percentage)) : 0
|
|
|
|
readonly property bool low: root.batteryPresent && root.fraction <= 0.15 && !root.charging
|
|
|
|
readonly property bool low: root.batteryPresent && root.fraction <= 0.15 && !root.charging
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Quickshell's UPower binding has no charge-cycle property (see the
|
|
|
|
|
|
|
|
// file-header comment on why the old charge-until threshold was
|
|
|
|
|
|
|
|
// dropped) — it comes straight from sysfs instead.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// device.nativePath turned out to be empty on real hardware (tested
|
|
|
|
|
|
|
|
// live), so rather than build a path from it, a shell glob finds
|
|
|
|
|
|
|
|
// whichever BAT* directory actually exists — same shell-out-to-a-CLI
|
|
|
|
|
|
|
|
// convention TailscaleIndicator.qml uses for its own live stats.
|
|
|
|
|
|
|
|
// This barely changes during a session, so one read at startup is
|
|
|
|
|
|
|
|
// enough; no polling timer.
|
|
|
|
|
|
|
|
property int cycleCount: -1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Process {
|
|
|
|
|
|
|
|
id: cycleCountProc
|
|
|
|
|
|
|
|
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/cycle_count\" 2>/dev/null"]
|
|
|
|
|
|
|
|
running: root.batteryPresent
|
|
|
|
|
|
|
|
stdout: StdioCollector {
|
|
|
|
|
|
|
|
id: cycleCountCollector
|
|
|
|
|
|
|
|
onStreamFinished: {
|
|
|
|
|
|
|
|
const n = parseInt(cycleCountCollector.text.trim(), 10)
|
|
|
|
|
|
|
|
root.cycleCount = (isNaN(n) || n <= 0) ? -1 : n
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// device.healthPercentage/healthSupported come from upowerd over D-Bus,
|
|
|
|
|
|
|
|
// and upowerd has been observed reporting energy-full as a flat 0 (a
|
|
|
|
|
|
|
|
// live daemon glitch, `upower -i` shows it too) while sysfs still has
|
|
|
|
|
|
|
|
// the real numbers underneath — same class of unreliability that put
|
|
|
|
|
|
|
|
// cycle_count on sysfs above instead of trusting upowerd's own
|
|
|
|
|
|
|
|
// charge-cycles field. Health is computed the same way UPower computes
|
|
|
|
|
|
|
|
// it (current full-charge capacity over the battery's original design
|
|
|
|
|
|
|
|
// capacity), just read directly so a daemon hiccup doesn't blank it.
|
|
|
|
|
|
|
|
property real energyFullWh: -1
|
|
|
|
|
|
|
|
property real energyFullDesignWh: -1
|
|
|
|
|
|
|
|
readonly property real healthPercent: (root.energyFullWh > 0 && root.energyFullDesignWh > 0)
|
|
|
|
|
|
|
|
? Math.min(100, (root.energyFullWh / root.energyFullDesignWh) * 100) : -1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Process {
|
|
|
|
|
|
|
|
id: energyFullProc
|
|
|
|
|
|
|
|
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/energy_full\" 2>/dev/null"]
|
|
|
|
|
|
|
|
running: root.batteryPresent
|
|
|
|
|
|
|
|
stdout: StdioCollector {
|
|
|
|
|
|
|
|
id: energyFullCollector
|
|
|
|
|
|
|
|
onStreamFinished: {
|
|
|
|
|
|
|
|
const n = parseFloat(energyFullCollector.text.trim())
|
|
|
|
|
|
|
|
root.energyFullWh = (isNaN(n) || n <= 0) ? -1 : (n / 1000000)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Process {
|
|
|
|
|
|
|
|
id: energyFullDesignProc
|
|
|
|
|
|
|
|
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/energy_full_design\" 2>/dev/null"]
|
|
|
|
|
|
|
|
running: root.batteryPresent
|
|
|
|
|
|
|
|
stdout: StdioCollector {
|
|
|
|
|
|
|
|
id: energyFullDesignCollector
|
|
|
|
|
|
|
|
onStreamFinished: {
|
|
|
|
|
|
|
|
const n = parseFloat(energyFullDesignCollector.text.trim())
|
|
|
|
|
|
|
|
root.energyFullDesignWh = (isNaN(n) || n <= 0) ? -1 : (n / 1000000)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// device.timeToFull/timeToEmpty are computed by Quickshell from
|
|
|
|
|
|
|
|
// energy/energyCapacity/changeRate — when upowerd's EnergyFull glitches
|
|
|
|
|
|
|
|
// to 0 (the same daemon flake HEALTH routes around above), that formula
|
|
|
|
|
|
|
|
// divides by a bogus capacity and comes out negative rather than a
|
|
|
|
|
|
|
|
// real estimate (confirmed live: timeToFull sat at -3751 while `upower
|
|
|
|
|
|
|
|
// -i` itself still reported a normal 26.7 minutes). This recomputes
|
|
|
|
|
|
|
|
// the same estimate straight from sysfs as a fallback, only used when
|
|
|
|
|
|
|
|
// Quickshell's own value isn't positive. Unlike cycle_count/design
|
|
|
|
|
|
|
|
// capacity above, energy_now and power_now change continuously while
|
|
|
|
|
|
|
|
// (dis)charging, so this polls on a timer instead of reading once.
|
|
|
|
|
|
|
|
property real sysfsEnergyNowWh: -1
|
|
|
|
|
|
|
|
property real sysfsPowerNowW: -1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
readonly property real fallbackTimeToFullSeconds: (root.sysfsPowerNowW > 0 && root.energyFullWh > 0 && root.sysfsEnergyNowWh >= 0)
|
|
|
|
|
|
|
|
? Math.max(0, (root.energyFullWh - root.sysfsEnergyNowWh) / root.sysfsPowerNowW * 3600) : -1
|
|
|
|
|
|
|
|
readonly property real fallbackTimeToEmptySeconds: (root.sysfsPowerNowW > 0 && root.sysfsEnergyNowWh > 0)
|
|
|
|
|
|
|
|
? (root.sysfsEnergyNowWh / root.sysfsPowerNowW * 3600) : -1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Prefers upowerd's own live estimate when it's sane; only falls back
|
|
|
|
|
|
|
|
// to the sysfs-computed one when upowerd's has gone zero/negative.
|
|
|
|
|
|
|
|
readonly property real timeRemainingSeconds: {
|
|
|
|
|
|
|
|
const primary = root.onBattery ? root.device.timeToEmpty : root.device.timeToFull
|
|
|
|
|
|
|
|
if (primary > 0) return primary
|
|
|
|
|
|
|
|
return root.onBattery ? root.fallbackTimeToEmptySeconds : root.fallbackTimeToFullSeconds
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Timer {
|
|
|
|
|
|
|
|
id: sysfsRateTimer
|
|
|
|
|
|
|
|
interval: 15000
|
|
|
|
|
|
|
|
repeat: true
|
|
|
|
|
|
|
|
running: root.batteryPresent
|
|
|
|
|
|
|
|
triggeredOnStart: true
|
|
|
|
|
|
|
|
onTriggered: {
|
|
|
|
|
|
|
|
if (!sysfsEnergyNowProc.running) sysfsEnergyNowProc.running = true
|
|
|
|
|
|
|
|
if (!sysfsPowerNowProc.running) sysfsPowerNowProc.running = true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Process {
|
|
|
|
|
|
|
|
id: sysfsEnergyNowProc
|
|
|
|
|
|
|
|
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/energy_now\" 2>/dev/null"]
|
|
|
|
|
|
|
|
stdout: StdioCollector {
|
|
|
|
|
|
|
|
id: sysfsEnergyNowCollector
|
|
|
|
|
|
|
|
onStreamFinished: {
|
|
|
|
|
|
|
|
const n = parseFloat(sysfsEnergyNowCollector.text.trim())
|
|
|
|
|
|
|
|
root.sysfsEnergyNowWh = (isNaN(n) || n < 0) ? -1 : (n / 1000000)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Process {
|
|
|
|
|
|
|
|
id: sysfsPowerNowProc
|
|
|
|
|
|
|
|
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/power_now\" 2>/dev/null"]
|
|
|
|
|
|
|
|
stdout: StdioCollector {
|
|
|
|
|
|
|
|
id: sysfsPowerNowCollector
|
|
|
|
|
|
|
|
onStreamFinished: {
|
|
|
|
|
|
|
|
const n = parseFloat(sysfsPowerNowCollector.text.trim())
|
|
|
|
|
|
|
|
root.sysfsPowerNowW = (isNaN(n) || n <= 0) ? -1 : (n / 1000000)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Raw UPower state string (Charging/Discharging/PendingCharge/…) — more
|
|
|
|
|
|
|
|
// granular than modeLabel below, which collapses those down to the
|
|
|
|
|
|
|
|
// three phrases the hero text and STATE row show.
|
|
|
|
|
|
|
|
readonly property string rawStateLabel: root.batteryPresent
|
|
|
|
|
|
|
|
? UPowerDeviceState.toString(root.deviceState) : "—"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Material Symbols' charging glyphs aren't an evenly-spaced "_bar"
|
|
|
|
|
|
|
|
// scale like the non-charging ones below — they're fixed at these
|
|
|
|
|
|
|
|
// seven breakpoints (confirmed against the installed font's ligature
|
|
|
|
|
|
|
|
// table), so this picks the highest one at or below the current
|
|
|
|
|
|
|
|
// percentage rather than rounding to an index.
|
|
|
|
|
|
|
|
function chargingIcon(fraction) {
|
|
|
|
|
|
|
|
const percent = fraction * 100
|
|
|
|
|
|
|
|
const thresholds = [20, 30, 50, 60, 80, 90, 100]
|
|
|
|
|
|
|
|
const icons = [
|
|
|
|
|
|
|
|
"battery_charging_20", "battery_charging_30", "battery_charging_50", "battery_charging_60",
|
|
|
|
|
|
|
|
"battery_charging_80", "battery_charging_90", "battery_charging_full"
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
for (let i = thresholds.length - 1; i >= 0; i--) {
|
|
|
|
|
|
|
|
if (percent >= thresholds[i]) return icons[i]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Below 20% there's no lower charging glyph — it's still the
|
|
|
|
|
|
|
|
// truthful floor of what's available while current is flowing in.
|
|
|
|
|
|
|
|
return icons[0]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function batteryIcon() {
|
|
|
|
function batteryIcon() {
|
|
|
|
if (!root.batteryPresent) return "battery_unknown"
|
|
|
|
if (!root.batteryPresent) return "battery_unknown"
|
|
|
|
if (root.fullyCharged) return "battery_full"
|
|
|
|
if (root.fullyCharged) return "battery_full"
|
|
|
|
if (root.charging) return "battery_charging_full"
|
|
|
|
if (root.charging) return root.chargingIcon(root.fraction)
|
|
|
|
if (root.fraction <= 0.1) return "battery_alert"
|
|
|
|
if (root.fraction <= 0.1) return "battery_alert"
|
|
|
|
const bars = ["battery_0_bar", "battery_1_bar", "battery_2_bar", "battery_3_bar", "battery_4_bar", "battery_5_bar", "battery_full"]
|
|
|
|
const bars = ["battery_0_bar", "battery_1_bar", "battery_2_bar", "battery_3_bar", "battery_4_bar", "battery_5_bar", "battery_full"]
|
|
|
|
return bars[Math.max(0, Math.min(bars.length - 1, Math.round(root.fraction * (bars.length - 1))))]
|
|
|
|
return bars[Math.max(0, Math.min(bars.length - 1, Math.round(root.fraction * (bars.length - 1))))]
|
|
|
|
@@ -74,6 +227,32 @@ Item {
|
|
|
|
return root.onBattery ? "On battery" : "Charging"
|
|
|
|
return root.onBattery ? "On battery" : "Charging"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Info grid rows — same shape as NetworkIndicator.qml's statRows/
|
|
|
|
|
|
|
|
// statCells: a flat label/value list, flattened again into individual
|
|
|
|
|
|
|
|
// grid cells (label, value, label, value, …) so a 4-column GridLayout
|
|
|
|
|
|
|
|
// lays them out as two label/value pairs per row.
|
|
|
|
|
|
|
|
readonly property var statRows: [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
label: "HEALTH",
|
|
|
|
|
|
|
|
value: root.healthPercent > 0 ? Math.round(root.healthPercent) + "%" : "—"
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
label: root.onBattery ? "TIME LEFT" : "TIME TO FULL",
|
|
|
|
|
|
|
|
value: root.batteryPresent ? root.formatDuration(root.timeRemainingSeconds) : "—"
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{ label: "STATUS", value: root.rawStateLabel },
|
|
|
|
|
|
|
|
{ label: "CYCLES", value: root.cycleCount > 0 ? String(root.cycleCount) : "—" }
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
readonly property var statCells: {
|
|
|
|
|
|
|
|
const cells = []
|
|
|
|
|
|
|
|
for (let i = 0; i < root.statRows.length; i++) {
|
|
|
|
|
|
|
|
cells.push({ text: root.statRows[i].label, isLabel: true })
|
|
|
|
|
|
|
|
cells.push({ text: root.statRows[i].value, isLabel: false })
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return cells
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Cute rotating status phrases while current is actually flowing —
|
|
|
|
// Cute rotating status phrases while current is actually flowing —
|
|
|
|
// ported as-is from omarchy's power panel, which pairs the same phrase
|
|
|
|
// ported as-is from omarchy's power panel, which pairs the same phrase
|
|
|
|
// sets with the same charging/on-battery states.
|
|
|
|
// sets with the same charging/on-battery states.
|
|
|
|
@@ -142,6 +321,34 @@ Item {
|
|
|
|
return entries
|
|
|
|
return entries
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- Automatic power profile --------------------------------------
|
|
|
|
|
|
|
|
// Battery-state-driven, on top of the manual picker above: below 20%
|
|
|
|
|
|
|
|
// → Power Saver (protect what's left), plugged in → Performance,
|
|
|
|
|
|
|
|
// unplugged and healthy → Balanced. Low battery wins even if a weak
|
|
|
|
|
|
|
|
// charger is still plugged in — running low is the more urgent signal.
|
|
|
|
|
|
|
|
// Performance only gets requested when the daemon actually reports it
|
|
|
|
|
|
|
|
// as available, falling back to Balanced instead of erroring on
|
|
|
|
|
|
|
|
// hardware without a performance profile (e.g. the ERROR this exact
|
|
|
|
|
|
|
|
// widget hit before power-profiles-daemon was installed).
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// This only ever writes when the *target* changes (crossing the 20%
|
|
|
|
|
|
|
|
// line, plugging/unplugging) rather than on every poll, so a manual
|
|
|
|
|
|
|
|
// pick from the buttons above still sticks until the next real
|
|
|
|
|
|
|
|
// transition instead of being fought immediately.
|
|
|
|
|
|
|
|
readonly property int desiredProfile: {
|
|
|
|
|
|
|
|
if (root.fraction <= 0.20) return PowerProfile.PowerSaver
|
|
|
|
|
|
|
|
if (!root.onBattery) return PowerProfiles.hasPerformanceProfile ? PowerProfile.Performance : PowerProfile.Balanced
|
|
|
|
|
|
|
|
return PowerProfile.Balanced
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function applyDesiredProfile() {
|
|
|
|
|
|
|
|
if (!root.batteryPresent) return
|
|
|
|
|
|
|
|
if (PowerProfiles.profile !== root.desiredProfile) PowerProfiles.profile = root.desiredProfile
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onDesiredProfileChanged: root.applyDesiredProfile()
|
|
|
|
|
|
|
|
Component.onCompleted: Qt.callLater(root.applyDesiredProfile)
|
|
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
Text {
|
|
|
|
id: label
|
|
|
|
id: label
|
|
|
|
anchors.centerIn: parent
|
|
|
|
anchors.centerIn: parent
|
|
|
|
@@ -217,33 +424,16 @@ Item {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
component StatRow: RowLayout {
|
|
|
|
|
|
|
|
id: statRow
|
|
|
|
|
|
|
|
width: parent.width
|
|
|
|
|
|
|
|
property string label: ""
|
|
|
|
|
|
|
|
property string value: ""
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
|
|
text: statRow.label
|
|
|
|
|
|
|
|
font.family: Appearance.fontFamily
|
|
|
|
|
|
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
|
|
|
|
|
|
color: Colors.muted
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
|
|
|
text: statRow.value
|
|
|
|
|
|
|
|
font.family: Appearance.fontFamily
|
|
|
|
|
|
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
|
|
|
|
|
|
color: Colors.muted
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- Panel -----------------------------------------------------------
|
|
|
|
// --- Panel -----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
PopupWindow {
|
|
|
|
PopupWindow {
|
|
|
|
id: panel
|
|
|
|
id: panel
|
|
|
|
visible: false
|
|
|
|
visible: false
|
|
|
|
color: "transparent"
|
|
|
|
color: "transparent"
|
|
|
|
readonly property int contentWidth: 244
|
|
|
|
// Wider than before (244) to give the 2-column info grid's
|
|
|
|
|
|
|
|
// label/value pairs enough room per half without eliding —
|
|
|
|
|
|
|
|
// same reasoning as NetworkIndicator.qml's own wider panel.
|
|
|
|
|
|
|
|
readonly property int contentWidth: 280
|
|
|
|
readonly property int contentPadding: 16
|
|
|
|
readonly property int contentPadding: 16
|
|
|
|
implicitWidth: contentWidth + contentPadding * 2
|
|
|
|
implicitWidth: contentWidth + contentPadding * 2
|
|
|
|
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
|
|
|
|
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
|
|
|
|
@@ -350,34 +540,27 @@ Item {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Stats
|
|
|
|
// Stats — a 2-column, 2-row info grid, same layout
|
|
|
|
RowLayout {
|
|
|
|
// NetworkIndicator.qml uses for its own live stats: a
|
|
|
|
|
|
|
|
// 4-column GridLayout fed a flattened label/value/label/value
|
|
|
|
|
|
|
|
// list, so each row holds two label/value pairs side by side
|
|
|
|
|
|
|
|
// with the values right-aligned against their own half.
|
|
|
|
|
|
|
|
GridLayout {
|
|
|
|
width: parent.width
|
|
|
|
width: parent.width
|
|
|
|
spacing: 16
|
|
|
|
columns: 4
|
|
|
|
|
|
|
|
columnSpacing: 12
|
|
|
|
|
|
|
|
rowSpacing: 5
|
|
|
|
|
|
|
|
|
|
|
|
Column {
|
|
|
|
Repeater {
|
|
|
|
Layout.fillWidth: true
|
|
|
|
model: root.statCells
|
|
|
|
spacing: 4
|
|
|
|
Text {
|
|
|
|
StatRow { label: "STATE"; value: root.modeLabel }
|
|
|
|
required property var modelData
|
|
|
|
StatRow {
|
|
|
|
Layout.fillWidth: !modelData.isLabel
|
|
|
|
label: "HEALTH"
|
|
|
|
horizontalAlignment: modelData.isLabel ? Text.AlignLeft : Text.AlignRight
|
|
|
|
value: (root.batteryPresent && root.device.healthSupported)
|
|
|
|
text: modelData.text
|
|
|
|
? Math.round(root.device.healthPercentage) + "%" : "—"
|
|
|
|
font.family: Appearance.fontFamily
|
|
|
|
}
|
|
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
|
|
}
|
|
|
|
color: modelData.isLabel ? Colors.muted : Colors.foreground
|
|
|
|
|
|
|
|
|
|
|
|
Column {
|
|
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
|
|
spacing: 4
|
|
|
|
|
|
|
|
StatRow {
|
|
|
|
|
|
|
|
label: root.onBattery ? "TIME LEFT" : "TIME TO FULL"
|
|
|
|
|
|
|
|
value: root.batteryPresent
|
|
|
|
|
|
|
|
? root.formatDuration(root.onBattery ? root.device.timeToEmpty : root.device.timeToFull)
|
|
|
|
|
|
|
|
: "—"
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
StatRow {
|
|
|
|
|
|
|
|
label: "RATE"
|
|
|
|
|
|
|
|
value: root.batteryPresent ? Math.abs(root.device.changeRate).toFixed(1) + " W" : "—"
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|