resources popout refresh + new components & reskinned old components
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 23s
Python / test (pull_request) Successful in 31s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m12s

This commit is contained in:
2026-06-14 23:09:10 +02:00
parent 4cb2d843a5
commit 2a50732bc2
37 changed files with 2736 additions and 1122 deletions
+60 -63
View File
@@ -1,92 +1,89 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Services
import qs.Components
import qs.Helpers
import qs.Config
Row {
Item {
id: root
anchors.bottom: parent.bottom
anchors.top: parent.top
padding: Appearance.padding.large
spacing: Appearance.spacing.large
implicitWidth: layout.implicitWidth + layout.anchors.margins * 4
ServiceRef {
service: Storage
}
ServiceRef {
service: Memory
}
ServiceRef {
service: Cpu
}
Ref {
service: SystemUsage
}
Resource {
color: DynamicColors.palette.m3primary
icon: "memory"
value: SystemUsage.cpuPerc
}
Resource {
color: DynamicColors.palette.m3secondary
icon: "memory_alt"
value: SystemUsage.memPerc
}
Resource {
color: DynamicColors.palette.m3tertiary
icon: "gamepad"
value: SystemUsage.gpuPerc
}
Resource {
color: DynamicColors.palette.m3primary
icon: "host"
value: SystemUsage.gpuMemUsed
}
Resource {
color: DynamicColors.palette.m3secondary
icon: "hard_disk"
value: SystemUsage.storagePerc
}
component Resource: Item {
id: res
required property color color
required property string icon
required property real value
ColumnLayout {
id: layout
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Appearance.padding.large
anchors.top: parent.top
implicitWidth: icon.implicitWidth
spacing: Appearance.spacing.normal
Behavior on value {
Anim {
duration: Appearance.anim.durations.large
}
Resource {
fgColor: DynamicColors.palette.m3primary
icon: "memory"
value: Cpu.percentage
}
CustomRect {
anchors.bottom: icon.top
anchors.bottomMargin: Appearance.spacing.small
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
implicitWidth: Config.dashboard.sizes.resourceProgessThickness
radius: Appearance.rounding.full
Resource {
fgColor: DynamicColors.palette.m3secondary
icon: "memory_alt"
value: Memory.percentage
}
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
color: res.color
implicitHeight: res.value * parent.height
radius: Appearance.rounding.full
Resource {
fgColor: DynamicColors.palette.m3tertiary
icon: "gamepad"
value: SystemUsage.gpuPerc
}
Resource {
fgColor: DynamicColors.palette.m3primary
icon: "host"
value: SystemUsage.gpuMemUsed
}
Resource {
fgColor: DynamicColors.palette.m3secondary
icon: "hard_disk"
value: Storage.percentage
}
}
component Resource: CircularProgress {
id: res
required property string icon
Layout.fillHeight: true
implicitSize: height
Behavior on clampedVal {
Anim {
}
}
MaterialIcon {
id: icon
anchors.bottom: parent.bottom
color: res.color
anchors.centerIn: parent
color: res.fgColor
text: res.icon
}
}
+6 -6
View File
@@ -23,7 +23,7 @@ GridLayout {
Resource {
Layout.bottomMargin: Appearance.padding.large
Layout.topMargin: Appearance.padding.large
colour: DynamicColors.palette.m3primary
color: DynamicColors.palette.m3primary
icon: "memory"
value: SystemUsage.cpuPerc
}
@@ -31,7 +31,7 @@ GridLayout {
Resource {
Layout.bottomMargin: Appearance.padding.large
Layout.topMargin: Appearance.padding.large
colour: DynamicColors.palette.m3secondary
color: DynamicColors.palette.m3secondary
icon: "memory_alt"
value: SystemUsage.memPerc
}
@@ -39,7 +39,7 @@ GridLayout {
component Resource: CustomRect {
id: res
required property color colour
required property color color
required property string icon
required property real value
@@ -58,8 +58,8 @@ GridLayout {
id: circ
anchors.fill: parent
bgColour: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3)
fgColour: res.colour
bgColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3)
fgColor: res.color
padding: Appearance.padding.large * 3
strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
value: res.value
@@ -69,7 +69,7 @@ GridLayout {
id: icon
anchors.centerIn: parent
color: res.colour
color: res.color
font.pointSize: (circ.arcRadius * 0.7) || 1
font.weight: 600
text: res.icon
+17 -7
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import QtQuick.Layouts
import ZShell.Services
import qs.Helpers
import qs.Modules
import qs.Config
@@ -16,7 +17,7 @@ CustomRect {
clip: true
color: visibilities.resources ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2
implicitWidth: rowLayout.implicitWidth + Appearance.padding.larger * 2
radius: Appearance.rounding.full
StateLayer {
@@ -29,6 +30,7 @@ CustomRect {
id: rowLayout
anchors.centerIn: parent
anchors.horizontalCenterOffset: -2
implicitHeight: root.implicitHeight
spacing: Appearance.spacing.smaller
@@ -36,13 +38,21 @@ CustomRect {
service: SystemUsage
}
ServiceRef {
service: Cpu
}
ServiceRef {
service: Memory
}
Resource {
Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: true
icon: "memory"
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
mainColor: root.visibilities.resources ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3primary
percentage: SystemUsage.cpuPerc
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3primary
percentage: Cpu.percentage
warningThreshold: 95
}
@@ -50,8 +60,8 @@ CustomRect {
Layout.fillHeight: true
icon: "memory_alt"
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
mainColor: root.visibilities.resources ? DynamicColors.palette.m3secondaryContainer : DynamicColors.palette.m3secondary
percentage: SystemUsage.memPerc
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3secondary
percentage: Memory.percentage
warningThreshold: 80
}
@@ -59,7 +69,7 @@ CustomRect {
Layout.fillHeight: true
icon: "gamepad"
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
mainColor: root.visibilities.resources ? DynamicColors.palette.m3tertiaryContainer : DynamicColors.palette.m3tertiary
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3tertiary
percentage: SystemUsage.gpuPerc
}
@@ -67,7 +77,7 @@ CustomRect {
Layout.fillHeight: true
icon: "developer_board"
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
mainColor: root.visibilities.resources ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3primary
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3primary
percentage: SystemUsage.gpuMemUsed
}
}
+134
View File
@@ -0,0 +1,134 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.UPower
import qs.Components
import qs.Config
CustomClippingRect {
id: root
property real animPerc: UPower.displayDevice.percentage
color: DynamicColors.palette.m3secondaryContainer
implicitWidth: 120
radius: Appearance.rounding.large
Behavior on animPerc {
Anim {
}
}
Contents {
id: layout
accentColor: DynamicColors.palette.m3primary
anchors.fill: parent
anchors.margins: Appearance.padding.larger
subTextColor: DynamicColors.palette.m3onSurfaceVariant
textColor: DynamicColors.palette.m3onSurface
}
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
clip: true
color: DynamicColors.palette.m3secondary
implicitHeight: parent.height * root.animPerc
radius: Appearance.rounding.extraSmall
Contents {
accentColor: DynamicColors.palette.m3primaryContainer
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: layout.anchors.margins
anchors.right: parent.right
height: layout.height
subTextColor: DynamicColors.palette.m3secondaryContainer
textColor: DynamicColors.palette.m3onSecondary
}
}
component Contents: ColumnLayout {
id: contents
required property color accentColor
readonly property bool charging: [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state)
required property color subTextColor
required property color textColor
spacing: 0
MaterialIcon {
Layout.leftMargin: -Appearance.padding.extraSmall
color: contents.accentColor
text: "battery_full"
}
CustomText {
Layout.fillWidth: true
color: contents.textColor
text: qsTr("Battery")
}
Item {
Layout.fillHeight: true
}
CustomText {
Layout.alignment: Qt.AlignRight
animate: true
color: contents.subTextColor
text: {
if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)
return qsTr("Full");
if (contents.charging)
return qsTr("Charging");
const s = UPower.displayDevice.timeToEmpty;
if (s === 0)
return qsTr("...");
const hr = Math.floor(s / 3600);
const min = Math.floor((s % 3600) / 60);
if (hr > 0)
return `${hr}h ${min}m`;
return `${min}m`;
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -Appearance.padding.small
Layout.rightMargin: -Appearance.padding.extraSmall
Layout.topMargin: -Appearance.padding.extraSmall
spacing: Appearance.spacing.extraSmall
MaterialIcon {
color: contents.accentColor
fill: 1
opacity: contents.charging ? 1 : 0
scale: contents.charging ? 1 : 0
text: "bolt"
Behavior on opacity {
Anim {
type: Anim.FastEffects
}
}
Behavior on scale {
Anim {
type: Anim.FastSpatial
}
}
}
CustomText {
color: contents.accentColor
text: `${Math.round(UPower.displayDevice.percentage * 100)}%`
}
}
}
}
+138
View File
@@ -0,0 +1,138 @@
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Config
CustomClippingRect {
id: root
required property color accent
required property string icon
required property string label
required property string subLabel
required property real temperature
required property real usage
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Math.max(tempProg.implicitHeight + detailsRow.implicitHeight + Appearance.spacing.large, usageColumn.implicitHeight + usageLabel.implicitHeight) + Appearance.padding.large * 2
implicitWidth: 450
radius: Appearance.rounding.large - Appearance.padding.normal
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
color: Qt.alpha(root.accent, 0.05)
implicitWidth: parent.width * root.usage
Behavior on implicitWidth {
Anim {
}
}
}
CircularProgress {
id: tempProg
anchors.left: parent.left
anchors.margins: Appearance.padding.large
anchors.top: parent.top
fgColor: root.accent
implicitSize: Math.max(icon.implicitWidth, icon.implicitHeight) + Appearance.padding.larger * 2
spacing: Appearance.spacing.extraSmall
strokeWidth: Appearance.padding.extraSmall
value: root.usage
Behavior on clampedVal {
Anim {
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
color: root.accent
text: root.icon
}
}
ColumnLayout {
anchors.left: tempProg.right
anchors.margins: Appearance.spacing.large
anchors.right: usageColumn.left
anchors.verticalCenter: tempProg.verticalCenter
spacing: Appearance.spacing.extraSmall
CustomText {
color: root.accent
text: root.label
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3onSurfaceVariant
elide: Text.ElideRight
text: root.subLabel
}
}
ColumnLayout {
id: detailsRow
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Appearance.padding.largeIncreased
spacing: Appearance.spacing.extraSmall
RowLayout {
Layout.leftMargin: -Appearance.padding.extraSmall
spacing: Appearance.spacing.extraSmall
MaterialIcon {
Layout.topMargin: Math.round(fontInfo.pointSize * 0.08)
color: root.temperature > 90 ? DynamicColors.palette.m3error : root.accent
fill: 1
text: root.temperature > 90 ? "thermometer_alert" : "thermometer"
}
CustomText {
text: `${Math.ceil(root.temperature)}°${"C"}`
}
}
CustomProgressBar {
fgColor: root.accent
implicitHeight: Appearance.padding.small
indeterminate: isNaN(root.usage) || isNaN(root.temperature)
value: root.temperature / 100
}
}
Column {
id: usageColumn
anchors.margins: Appearance.padding.large
anchors.right: parent.right
anchors.rightMargin: 32
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
id: usageLabel
anchors.right: parent.right
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
text: qsTr("Usage")
}
CustomText {
anchors.right: parent.right
color: root.accent
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: isNaN(root.usage) ? "...%" : Math.round(root.usage * 100) + "%"
}
}
}
+85
View File
@@ -0,0 +1,85 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Services
import qs.Components
import qs.Config
CustomRect {
id: root
readonly property color accent: DynamicColors.palette.m3tertiary
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
implicitWidth: layout.implicitWidth + Appearance.padding.extraLargeIncreased * 2
radius: Appearance.rounding.medium
ServiceRef {
service: Memory
}
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: Appearance.spacing.extraSmall
RowLayout {
Layout.leftMargin: -Appearance.padding.extraSmall
spacing: Appearance.spacing.small
MaterialIcon {
color: root.accent
fill: 1
text: "memory_alt"
}
CustomText {
text: qsTr("Memory")
}
}
CircularProgress {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Appearance.spacing.large
fgColor: root.accent
implicitSize: usageColumn.implicitHeight + thickness + Appearance.padding.largeIncreased * 2
startAngle: -225
sweepAngle: 270
value: Memory.percentage
Behavior on clampedVal {
Anim {
}
}
ColumnLayout {
id: usageColumn
anchors.centerIn: parent
anchors.verticalCenterOffset: Appearance.padding.extraSmall
spacing: 0
CustomText {
Layout.alignment: Qt.AlignHCenter
color: root.accent
text: Math.round(Memory.percentage * 100) + "%"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
text: qsTr("Used")
}
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
text: {
const fmt = UsageFmt.formatKib(Memory.used, Memory.total);
return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`;
}
}
}
}
+183
View File
@@ -0,0 +1,183 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Internal
import qs.Helpers
import qs.Components
import qs.Config
CustomRect {
id: root
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: 220
implicitWidth: 300
radius: Appearance.rounding.large - Appearance.padding.normal
Ref {
service: NetworkUsage
}
ColumnLayout {
id: layout
anchors.bottomMargin: Appearance.padding.larger
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: 0
RowLayout {
spacing: Appearance.spacing.small
MaterialIcon {
color: DynamicColors.palette.m3primary
text: "swap_vert"
}
CustomText {
text: qsTr("Network")
}
}
Item {
Layout.bottomMargin: Appearance.spacing.small
Layout.fillHeight: true
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.larger
SparklineItem {
id: sparkline
property real smoothMax: targetMax
property real targetMax: 1024
anchors.fill: parent
historyLength: NetworkUsage.historyLength
line1: NetworkUsage.uploadBuffer // qmllint disable missing-type
line1Color: DynamicColors.palette.m3secondary
line1FillAlpha: 0.15
line2: NetworkUsage.downloadBuffer // qmllint disable missing-type
line2Color: DynamicColors.palette.m3tertiary
line2FillAlpha: 0.2
maxValue: smoothMax
Behavior on smoothMax {
Anim {
}
}
Connections {
function onValuesChanged(): void {
sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024);
slideAnim.restart();
}
target: NetworkUsage.downloadBuffer
}
NumberAnimation {
id: slideAnim
duration: Config.dashboard.resourceUpdateInterval
easing.type: Easing.Linear
from: 0
property: "slideProgress"
target: sparkline
to: 1
}
}
// "Collecting data" placeholder
CustomText {
anchors.centerIn: parent
color: DynamicColors.palette.m3outline
text: qsTr("Collecting data...")
visible: NetworkUsage.downloadBuffer.count < 2
}
}
// Download row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
color: DynamicColors.palette.m3tertiary
text: "download"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
text: qsTr("Download")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3tertiary
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
}
}
// Upload row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
color: DynamicColors.palette.m3secondary
text: "upload"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
text: qsTr("Upload")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3secondary
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
}
}
// Session totals
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
text: "history"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
text: qsTr("Total")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
text: {
const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0);
const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0);
return (down && up) ? `${down.value.toFixed(1)}${down.unit} ${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B";
}
}
}
}
}
+127
View File
@@ -0,0 +1,127 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Services
import qs.Components
import qs.Config
CustomRect {
id: root
readonly property color accent: DynamicColors.palette.m3secondary
readonly property real percentage: Storage.primaryDisk?.perc ?? 0
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
radius: Appearance.rounding.large - Appearance.padding.normal
ServiceRef {
service: Storage
}
ColumnLayout {
id: layout
anchors.left: parent.left
anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
RowLayout {
id: row
Layout.alignment: Qt.AlignHCenter
spacing: Appearance.spacing.large
CircularProgress {
fgColor: root.accent
implicitSize: usageColumn.implicitHeight + thickness + Appearance.padding.large * 2
startAngle: -225
sweepAngle: 270
value: root.percentage
Behavior on clampedVal {
Anim {
}
}
ColumnLayout {
id: usageColumn
anchors.centerIn: parent
spacing: 0
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
color: root.accent
font.pointSize: Appearance.font.size.medium
text: "hard_drive"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: root.accent
text: Math.round(root.percentage * 100) + "%"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
text: qsTr("Used")
}
}
}
ColumnLayout {
spacing: Appearance.spacing.extraSmall
CustomText {
text: qsTr("Storage")
}
CustomText {
color: root.accent
text: {
if (!Storage.primaryDisk)
return qsTr("No disks detected");
const fmt = UsageFmt.formatKib(Storage.primaryDisk.used, Storage.primaryDisk.total);
return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`;
}
}
}
}
CustomSplitButton {
Layout.alignment: Qt.AlignHCenter
active: menuItems.find(m => m.modelData === Storage.primaryDisk) ?? menuItems[0] ?? null
enabled: Storage.disks.length
fallbackIcon: "storage"
fallbackText: qsTr("No disks")
menuItems: disks.instances
minLeftWidth: row.implicitWidth * 0.6
type: CustomSplitButton.Tonal
menu.onItemSelected: item => Storage.manualPrimaryDisk = (item as DiskItem).modelData
Variants {
id: disks
model: Storage.disks
DiskItem {
}
}
}
}
component DiskItem: MenuItem {
required property var modelData
activeIcon: "storage"
icon: modelData === Storage.primaryDisk ? "check" : ""
text: modelData.mount
}
}
+71 -743
View File
@@ -1,792 +1,120 @@
import Quickshell
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Services.UPower
import ZShell.Internal
import qs.Components
import ZShell.Services
import qs.Modules.Resources.Cards
import qs.Helpers
import qs.Components
import qs.Config
Item {
id: root
readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2
readonly property real nonAnimHeight: content.implicitHeight + Appearance.padding.normal * 2
readonly property real nonAnimWidth: Math.max(minWidth, content.implicitWidth) + Appearance.padding.normal * 2
required property real padding
required property PersistentProperties visibilities
function displayTemp(temp: real): string {
return `${Math.ceil(temp)}°C`;
}
implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth
implicitHeight: content.implicitHeight + Appearance.padding.normal * 2
implicitWidth: content.implicitWidth
RowLayout {
id: content
anchors.left: parent.left
anchors.leftMargin: root.padding
anchors.margins: Appearance.padding.normal
anchors.right: parent.right
anchors.rightMargin: root.padding
anchors.verticalCenter: parent.verticalCenter
spacing: Appearance.spacing.normal
Ref {
service: SystemUsage
}
spacing: Appearance.spacing.larger
ColumnLayout {
id: mainColumn
Layout.fillWidth: true
spacing: Appearance.spacing.normal
spacing: Appearance.spacing.larger
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE")
spacing: Appearance.spacing.larger
visible: cpuCard.active || gpuCard.active
HeroCard {
Layout.fillWidth: true
Layout.minimumWidth: 400
Layout.preferredHeight: 150
accentColor: DynamicColors.palette.m3primary
icon: "memory"
mainLabel: qsTr("Usage")
mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%`
secondaryLabel: qsTr("Temp")
secondaryValue: root.displayTemp(SystemUsage.cpuTemp)
temperature: SystemUsage.cpuTemp
title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU")
usage: SystemUsage.cpuPerc
visible: Config.dashboard.performance.showCpu
}
WrappedLoader {
id: cpuCard
HeroCard {
Layout.fillWidth: true
Layout.minimumWidth: 400
Layout.preferredHeight: 150
accentColor: DynamicColors.palette.m3secondary
icon: "desktop_windows"
mainLabel: qsTr("Usage")
mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%`
secondaryLabel: qsTr("Temp")
secondaryValue: root.displayTemp(SystemUsage.gpuTemp)
temperature: SystemUsage.gpuTemp
title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU")
usage: SystemUsage.gpuPerc
visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE"
}
}
active: Config.dashboard.performance.showCpu
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork
sourceComponent: HeroCard {
accent: DynamicColors.palette.m3primary
icon: "memory"
label: qsTr("CPU")
subLabel: Cpu.name
temperature: Cpu.temperature
usage: Cpu.percentage
GaugeCard {
Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork
Layout.minimumWidth: 250
Layout.preferredHeight: 220
accentColor: DynamicColors.palette.m3tertiary
icon: "memory_alt"
percentage: SystemUsage.memPerc
subtitle: {
const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed);
const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal);
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
}
title: qsTr("Memory")
visible: Config.dashboard.performance.showMemory
}
StorageGaugeCard {
Layout.fillWidth: !Config.dashboard.performance.showNetwork
Layout.minimumWidth: 250
Layout.preferredHeight: 220
visible: Config.dashboard.performance.showStorage
}
NetworkCard {
Layout.fillWidth: true
Layout.minimumWidth: 200
Layout.preferredHeight: 220
visible: Config.dashboard.performance.showNetwork
}
}
}
BatteryTank {
Layout.preferredHeight: mainColumn.implicitHeight
Layout.preferredWidth: 120
visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery
}
}
component BatteryTank: CustomClippingRect {
id: batteryTank
property color accentColor: DynamicColors.palette.m3primary
property real animatedPercentage: 0
property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging
property real percentage: UPower.displayDevice.percentage
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - Appearance.padding.normal
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage
// Background Fill
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
color: Qt.alpha(batteryTank.accentColor, 0.15)
height: parent.height * batteryTank.animatedPercentage
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
// Header Section
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
color: batteryTank.accentColor
font.pointSize: Appearance.font.size.large
text: {
if (!UPower.displayDevice.isLaptopBattery) {
if (PowerProfiles.profile === PowerProfile.PowerSaver)
return "energy_savings_leaf";
if (PowerProfiles.profile === PowerProfile.Performance)
return "rocket_launch";
return "balance";
ServiceRef {
service: Cpu
}
if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)
return "battery_full";
const perc = UPower.displayDevice.percentage;
const charging = [UPowerDeviceState.Charging, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state);
if (perc >= 0.99)
return "battery_full";
let level = Math.floor(perc * 7);
if (charging && (level === 4 || level === 1))
level--;
return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`;
}
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3onSurface
font.pointSize: Appearance.font.size.normal
text: qsTr("Battery")
WrappedLoader {
id: gpuCard
active: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== ""
sourceComponent: HeroCard {
accent: DynamicColors.palette.m3secondary
icon: "desktop_windows"
label: qsTr("GPU")
subLabel: SystemUsage.gpuName
temperature: SystemUsage.gpuTemp
usage: SystemUsage.gpuPerc
Ref {
service: SystemUsage
}
}
}
}
Item {
Layout.fillHeight: true
}
RowLayout {
spacing: Appearance.spacing.larger
visible: storageCard.active || networkCard.active || memoryCard.active
// Bottom Info Section
ColumnLayout {
Layout.fillWidth: true
spacing: -4
WrappedLoader {
id: storageCard
CustomText {
Layout.alignment: Qt.AlignRight
color: batteryTank.accentColor
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: `${Math.round(batteryTank.percentage * 100)}%`
active: Config.dashboard.performance.showStorage
sourceComponent: StorageCard {
}
}
CustomText {
Layout.alignment: Qt.AlignRight
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.smaller
text: {
if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)
return qsTr("Full");
WrappedLoader {
id: memoryCard
if (batteryTank.isCharging)
return qsTr("Charging");
active: Config.dashboard.performance.showMemory
const s = UPower.displayDevice.timeToEmpty;
if (s === 0)
return qsTr("...");
sourceComponent: MemoryCard {
}
}
const hr = Math.floor(s / 3600);
const min = Math.floor((s % 3600) / 60);
if (hr > 0)
return `${hr}h ${min}m`;
WrappedLoader {
id: networkCard
return `${min}m`;
active: Config.dashboard.performance.showNetwork
sourceComponent: NetworkCard {
}
}
}
}
}
component CardHeader: RowLayout {
property color accentColor: DynamicColors.palette.m3primary
property string icon
property string title
WrappedLoader {
Layout.fillWidth: false
active: Battery.isLaptop && Config.dashboard.performance.showBattery
sourceComponent: BatteryTank {
}
}
}
component WrappedLoader: Loader {
Layout.fillHeight: true
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
color: parent.accentColor
fill: 1
font.pointSize: Appearance.spacing.large
text: parent.icon
}
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Appearance.font.size.normal
text: parent.title
}
}
component GaugeCard: CustomRect {
id: gaugeCard
property color accentColor: DynamicColors.palette.m3primary
property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
property string icon
property real percentage: 0
property string subtitle
property string title
clip: true
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - Appearance.padding.normal
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.smaller
CardHeader {
accentColor: gaugeCard.accentColor
icon: gaugeCard.icon
title: gaugeCard.title
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
ArcGauge {
accentColor: gaugeCard.accentColor
anchors.centerIn: parent
height: width
percentage: gaugeCard.animatedPercentage
startAngle: gaugeCard.arcStartAngle
sweepAngle: gaugeCard.arcSweep
trackColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
width: Math.min(parent.width, parent.height)
}
CustomText {
anchors.centerIn: parent
color: gaugeCard.accentColor
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: `${Math.round(gaugeCard.percentage * 100)}%`
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.smaller
text: gaugeCard.subtitle
}
}
}
component HeroCard: CustomClippingRect {
id: heroCard
property color accentColor: DynamicColors.palette.m3primary
property real animatedTemp: 0
property real animatedUsage: 0
property string icon
property string mainLabel
property string mainValue
readonly property real maxTemp: 100
property string secondaryLabel
property string secondaryValue
readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp))
property real temperature: 0
property string title
property real usage: 0
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - Appearance.padding.normal
Behavior on animatedTemp {
Anim {
duration: Appearance.anim.durations.large
}
}
Behavior on animatedUsage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: {
animatedUsage = usage;
animatedTemp = tempProgress;
}
onTempProgressChanged: animatedTemp = tempProgress
onUsageChanged: animatedUsage = usage
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
color: Qt.alpha(heroCard.accentColor, 0.15)
implicitWidth: parent.width * heroCard.animatedUsage
}
CardHeader {
accentColor: heroCard.accentColor
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.large
anchors.top: parent.top
anchors.topMargin: Math.round(Appearance.padding.large * 1.2)
icon: heroCard.icon
title: heroCard.title
width: parent.width - anchors.leftMargin - usageColumn.anchors.rightMargin - usageLabel.width - Appearance.spacing.normal
}
Column {
anchors.bottom: parent.bottom
anchors.bottomMargin: Math.round(Appearance.padding.large * 1.3)
anchors.left: parent.left
anchors.margins: Math.round(Appearance.padding.large * 1.2)
anchors.right: parent.right
spacing: Appearance.spacing.small
Row {
spacing: Appearance.spacing.small
CustomText {
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
text: heroCard.secondaryValue
}
CustomText {
anchors.baseline: parent.children[0].baseline
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: heroCard.secondaryLabel
}
}
ProgressBar {
bgColor: Qt.alpha(heroCard.accentColor, 0.2)
fgColor: heroCard.accentColor
implicitHeight: 6
implicitWidth: parent.width * 0.5
value: heroCard.tempProgress
}
}
Column {
id: usageColumn
anchors.margins: Appearance.padding.large
anchors.right: parent.right
anchors.rightMargin: 32
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
id: usageLabel
anchors.right: parent.right
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
text: heroCard.mainLabel
}
CustomText {
anchors.right: parent.right
color: heroCard.accentColor
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: heroCard.mainValue
}
}
}
component NetworkCard: CustomRect {
id: networkCard
property color accentColor: DynamicColors.palette.m3primary
clip: true
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - Appearance.padding.normal
Ref {
service: NetworkUsage
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
CardHeader {
accentColor: networkCard.accentColor
icon: "swap_vert"
title: qsTr("Network")
}
// Sparkline graph
Item {
Layout.fillHeight: true
Layout.fillWidth: true
SparklineItem {
id: sparkline
property real smoothMax: targetMax
property real targetMax: 1024
anchors.fill: parent
historyLength: NetworkUsage.historyLength
line1: NetworkUsage.uploadBuffer // qmllint disable missing-type
line1Color: DynamicColors.palette.m3secondary
line1FillAlpha: 0.15
line2: NetworkUsage.downloadBuffer // qmllint disable missing-type
line2Color: DynamicColors.palette.m3tertiary
line2FillAlpha: 0.2
maxValue: smoothMax
Behavior on smoothMax {
Anim {
duration: Appearance.anim.durations.large
}
}
Connections {
function onValuesChanged(): void {
sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024);
slideAnim.restart();
}
target: NetworkUsage.downloadBuffer
}
NumberAnimation {
id: slideAnim
duration: Config.dashboard.resourceUpdateInterval
from: 0
property: "slideProgress"
target: sparkline
to: 1
}
}
// "No data" placeholder
CustomText {
anchors.centerIn: parent
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
opacity: 0.6
text: qsTr("Collecting data...")
visible: NetworkUsage.downloadBuffer.count < 2
}
}
// Download row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
text: "download"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: qsTr("Download")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
}
}
// Upload row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
color: DynamicColors.palette.m3secondary
font.pointSize: Appearance.font.size.normal
text: "upload"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: qsTr("Upload")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3secondary
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
}
}
// Session totals
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
text: "history"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: qsTr("Total")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: {
const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0);
const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0);
return (down && up) ? `${down.value.toFixed(1)}${down.unit} ${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B";
}
}
}
}
}
component ProgressBar: CustomRect {
id: progressBar
property real animatedValue: 0
property color bgColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
property color fgColor: DynamicColors.palette.m3primary
property real value: 0
color: bgColor
radius: Appearance.rounding.full
Behavior on animatedValue {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: animatedValue = value
onValueChanged: animatedValue = value
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
color: progressBar.fgColor
radius: Appearance.rounding.full
width: parent.width * progressBar.animatedValue
}
}
component StorageGaugeCard: CustomRect {
id: storageGaugeCard
property color accentColor: DynamicColors.palette.m3secondary
property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null
property int currentDiskIndex: 0
property int diskCount: 0
clip: true
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - Appearance.padding.normal
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: {
diskCount = SystemUsage.disks.length;
if (currentDisk)
animatedPercentage = currentDisk.perc;
}
onCurrentDiskChanged: {
if (currentDisk)
animatedPercentage = currentDisk.perc;
}
// Update diskCount and animatedPercentage when disks data changes
Connections {
function onDisksChanged() {
if (SystemUsage.disks.length !== storageGaugeCard.diskCount)
storageGaugeCard.diskCount = SystemUsage.disks.length;
// Update animated percentage when disk data refreshes
if (storageGaugeCard.currentDisk)
storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc;
}
target: SystemUsage
}
MouseArea {
anchors.fill: parent
onWheel: wheel => {
if (wheel.angleDelta.y > 0)
storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount;
else if (wheel.angleDelta.y < 0)
storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount;
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.smaller
CardHeader {
accentColor: storageGaugeCard.accentColor
icon: "hard_disk"
title: {
const base = qsTr("Storage");
if (!storageGaugeCard.currentDisk)
return base;
return `${base} - ${storageGaugeCard.currentDisk.mount}`;
}
// Scroll hint icon
MaterialIcon {
ToolTip.delay: 500
ToolTip.text: qsTr("Scroll to switch disks")
ToolTip.visible: hintHover.hovered
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
opacity: 0.7
text: "unfold_more"
visible: storageGaugeCard.diskCount > 1
HoverHandler {
id: hintHover
}
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
ArcGauge {
accentColor: storageGaugeCard.accentColor
anchors.centerIn: parent
height: width
percentage: storageGaugeCard.animatedPercentage
startAngle: storageGaugeCard.arcStartAngle
sweepAngle: storageGaugeCard.arcSweep
trackColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
width: Math.min(parent.width, parent.height)
}
CustomText {
anchors.centerIn: parent
color: storageGaugeCard.accentColor
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—"
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.smaller
text: {
if (!storageGaugeCard.currentDisk)
return "—";
const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used);
const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total);
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
}
}
}
visible: active
}
}
-2
View File
@@ -34,8 +34,6 @@ Item {
anchors.centerIn: parent
sourceComponent: Content {
padding: Appearance.padding.normal
visibilities: root.visibilities
}
}
}