494 lines
20 KiB
QML
494 lines
20 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import Quickshell.Services.UPower
|
|
|
|
import "../../config" as Cfg
|
|
import "../../components"
|
|
|
|
ModuleChip {
|
|
id: root
|
|
|
|
readonly property var bat: UPower.displayDevice
|
|
readonly property bool batReady: bat && bat.ready && bat.isLaptopBattery
|
|
|
|
readonly property bool moduleEnabled: batReady
|
|
|
|
readonly property real pct: batReady ? bat.percentage * 100 : 72
|
|
readonly property int state: batReady ? bat.state : UPowerDeviceState.Unknown
|
|
readonly property bool charging: state === UPowerDeviceState.Charging
|
|
readonly property bool fullCharge: state === UPowerDeviceState.FullyCharged
|
|
|
|
readonly property string batIcon: {
|
|
if (!batReady) return ""
|
|
if (fullCharge) return ""
|
|
if (charging) {
|
|
if (pct >= 90) return ""
|
|
if (pct >= 80) return ""
|
|
if (pct >= 70) return ""
|
|
if (pct >= 60) return ""
|
|
if (pct >= 50) return ""
|
|
if (pct >= 40) return ""
|
|
if (pct >= 30) return ""
|
|
if (pct >= 20) return ""
|
|
if (pct >= 10) return ""
|
|
return ""
|
|
}
|
|
if (pct >= 90) return ""
|
|
if (pct >= 80) return ""
|
|
if (pct >= 70) return ""
|
|
if (pct >= 60) return ""
|
|
if (pct >= 50) return ""
|
|
if (pct >= 40) return ""
|
|
if (pct >= 30) return ""
|
|
if (pct >= 20) return ""
|
|
if (pct >= 10) return ""
|
|
return ""
|
|
}
|
|
|
|
readonly property bool isCritical: batReady && !charging && pct <= 15
|
|
readonly property bool isLow: batReady && !charging && pct <= 30
|
|
|
|
readonly property color iconColor: {
|
|
if (fullCharge) return t.batteryFull
|
|
if (charging) return t.batteryCharging
|
|
if (isCritical) return t.batteryCritical
|
|
if (isLow) return t.batteryLow
|
|
return t.batteryIcon
|
|
}
|
|
|
|
readonly property var up: UPower
|
|
readonly property var pp: PowerProfiles
|
|
|
|
readonly property string typeText: {
|
|
if (!batReady) return "—"
|
|
switch (bat.type) {
|
|
case UPowerDeviceType.Battery: return "Battery"
|
|
case UPowerDeviceType.Ups: return "UPS"
|
|
case UPowerDeviceType.Mouse: return "Mouse"
|
|
case UPowerDeviceType.Keyboard: return "Keyboard"
|
|
case UPowerDeviceType.Phone: return "Phone"
|
|
case UPowerDeviceType.Tablet: return "Tablet"
|
|
default: return "Other"
|
|
}
|
|
}
|
|
|
|
readonly property string degradationText: {
|
|
if (pp.degradationReason === PerformanceDegradationReason.None) return ""
|
|
if (pp.degradationReason === PerformanceDegradationReason.LapDetected) return "Lap detected"
|
|
if (pp.degradationReason === PerformanceDegradationReason.HighTemperature) return "High temp"
|
|
return ""
|
|
}
|
|
|
|
readonly property var profileHolds: {
|
|
let names = []
|
|
for (let i = 0; i < pp.holds.length; i++) {
|
|
let h = pp.holds[i]
|
|
let label = PowerProfile.toString(h.profile)
|
|
if (h.applicationId) label += " (" + h.applicationId + ")"
|
|
names.push(label)
|
|
}
|
|
return names
|
|
}
|
|
|
|
chipColor: t.batBg
|
|
chipColorHovered: t.batBgHover
|
|
chipBorderColor: t.batBorder
|
|
chipBorderColorHovered: t.batBorderHover
|
|
chipHoverEnabled: true
|
|
chipTapAction: () => batteryPopup.toggle()
|
|
chipAnimWidth: true
|
|
|
|
width: batLayout.implicitWidth + 24
|
|
radius: panelRadius
|
|
|
|
RowLayout {
|
|
id: batLayout
|
|
anchors.centerIn: parent
|
|
spacing: 0
|
|
|
|
Text {
|
|
text: root.batIcon
|
|
color: root.iconColor
|
|
font.pixelSize: 16
|
|
Layout.alignment: Qt.AlignVCenter
|
|
Layout.rightMargin: 7
|
|
|
|
SequentialAnimation on opacity {
|
|
running: root.isCritical
|
|
loops: Animation.Infinite
|
|
NumberAnimation { to: 0.3; duration: 700; easing.type: Easing.InOutSine }
|
|
NumberAnimation { to: 1.0; duration: 700; easing.type: Easing.InOutSine }
|
|
}
|
|
opacity: 1.0
|
|
}
|
|
|
|
Text {
|
|
text: root.batReady ? Math.round(root.pct) + "%" : "—"
|
|
color: root.t.batText
|
|
font.pixelSize: 13
|
|
font.bold: true
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
|
|
Text {
|
|
id: chargingLabel
|
|
text: root.charging ? " +" : (root.fullCharge ? " ✓" : "")
|
|
color: root.iconColor
|
|
font.pixelSize: 13
|
|
font.bold: true
|
|
verticalAlignment: Text.AlignVCenter
|
|
|
|
opacity: (root.hovered || batteryPopup.popupOpen) ? 1.0 : 0.0
|
|
Layout.preferredWidth: (root.hovered || batteryPopup.popupOpen)
|
|
? implicitWidth : 0
|
|
clip: true
|
|
|
|
Behavior on opacity { NumberAnimation { duration: 200 } }
|
|
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
|
}
|
|
}
|
|
|
|
StandardPopup {
|
|
id: batteryPopup
|
|
ns: "battery"
|
|
chipRoot: root
|
|
cardWidth: 260
|
|
cardColor: t.batPopupBg
|
|
cardBorderColor: t.batPopupBorder
|
|
cardHeight: cardCol.implicitHeight + 28
|
|
showShadow: true
|
|
layerEnabled: true
|
|
|
|
ColumnLayout {
|
|
id: cardCol
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
top: parent.top
|
|
margins: 18
|
|
topMargin: 20
|
|
}
|
|
spacing: 12
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 14
|
|
|
|
Item {
|
|
width: 52
|
|
height: 52
|
|
|
|
Rectangle {
|
|
anchors.centerIn: parent
|
|
width: 46
|
|
height: 46
|
|
radius: 23
|
|
color: Qt.rgba(
|
|
Qt.darker(root.iconColor, 1.0).r,
|
|
Qt.darker(root.iconColor, 1.0).g,
|
|
Qt.darker(root.iconColor, 1.0).b,
|
|
0.15
|
|
)
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: root.batIcon
|
|
color: root.iconColor
|
|
font.pixelSize: 26
|
|
|
|
SequentialAnimation on opacity {
|
|
running: root.isCritical
|
|
loops: Animation.Infinite
|
|
NumberAnimation { to: 0.3; duration: 700; easing.type: Easing.InOutSine }
|
|
NumberAnimation { to: 1.0; duration: 700; easing.type: Easing.InOutSine }
|
|
}
|
|
opacity: 1.0
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
spacing: 2
|
|
Layout.fillWidth: true
|
|
|
|
Text {
|
|
text: root.batReady ? Math.round(root.pct) + "%" : "—"
|
|
color: root.t.batPopupHeader
|
|
font.pixelSize: 30
|
|
font.bold: true
|
|
}
|
|
|
|
Text {
|
|
text: {
|
|
if (!root.batReady) return "No battery"
|
|
if (root.fullCharge) return "Fully charged"
|
|
if (root.charging) return "Charging"
|
|
if (root.isCritical) return "Critical"
|
|
if (root.isLow) return "Low"
|
|
return "Discharging"
|
|
}
|
|
color: root.iconColor
|
|
font.pixelSize: 11
|
|
font.capitalization: Font.AllUppercase
|
|
font.letterSpacing: 0.8
|
|
font.bold: true
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
height: 10
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: 5
|
|
color: root.t.batProgressTrack
|
|
}
|
|
|
|
Rectangle {
|
|
width: Math.max(height, parent.width * (root.pct / 100))
|
|
height: parent.height
|
|
radius: 5
|
|
clip: false
|
|
|
|
gradient: Gradient {
|
|
orientation: Gradient.Horizontal
|
|
GradientStop {
|
|
position: 0.0
|
|
color: Qt.darker(root.iconColor, 1.25)
|
|
}
|
|
GradientStop {
|
|
position: 1.0
|
|
color: root.iconColor
|
|
}
|
|
}
|
|
|
|
Behavior on width { NumberAnimation { duration: 450; easing.type: Easing.OutCubic } }
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: 5
|
|
color: "transparent"
|
|
border.color: Qt.rgba(root.iconColor.r,
|
|
root.iconColor.g,
|
|
root.iconColor.b, 0.2)
|
|
border.width: 1
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
color: root.t.batPopupSeparator
|
|
opacity: 0.6
|
|
}
|
|
|
|
component DetailRow: RowLayout {
|
|
required property string label
|
|
required property string value
|
|
Layout.fillWidth: true
|
|
|
|
Text {
|
|
text: label
|
|
color: root.t.batPopupDim
|
|
font.pixelSize: 12
|
|
Layout.fillWidth: true
|
|
}
|
|
Text {
|
|
text: value
|
|
color: root.t.batPopupText
|
|
font.pixelSize: 12
|
|
font.bold: true
|
|
font.family: "monospace"
|
|
}
|
|
}
|
|
|
|
DetailRow {
|
|
label: root.charging ? "Time to full" : "Time left"
|
|
value: {
|
|
if (!root.batReady || root.fullCharge) return "—"
|
|
let secs = root.charging ? bat.timeToFull : bat.timeToEmpty
|
|
if (secs <= 0) return "—"
|
|
let h = Math.floor(secs / 3600)
|
|
let m = Math.floor((secs % 3600) / 60)
|
|
return h > 0 ? h + "h " + m + "m" : m + "m"
|
|
}
|
|
}
|
|
|
|
DetailRow {
|
|
label: "Rate"
|
|
value: {
|
|
if (!root.batReady) return "—"
|
|
let r = bat.changeRate
|
|
return (r >= 0 ? "+" : "") + r.toFixed(1) + " W"
|
|
}
|
|
}
|
|
|
|
DetailRow {
|
|
label: "Health"
|
|
value: {
|
|
if (!root.batReady) return "—"
|
|
if (!bat.healthSupported) return "Undetected"
|
|
return Math.round(bat.healthPercentage) + "%"
|
|
}
|
|
}
|
|
|
|
DetailRow {
|
|
label: "Energy"
|
|
value: {
|
|
if (!root.batReady) return "—"
|
|
return bat.energy.toFixed(1) + " / "
|
|
+ bat.energyCapacity.toFixed(1) + " Wh"
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
height: 16
|
|
|
|
Rectangle {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: parent.width
|
|
height: 1
|
|
color: root.t.batPopupSeparator
|
|
opacity: 0.6
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.centerIn: parent
|
|
width: sectionLabel.implicitWidth + 14
|
|
height: sectionLabel.implicitHeight + 4
|
|
radius: 4
|
|
color: root.t.batPopupBg
|
|
|
|
Text {
|
|
id: sectionLabel
|
|
anchors.centerIn: parent
|
|
text: "Power Profile"
|
|
color: root.t.batPopupDim
|
|
font.pixelSize: 10
|
|
font.capitalization: Font.AllUppercase
|
|
font.letterSpacing: 0.8
|
|
}
|
|
}
|
|
}
|
|
|
|
component ProfileButton: Rectangle {
|
|
required property string text
|
|
required property var profileValue
|
|
property bool interactive: true
|
|
|
|
property bool isActive: interactive && (root.pp.profile === profileValue)
|
|
|
|
height: 38
|
|
radius: 10
|
|
color: isActive ? root.iconColor
|
|
: Qt.rgba(root.iconColor.r, root.iconColor.g, root.iconColor.b,
|
|
(profileHover.containsMouse && interactive) ? 0.14 : 0.06)
|
|
border.color: isActive
|
|
? Qt.lighter(root.iconColor, 1.2)
|
|
: Qt.rgba(root.iconColor.r, root.iconColor.g, root.iconColor.b, 0.12)
|
|
border.width: 1
|
|
|
|
Behavior on color { ColorAnimation { duration: 150 } }
|
|
Behavior on border.color { ColorAnimation { duration: 150 } }
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: parent.text
|
|
font.pixelSize: 10
|
|
font.bold: parent.isActive
|
|
font.letterSpacing: 0.4
|
|
color: parent.isActive ? root.t.batProfileActiveText : root.t.batPopupText
|
|
}
|
|
|
|
HoverHandler { id: profileHover; enabled: parent.interactive }
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: parent.interactive ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
onClicked: {
|
|
if (parent.interactive) root.pp.profile = parent.profileValue
|
|
}
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 6
|
|
|
|
ProfileButton {
|
|
text: "Power Saver"
|
|
profileValue: PowerProfile.PowerSaver
|
|
Layout.fillWidth: true
|
|
}
|
|
ProfileButton {
|
|
text: "Balanced"
|
|
profileValue: PowerProfile.Balanced
|
|
Layout.fillWidth: true
|
|
}
|
|
ProfileButton {
|
|
text: "Performance"
|
|
profileValue: PowerProfile.Performance
|
|
interactive: root.pp.hasPerformanceProfile
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
|
|
DetailRow {
|
|
label: "Degradation"
|
|
value: root.degradationText || "None"
|
|
}
|
|
|
|
DetailRow {
|
|
label: "Holds"
|
|
value: root.profileHolds.length > 0 ? root.profileHolds[0] : "—"
|
|
visible: root.profileHolds.length > 0
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
height: 16
|
|
|
|
Rectangle {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: parent.width
|
|
height: 1
|
|
color: root.t.batPopupSeparator
|
|
opacity: 0.6
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.centerIn: parent
|
|
width: devLabel.implicitWidth + 14
|
|
height: devLabel.implicitHeight + 4
|
|
radius: 4
|
|
color: root.t.batPopupBg
|
|
|
|
Text {
|
|
id: devLabel
|
|
anchors.centerIn: parent
|
|
text: "Device"
|
|
color: root.t.batPopupDim
|
|
font.pixelSize: 10
|
|
font.capitalization: Font.AllUppercase
|
|
font.letterSpacing: 0.8
|
|
}
|
|
}
|
|
}
|
|
|
|
DetailRow { label: "Type"; value: root.typeText }
|
|
DetailRow { label: "Model"; value: root.batReady ? (bat.model || "Undetected") : "—" }
|
|
DetailRow { label: "Power supply"; value: root.batReady ? (bat.powerSupply ? "Yes" : "No") : "—" }
|
|
DetailRow { label: "On battery"; value: root.up.onBattery ? "Yes" : "No" }
|
|
DetailRow { label: "Devices"; value: root.up.devices.values.length + " total" }
|
|
|
|
Item { height: 2 }
|
|
}
|
|
}
|
|
}
|