summaryrefslogtreecommitdiff
path: root/modules/dashboard/Performance.qml
diff options
context:
space:
mode:
authorThanh Minh <112760114+tmih06@users.noreply.github.com>2026-02-19 18:53:22 +0700
committerGitHub <noreply@github.com>2026-02-19 22:53:22 +1100
commit46174d1934370b2f4a7da43a3dbc0289c14a5a2d (patch)
tree2f401649de42e204f9904ed7797a3600e4654b57 /modules/dashboard/Performance.qml
parentfeat: add wallpaperEnabled option (#1187) (diff)
downloadcaelestia-shell-46174d1934370b2f4a7da43a3dbc0289c14a5a2d.tar.gz
caelestia-shell-46174d1934370b2f4a7da43a3dbc0289c14a5a2d.tar.bz2
caelestia-shell-46174d1934370b2f4a7da43a3dbc0289c14a5a2d.zip
dashboard/performance: new design, configurable, controlcenter support (#975)
* feat(dashboard): add configurable performance resources - Add config options to show/hide Battery, GPU, CPU, Memory, Storage - Make dashboard responsive based on number of visible resources - Scale resource sizes and spacing dynamically for 3, 4, or 5 items - Battery shows charge status and time remaining/to full - Each resource can be individually toggled via config * fix(dashboard): add dynamic right margin for last visible resource Ensures the rightmost resource always has proper margin to prevent content from being cut off at the edge * fix(performance): comment out duplicated value2 properties for memory and storage resources * controlcenter: add settings for dashboard * feat: handle readonly properties and re-usable codes * Feature/performance tab rework (#5) * dashboard/performance: rework tab with card-based grid layout - Replace circular arc meters with card-based grid layout - CPU/GPU cards show hardware name, usage and temperature with horizontal bars - Memory card with 3/4 arc indicator and used/total at bottom - Storage card shows physical disks from lsblk with aggregated partition usage - Add cpuName, gpuName, cpuFreq, cpuMaxFreq, disks properties to SystemUsage - Clean hardware names (remove Intel/AMD/NVIDIA prefixes, TM/R symbols) * dashboard/performance: new hero card design * dashboard/performance: update storage indicators to be reponsive to the physical disks count * dashboard/performance: fix the overlay bounding issue * dashboard/perfromance: refactor code * dashboard/performance: add battery gauge * dashboard/performance: correct battery icon * dashboard/performance: configurable battery * dashboard/performance: update layout * dashboard/performance: move the "Usage" text on top and smaller the font size * dashboard/performance: add a lot of configurations * dashboard/performance: add network metrics * fix: issue with hot reload * chore: update default vaule for mainValueSpacing to 0 * chore: group settings into collapasible sections * chore: making GPU & Battery toggle not showing if not found * chore: fix network widget spacing & text * chore: remove old disk bars configs, add update interval * chore: remove old & unused value, functions * chore: network graph update smoothly when data points change * chore: refactor settings - de-flood settings, most of the font & size setting now follow the global Appearance config - Most of sliders are not needed anymore, only keep the update interval slider - clean up * chore: remove readonly properties from the controlcenter/dashboard. * chore: minor fix * fix: fix warning about onPercChange() * fix: network metrics negative number * fix: add minimal height & width, placeholder for none toggled * fix: network graph move smoothly (#6) * fix: network graph move smoothly * clean up * fix: graph animation even more smooth * fix: padding issue * chore: network icons short description * fix --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
Diffstat (limited to 'modules/dashboard/Performance.qml')
-rw-r--r--modules/dashboard/Performance.qml1026
1 files changed, 883 insertions, 143 deletions
diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml
index 5e00d89..a4e24c4 100644
--- a/modules/dashboard/Performance.qml
+++ b/modules/dashboard/Performance.qml
@@ -1,227 +1,967 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Services.UPower
import qs.components
import qs.components.misc
-import qs.services
import qs.config
-import QtQuick
-import QtQuick.Layouts
+import qs.services
-RowLayout {
+Item {
id: root
- readonly property int padding: Appearance.padding.large
+ readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2
function displayTemp(temp: real): string {
return `${Math.ceil(Config.services.useFahrenheit ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheit ? "F" : "C"}`;
}
- spacing: Appearance.spacing.large * 3
+ implicitWidth: Math.max(minWidth, content.implicitWidth)
+ implicitHeight: placeholder.visible ? placeholder.height : content.implicitHeight
- Ref {
- service: SystemUsage
- }
+ StyledRect {
+ id: placeholder
+
+ anchors.centerIn: parent
+ width: 400
+ height: 350
+ radius: Appearance.rounding.large
+ color: Colours.tPalette.m3surfaceContainer
+ visible: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery)
- Resource {
- Layout.alignment: Qt.AlignVCenter
- Layout.topMargin: root.padding
- Layout.bottomMargin: root.padding
- Layout.leftMargin: root.padding * 2
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: Appearance.spacing.normal
- value1: Math.min(1, SystemUsage.gpuTemp / 90)
- value2: SystemUsage.gpuPerc
+ MaterialIcon {
+ Layout.alignment: Qt.AlignHCenter
+ text: "tune"
+ font.pointSize: Appearance.font.size.extraLarge * 2
+ color: Colours.palette.m3onSurfaceVariant
+ }
- label1: root.displayTemp(SystemUsage.gpuTemp)
- label2: `${Math.round(SystemUsage.gpuPerc * 100)}%`
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ text: qsTr("No widgets enabled")
+ font.pointSize: Appearance.font.size.large
+ color: Colours.palette.m3onSurface
+ }
- sublabel1: qsTr("GPU temp")
- sublabel2: qsTr("Usage")
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ text: qsTr("Enable widgets in dashboard settings")
+ font.pointSize: Appearance.font.size.small
+ color: Colours.palette.m3onSurfaceVariant
+ }
+ }
}
- Resource {
- Layout.alignment: Qt.AlignVCenter
- Layout.topMargin: root.padding
- Layout.bottomMargin: root.padding
+ RowLayout {
+ id: content
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ spacing: Appearance.spacing.normal
+ visible: !placeholder.visible
+
+ Ref {
+ service: SystemUsage
+ }
+
+ ColumnLayout {
+ id: mainColumn
+
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.normal
- primary: true
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.normal
+ visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE")
- value1: Math.min(1, SystemUsage.cpuTemp / 90)
- value2: SystemUsage.cpuPerc
+ HeroCard {
+ Layout.fillWidth: true
+ Layout.minimumWidth: 400
+ Layout.preferredHeight: 150
+ visible: Config.dashboard.performance.showCpu
+ icon: "memory"
+ title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU")
+ mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%`
+ mainLabel: qsTr("Usage")
+ secondaryValue: root.displayTemp(SystemUsage.cpuTemp)
+ secondaryLabel: qsTr("Temp")
+ usage: SystemUsage.cpuPerc
+ temperature: SystemUsage.cpuTemp
+ accentColor: Colours.palette.m3primary
+ }
- label1: root.displayTemp(SystemUsage.cpuTemp)
- label2: `${Math.round(SystemUsage.cpuPerc * 100)}%`
+ HeroCard {
+ Layout.fillWidth: true
+ Layout.minimumWidth: 400
+ Layout.preferredHeight: 150
+ visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE"
+ icon: "desktop_windows"
+ title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU")
+ mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%`
+ mainLabel: qsTr("Usage")
+ secondaryValue: root.displayTemp(SystemUsage.gpuTemp)
+ secondaryLabel: qsTr("Temp")
+ usage: SystemUsage.gpuPerc
+ temperature: SystemUsage.gpuTemp
+ accentColor: Colours.palette.m3secondary
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.normal
+ visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork
+
+ GaugeCard {
+ Layout.minimumWidth: 250
+ Layout.preferredHeight: 220
+ Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork
+ icon: "memory_alt"
+ title: qsTr("Memory")
+ 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}`;
+ }
+ accentColor: Colours.palette.m3tertiary
+ visible: Config.dashboard.performance.showMemory
+ }
- sublabel1: qsTr("CPU temp")
- sublabel2: qsTr("Usage")
+ StorageGaugeCard {
+ Layout.minimumWidth: 250
+ Layout.preferredHeight: 220
+ Layout.fillWidth: !Config.dashboard.performance.showNetwork
+ visible: Config.dashboard.performance.showStorage
+ }
+
+ NetworkCard {
+ Layout.fillWidth: true
+ Layout.minimumWidth: 200
+ Layout.preferredHeight: 220
+ visible: Config.dashboard.performance.showNetwork
+ }
+ }
+ }
+
+ BatteryTank {
+ Layout.preferredWidth: 120
+ Layout.preferredHeight: mainColumn.implicitHeight
+ visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery
+ }
}
- Resource {
- Layout.alignment: Qt.AlignVCenter
- Layout.topMargin: root.padding
- Layout.bottomMargin: root.padding
- Layout.rightMargin: root.padding * 3
+ component BatteryTank: StyledClippingRect {
+ id: batteryTank
+
+ property real percentage: UPower.displayDevice.percentage
+ property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging
+ property color accentColor: Colours.palette.m3primary
+ property real animatedPercentage: 0
- value1: SystemUsage.memPerc
- value2: SystemUsage.storagePerc
+ color: Colours.tPalette.m3surfaceContainer
+ radius: Appearance.rounding.large
+ Component.onCompleted: animatedPercentage = percentage
+ onPercentageChanged: animatedPercentage = percentage
- label1: {
- const fmt = SystemUsage.formatKib(SystemUsage.memUsed);
- return `${+fmt.value.toFixed(1)}${fmt.unit}`;
+ // Background Fill
+ StyledRect {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: parent.height * batteryTank.animatedPercentage
+ color: Qt.alpha(batteryTank.accentColor, 0.15)
}
- label2: {
- const fmt = SystemUsage.formatKib(SystemUsage.storageUsed);
- return `${Math.floor(fmt.value)}${fmt.unit}`;
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.large
+ spacing: Appearance.spacing.small
+
+ // Header Section
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.small
+
+ MaterialIcon {
+ text: {
+ if (!UPower.displayDevice.isLaptopBattery) {
+ if (PowerProfiles.profile === PowerProfile.PowerSaver)
+ return "energy_savings_leaf";
+
+ if (PowerProfiles.profile === PowerProfile.Performance)
+ return "rocket_launch";
+
+ return "balance";
+ }
+ 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`;
+ }
+ font.pointSize: Appearance.font.size.large
+ color: batteryTank.accentColor
+ }
+
+ StyledText {
+ Layout.fillWidth: true
+ text: qsTr("Battery")
+ font.pointSize: Appearance.font.size.normal
+ color: Colours.palette.m3onSurface
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+
+ // Bottom Info Section
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: -4
+
+ StyledText {
+ Layout.alignment: Qt.AlignRight
+ text: `${Math.round(batteryTank.percentage * 100)}%`
+ font.pointSize: Appearance.font.size.extraLarge
+ font.weight: Font.Medium
+ color: batteryTank.accentColor
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignRight
+ text: {
+ if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)
+ return qsTr("Full");
+
+ if (batteryTank.isCharging)
+ 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`;
+ }
+ font.pointSize: Appearance.font.size.smaller
+ color: Colours.palette.m3onSurfaceVariant
+ }
+ }
}
- sublabel1: qsTr("Memory")
- sublabel2: qsTr("Storage")
+ Behavior on animatedPercentage {
+ Anim {
+ duration: Appearance.anim.durations.large
+ }
+ }
}
- component Resource: Item {
- id: res
+ component CardHeader: RowLayout {
+ property string icon
+ property string title
+ property color accentColor: Colours.palette.m3primary
+
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.small
- required property real value1
- required property real value2
- required property string sublabel1
- required property string sublabel2
- required property string label1
- required property string label2
+ MaterialIcon {
+ text: parent.icon
+ fill: 1
+ color: parent.accentColor
+ font.pointSize: Appearance.spacing.large
+ }
- property bool primary
- readonly property real primaryMult: primary ? 1.2 : 1
+ StyledText {
+ Layout.fillWidth: true
+ text: parent.title
+ font.pointSize: Appearance.font.size.normal
+ elide: Text.ElideRight
+ }
+ }
- readonly property real thickness: Config.dashboard.sizes.resourceProgessThickness * primaryMult
+ component ProgressBar: StyledRect {
+ id: progressBar
- property color fg1: Colours.palette.m3primary
- property color fg2: Colours.palette.m3secondary
- property color bg1: Colours.palette.m3primaryContainer
- property color bg2: Colours.palette.m3secondaryContainer
+ property real value: 0
+ property color fgColor: Colours.palette.m3primary
+ property color bgColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
+ property real animatedValue: 0
- implicitWidth: Config.dashboard.sizes.resourceSize * primaryMult
- implicitHeight: Config.dashboard.sizes.resourceSize * primaryMult
+ color: bgColor
+ radius: Appearance.rounding.full
+ Component.onCompleted: animatedValue = value
+ onValueChanged: animatedValue = value
- onValue1Changed: canvas.requestPaint()
- onValue2Changed: canvas.requestPaint()
- onFg1Changed: canvas.requestPaint()
- onFg2Changed: canvas.requestPaint()
- onBg1Changed: canvas.requestPaint()
- onBg2Changed: canvas.requestPaint()
+ StyledRect {
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ width: parent.width * progressBar.animatedValue
+ color: progressBar.fgColor
+ radius: Appearance.rounding.full
+ }
- Column {
- anchors.centerIn: parent
+ Behavior on animatedValue {
+ Anim {
+ duration: Appearance.anim.durations.large
+ }
+ }
+ }
- StyledText {
- anchors.horizontalCenter: parent.horizontalCenter
+ component HeroCard: StyledClippingRect {
+ id: heroCard
+
+ property string icon
+ property string title
+ property string mainValue
+ property string mainLabel
+ property string secondaryValue
+ property string secondaryLabel
+ property real usage: 0
+ property real temperature: 0
+ property color accentColor: Colours.palette.m3primary
+ readonly property real maxTemp: 100
+ readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp))
+ property real animatedUsage: 0
+ property real animatedTemp: 0
- text: res.label1
- font.pointSize: Appearance.font.size.extraLarge * res.primaryMult
+ color: Colours.tPalette.m3surfaceContainer
+ radius: Appearance.rounding.large
+ Component.onCompleted: {
+ animatedUsage = usage;
+ animatedTemp = tempProgress;
+ }
+ onUsageChanged: animatedUsage = usage
+ onTempProgressChanged: animatedTemp = tempProgress
+
+ StyledRect {
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ width: parent.width * heroCard.animatedUsage
+ color: Qt.alpha(heroCard.accentColor, 0.15)
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.leftMargin: Appearance.padding.large
+ anchors.rightMargin: Appearance.padding.large
+ anchors.topMargin: Appearance.padding.normal
+ anchors.bottomMargin: Appearance.padding.normal
+ spacing: Appearance.spacing.small
+
+ CardHeader {
+ icon: heroCard.icon
+ title: heroCard.title
+ accentColor: heroCard.accentColor
}
- StyledText {
- anchors.horizontalCenter: parent.horizontalCenter
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: Appearance.spacing.normal
- text: res.sublabel1
- color: Colours.palette.m3onSurfaceVariant
- font.pointSize: Appearance.font.size.smaller * res.primaryMult
+ Column {
+ Layout.alignment: Qt.AlignBottom
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.small
+
+ Row {
+ spacing: Appearance.spacing.small
+
+ StyledText {
+ text: heroCard.secondaryValue
+ font.pointSize: Appearance.font.size.normal
+ font.weight: Font.Medium
+ }
+
+ StyledText {
+ text: heroCard.secondaryLabel
+ font.pointSize: Appearance.font.size.small
+ color: Colours.palette.m3onSurfaceVariant
+ anchors.baseline: parent.children[0].baseline
+ }
+ }
+
+ ProgressBar {
+ width: parent.width * 0.5
+ height: 6
+ value: heroCard.tempProgress
+ fgColor: heroCard.accentColor
+ bgColor: Qt.alpha(heroCard.accentColor, 0.2)
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
}
}
Column {
- anchors.horizontalCenter: parent.right
- anchors.top: parent.verticalCenter
- anchors.horizontalCenterOffset: -res.thickness / 2
- anchors.topMargin: res.thickness / 2 + Appearance.spacing.small
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: Appearance.padding.large
+ anchors.rightMargin: 32
+ spacing: 0
StyledText {
- anchors.horizontalCenter: parent.horizontalCenter
-
- text: res.label2
- font.pointSize: Appearance.font.size.smaller * res.primaryMult
+ anchors.right: parent.right
+ text: heroCard.mainLabel
+ font.pointSize: Appearance.font.size.normal
+ color: Colours.palette.m3onSurfaceVariant
}
StyledText {
- anchors.horizontalCenter: parent.horizontalCenter
+ anchors.right: parent.right
+ text: heroCard.mainValue
+ font.pointSize: Appearance.font.size.extraLarge
+ font.weight: Font.Medium
+ color: heroCard.accentColor
+ }
+ }
- text: res.sublabel2
- color: Colours.palette.m3onSurfaceVariant
- font.pointSize: Appearance.font.size.small * res.primaryMult
+ Behavior on animatedUsage {
+ Anim {
+ duration: Appearance.anim.durations.large
+ }
+ }
+
+ Behavior on animatedTemp {
+ Anim {
+ duration: Appearance.anim.durations.large
}
}
+ }
+
+ component GaugeCard: StyledRect {
+ id: gaugeCard
- Canvas {
- id: canvas
+ property string icon
+ property string title
+ property real percentage: 0
+ property string subtitle
+ property color accentColor: Colours.palette.m3primary
+ readonly property real arcStartAngle: 0.75 * Math.PI
+ readonly property real arcSweep: 1.5 * Math.PI
+ property real animatedPercentage: 0
- readonly property real centerX: width / 2
- readonly property real centerY: height / 2
+ color: Colours.tPalette.m3surfaceContainer
+ radius: Appearance.rounding.large
+ clip: true
+ Component.onCompleted: animatedPercentage = percentage
+ onPercentageChanged: animatedPercentage = percentage
- readonly property real arc1Start: degToRad(45)
- readonly property real arc1End: degToRad(220)
- readonly property real arc2Start: degToRad(230)
- readonly property real arc2End: degToRad(360)
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.large
+ spacing: Appearance.spacing.smaller
- function degToRad(deg: int): real {
- return deg * Math.PI / 180;
+ CardHeader {
+ icon: gaugeCard.icon
+ title: gaugeCard.title
+ accentColor: gaugeCard.accentColor
}
- anchors.fill: parent
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ Canvas {
+ id: gaugeCanvas
- onPaint: {
- const ctx = getContext("2d");
- ctx.reset();
+ anchors.centerIn: parent
+ width: Math.min(parent.width, parent.height)
+ height: width
+ onPaint: {
+ const ctx = getContext("2d");
+ ctx.reset();
+ const cx = width / 2;
+ const cy = height / 2;
+ const radius = (Math.min(width, height) - 12) / 2;
+ const lineWidth = 10;
+ ctx.beginPath();
+ ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep);
+ ctx.lineWidth = lineWidth;
+ ctx.lineCap = "round";
+ ctx.strokeStyle = Colours.layer(Colours.palette.m3surfaceContainerHigh, 2);
+ ctx.stroke();
+ if (gaugeCard.animatedPercentage > 0) {
+ ctx.beginPath();
+ ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep * gaugeCard.animatedPercentage);
+ ctx.lineWidth = lineWidth;
+ ctx.lineCap = "round";
+ ctx.strokeStyle = gaugeCard.accentColor;
+ ctx.stroke();
+ }
+ }
+ Component.onCompleted: requestPaint()
- ctx.lineWidth = res.thickness;
- ctx.lineCap = Appearance.rounding.scale === 0 ? "square" : "round";
+ Connections {
+ function onAnimatedPercentageChanged() {
+ gaugeCanvas.requestPaint();
+ }
- const radius = (Math.min(width, height) - ctx.lineWidth) / 2;
- const cx = centerX;
- const cy = centerY;
- const a1s = arc1Start;
- const a1e = arc1End;
- const a2s = arc2Start;
- const a2e = arc2End;
+ target: gaugeCard
+ }
- ctx.beginPath();
- ctx.arc(cx, cy, radius, a1s, a1e, false);
- ctx.strokeStyle = res.bg1;
- ctx.stroke();
+ Connections {
+ function onPaletteChanged() {
+ gaugeCanvas.requestPaint();
+ }
- ctx.beginPath();
- ctx.arc(cx, cy, radius, a1s, (a1e - a1s) * res.value1 + a1s, false);
- ctx.strokeStyle = res.fg1;
- ctx.stroke();
+ target: Colours
+ }
+ }
- ctx.beginPath();
- ctx.arc(cx, cy, radius, a2s, a2e, false);
- ctx.strokeStyle = res.bg2;
- ctx.stroke();
+ StyledText {
+ anchors.centerIn: parent
+ text: `${Math.round(gaugeCard.percentage * 100)}%`
+ font.pointSize: Appearance.font.size.extraLarge
+ font.weight: Font.Medium
+ color: gaugeCard.accentColor
+ }
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ text: gaugeCard.subtitle
+ font.pointSize: Appearance.font.size.smaller
+ color: Colours.palette.m3onSurfaceVariant
+ }
+ }
- ctx.beginPath();
- ctx.arc(cx, cy, radius, a2s, (a2e - a2s) * res.value2 + a2s, false);
- ctx.strokeStyle = res.fg2;
- ctx.stroke();
+ Behavior on animatedPercentage {
+ Anim {
+ duration: Appearance.anim.durations.large
}
}
+ }
+
+ component StorageGaugeCard: StyledRect {
+ id: storageGaugeCard
+
+ property int currentDiskIndex: 0
+ readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null
+ property int diskCount: 0
+ readonly property real arcStartAngle: 0.75 * Math.PI
+ readonly property real arcSweep: 1.5 * Math.PI
+ property real animatedPercentage: 0
+ property color accentColor: Colours.palette.m3secondary
- Behavior on value1 {
- Anim {}
+ color: Colours.tPalette.m3surfaceContainer
+ radius: Appearance.rounding.large
+ clip: true
+ 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;
- Behavior on value2 {
- Anim {}
+ // Update animated percentage when disk data refreshes
+ if (storageGaugeCard.currentDisk)
+ storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc;
+ }
+
+ target: SystemUsage
}
- Behavior on fg1 {
- CAnim {}
+ 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;
+ }
}
- Behavior on fg2 {
- CAnim {}
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.large
+ spacing: Appearance.spacing.smaller
+
+ CardHeader {
+ icon: "hard_disk"
+ title: {
+ const base = qsTr("Storage");
+ if (!storageGaugeCard.currentDisk)
+ return base;
+
+ return `${base} - ${storageGaugeCard.currentDisk.mount}`;
+ }
+ accentColor: storageGaugeCard.accentColor
+
+ // Scroll hint icon
+ MaterialIcon {
+ text: "unfold_more"
+ color: Colours.palette.m3onSurfaceVariant
+ font.pointSize: Appearance.font.size.normal
+ visible: storageGaugeCard.diskCount > 1
+ opacity: 0.7
+ ToolTip.visible: hintHover.hovered
+ ToolTip.text: qsTr("Scroll to switch disks")
+ ToolTip.delay: 500
+
+ HoverHandler {
+ id: hintHover
+ }
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ Canvas {
+ id: storageGaugeCanvas
+
+ anchors.centerIn: parent
+ width: Math.min(parent.width, parent.height)
+ height: width
+ onPaint: {
+ const ctx = getContext("2d");
+ ctx.reset();
+ const cx = width / 2;
+ const cy = height / 2;
+ const radius = (Math.min(width, height) - 12) / 2;
+ const lineWidth = 10;
+ ctx.beginPath();
+ ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep);
+ ctx.lineWidth = lineWidth;
+ ctx.lineCap = "round";
+ ctx.strokeStyle = Colours.layer(Colours.palette.m3surfaceContainerHigh, 2);
+ ctx.stroke();
+ if (storageGaugeCard.animatedPercentage > 0) {
+ ctx.beginPath();
+ ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep * storageGaugeCard.animatedPercentage);
+ ctx.lineWidth = lineWidth;
+ ctx.lineCap = "round";
+ ctx.strokeStyle = storageGaugeCard.accentColor;
+ ctx.stroke();
+ }
+ }
+ Component.onCompleted: requestPaint()
+
+ Connections {
+ function onAnimatedPercentageChanged() {
+ storageGaugeCanvas.requestPaint();
+ }
+
+ target: storageGaugeCard
+ }
+
+ Connections {
+ function onPaletteChanged() {
+ storageGaugeCanvas.requestPaint();
+ }
+
+ target: Colours
+ }
+ }
+
+ StyledText {
+ anchors.centerIn: parent
+ text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—"
+ font.pointSize: Appearance.font.size.extraLarge
+ font.weight: Font.Medium
+ color: storageGaugeCard.accentColor
+ }
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ 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}`;
+ }
+ font.pointSize: Appearance.font.size.smaller
+ color: Colours.palette.m3onSurfaceVariant
+ }
}
- Behavior on bg1 {
- CAnim {}
+ Behavior on animatedPercentage {
+ Anim {
+ duration: Appearance.anim.durations.large
+ }
}
+ }
+
+ component NetworkCard: StyledRect {
+ id: networkCard
+
+ property color accentColor: Colours.palette.m3primary
- Behavior on bg2 {
- CAnim {}
+ color: Colours.tPalette.m3surfaceContainer
+ radius: Appearance.rounding.large
+ clip: true
+
+ Ref {
+ service: NetworkUsage
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.large
+ spacing: Appearance.spacing.small
+
+ CardHeader {
+ icon: "swap_vert"
+ title: qsTr("Network")
+ accentColor: networkCard.accentColor
+ }
+
+ // Sparkline graph
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ Canvas {
+ id: sparklineCanvas
+
+ property var downHistory: NetworkUsage.downloadHistory
+ property var upHistory: NetworkUsage.uploadHistory
+ property real targetMax: 1024
+ property real smoothMax: targetMax
+ property real slideProgress: 0
+ property int _tickCount: 0
+ property int _lastTickCount: -1
+
+ function checkAndAnimate(): void {
+ const currentLength = (downHistory || []).length;
+ if (currentLength > 0 && _tickCount !== _lastTickCount) {
+ _lastTickCount = _tickCount;
+ updateMax();
+ }
+ }
+
+ function updateMax(): void {
+ const downHist = downHistory || [];
+ const upHist = upHistory || [];
+ const allValues = downHist.concat(upHist);
+ targetMax = Math.max(...allValues, 1024);
+ requestPaint();
+ }
+
+ anchors.fill: parent
+ onDownHistoryChanged: checkAndAnimate()
+ onUpHistoryChanged: checkAndAnimate()
+ onSmoothMaxChanged: requestPaint()
+ onSlideProgressChanged: requestPaint()
+
+ onPaint: {
+ const ctx = getContext("2d");
+ ctx.reset();
+ const w = width;
+ const h = height;
+ const downHist = downHistory || [];
+ const upHist = upHistory || [];
+ if (downHist.length < 2 && upHist.length < 2)
+ return;
+
+ const maxVal = smoothMax;
+
+ const drawLine = (history, color, fillAlpha) => {
+ if (history.length < 2)
+ return;
+
+ const len = history.length;
+ const stepX = w / (NetworkUsage.historyLength - 1);
+ const startX = w - (len - 1) * stepX - stepX * slideProgress + stepX;
+ ctx.beginPath();
+ ctx.moveTo(startX, h - (history[0] / maxVal) * h);
+ for (let i = 1; i < len; i++) {
+ const x = startX + i * stepX;
+ const y = h - (history[i] / maxVal) * h;
+ ctx.lineTo(x, y);
+ }
+ ctx.strokeStyle = color;
+ ctx.lineWidth = 2;
+ ctx.lineCap = "round";
+ ctx.lineJoin = "round";
+ ctx.stroke();
+ ctx.lineTo(startX + (len - 1) * stepX, h);
+ ctx.lineTo(startX, h);
+ ctx.closePath();
+ ctx.fillStyle = Qt.rgba(Qt.color(color).r, Qt.color(color).g, Qt.color(color).b, fillAlpha);
+ ctx.fill();
+ };
+
+ drawLine(upHist, Colours.palette.m3secondary.toString(), 0.15);
+ drawLine(downHist, Colours.palette.m3tertiary.toString(), 0.2);
+ }
+
+ Component.onCompleted: updateMax()
+
+ Connections {
+ function onPaletteChanged() {
+ sparklineCanvas.requestPaint();
+ }
+
+ target: Colours
+ }
+
+ Timer {
+ interval: Config.dashboard.resourceUpdateInterval
+ running: true
+ repeat: true
+ onTriggered: sparklineCanvas._tickCount++
+ }
+
+ NumberAnimation on slideProgress {
+ from: 0
+ to: 1
+ duration: Config.dashboard.resourceUpdateInterval
+ loops: Animation.Infinite
+ running: true
+ }
+
+ Behavior on smoothMax {
+ Anim {
+ duration: Appearance.anim.durations.large
+ }
+ }
+ }
+
+ // "No data" placeholder
+ StyledText {
+ anchors.centerIn: parent
+ text: qsTr("Collecting data...")
+ font.pointSize: Appearance.font.size.small
+ color: Colours.palette.m3onSurfaceVariant
+ visible: NetworkUsage.downloadHistory.length < 2
+ opacity: 0.6
+ }
+ }
+
+ // Download row
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.normal
+
+ MaterialIcon {
+ text: "download"
+ color: Colours.palette.m3tertiary
+ font.pointSize: Appearance.font.size.normal
+ }
+
+ StyledText {
+ text: qsTr("Download")
+ font.pointSize: Appearance.font.size.small
+ color: Colours.palette.m3onSurfaceVariant
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ StyledText {
+ text: {
+ const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0);
+ return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
+ }
+ font.pointSize: Appearance.font.size.normal
+ font.weight: Font.Medium
+ color: Colours.palette.m3tertiary
+ }
+ }
+
+ // Upload row
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.normal
+
+ MaterialIcon {
+ text: "upload"
+ color: Colours.palette.m3secondary
+ font.pointSize: Appearance.font.size.normal
+ }
+
+ StyledText {
+ text: qsTr("Upload")
+ font.pointSize: Appearance.font.size.small
+ color: Colours.palette.m3onSurfaceVariant
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ StyledText {
+ text: {
+ const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0);
+ return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
+ }
+ font.pointSize: Appearance.font.size.normal
+ font.weight: Font.Medium
+ color: Colours.palette.m3secondary
+ }
+ }
+
+ // Session totals
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.normal
+
+ MaterialIcon {
+ text: "history"
+ color: Colours.palette.m3onSurfaceVariant
+ font.pointSize: Appearance.font.size.normal
+ }
+
+ StyledText {
+ text: qsTr("Total")
+ font.pointSize: Appearance.font.size.small
+ color: Colours.palette.m3onSurfaceVariant
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ StyledText {
+ 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";
+ }
+ font.pointSize: Appearance.font.size.small
+ color: Colours.palette.m3onSurfaceVariant
+ }
+ }
}
}
}