import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell.Services.UPower import Caelestia.Internal import qs.components import qs.components.misc import qs.config import qs.services Item { id: root readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2 function displayTemp(temp: real): string { return `${Math.ceil(Config.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheitPerformance ? "F" : "C"}`; } implicitWidth: Math.max(minWidth, content.implicitWidth) implicitHeight: placeholder.visible ? placeholder.height : content.implicitHeight 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) ColumnLayout { anchors.centerIn: parent spacing: Appearance.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "tune" font.pointSize: Appearance.font.size.extraLarge * 2 color: Colours.palette.m3onSurfaceVariant } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("No widgets enabled") font.pointSize: Appearance.font.size.large color: Colours.palette.m3onSurface } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Enable widgets in dashboard settings") font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } } } 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 RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") 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 } 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 } 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 } } 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 color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.large Component.onCompleted: animatedPercentage = percentage onPercentageChanged: animatedPercentage = percentage // 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) } 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 } } } Behavior on animatedPercentage { Anim { duration: Appearance.anim.durations.large } } } component CardHeader: RowLayout { property string icon property string title property color accentColor: Colours.palette.m3primary Layout.fillWidth: true spacing: Appearance.spacing.small MaterialIcon { text: parent.icon fill: 1 color: parent.accentColor font.pointSize: Appearance.spacing.large } StyledText { Layout.fillWidth: true text: parent.title font.pointSize: Appearance.font.size.normal elide: Text.ElideRight } } component ProgressBar: StyledRect { id: progressBar property real value: 0 property color fgColor: Colours.palette.m3primary property color bgColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) property real animatedValue: 0 color: bgColor radius: Appearance.rounding.full Component.onCompleted: animatedValue = value onValueChanged: animatedValue = value 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 } Behavior on animatedValue { Anim { duration: Appearance.anim.durations.large } } } 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 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 } RowLayout { Layout.fillWidth: true Layout.fillHeight: true spacing: Appearance.spacing.normal 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.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large anchors.rightMargin: 32 spacing: 0 StyledText { anchors.right: parent.right text: heroCard.mainLabel font.pointSize: Appearance.font.size.normal color: Colours.palette.m3onSurfaceVariant } StyledText { anchors.right: parent.right text: heroCard.mainValue font.pointSize: Appearance.font.size.extraLarge font.weight: Font.Medium color: heroCard.accentColor } } Behavior on animatedUsage { Anim { duration: Appearance.anim.durations.large } } Behavior on animatedTemp { Anim { duration: Appearance.anim.durations.large } } } component GaugeCard: StyledRect { id: gaugeCard 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 color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.large clip: true Component.onCompleted: animatedPercentage = percentage onPercentageChanged: animatedPercentage = percentage ColumnLayout { anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.smaller CardHeader { icon: gaugeCard.icon title: gaugeCard.title accentColor: gaugeCard.accentColor } Item { Layout.fillWidth: true Layout.fillHeight: true ArcGauge { anchors.centerIn: parent width: Math.min(parent.width, parent.height) height: width percentage: gaugeCard.animatedPercentage accentColor: gaugeCard.accentColor trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) startAngle: gaugeCard.arcStartAngle sweepAngle: gaugeCard.arcSweep } 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 } } 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 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; // 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 { 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 ArcGauge { anchors.centerIn: parent width: Math.min(parent.width, parent.height) height: width percentage: storageGaugeCard.animatedPercentage accentColor: storageGaugeCard.accentColor trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) startAngle: storageGaugeCard.arcStartAngle sweepAngle: storageGaugeCard.arcSweep } 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 animatedPercentage { Anim { duration: Appearance.anim.durations.large } } } component NetworkCard: StyledRect { id: networkCard property color accentColor: Colours.palette.m3primary 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 SparklineItem { id: sparkline property real targetMax: 1024 property real smoothMax: targetMax anchors.fill: parent line1: NetworkUsage.uploadBuffer line1Color: Colours.palette.m3secondary line1FillAlpha: 0.15 line2: NetworkUsage.downloadBuffer line2Color: Colours.palette.m3tertiary line2FillAlpha: 0.2 maxValue: smoothMax historyLength: NetworkUsage.historyLength Connections { target: NetworkUsage.downloadBuffer function onValuesChanged(): void { sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); slideAnim.restart(); } } NumberAnimation { id: slideAnim target: sparkline property: "slideProgress" from: 0 to: 1 duration: Config.dashboard.resourceUpdateInterval } 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.downloadBuffer.count < 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 } } } } }