From 92e121b356e93825dfcd53907c86b00160ee27cb Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 10 Aug 2025 19:25:44 +0200 Subject: [PATCH] Tons of small changes, add SidePanel, NCard, NCircleStat, NSystemMnitor --- Modules/DemoPanel/DemoPanel.qml | 24 ++++++- Modules/SidePanel/MediaCard.qml | 44 ++++++++++++ Modules/SidePanel/ProfileCard.qml | 68 ++++++++++++++++++ Modules/SidePanel/SidePanel.qml | 110 ++++++++++++++++++++++++++++- Modules/SidePanel/SystemCard.qml | 40 +++++++++++ Modules/SidePanel/WeatherCard.qml | 59 ++++++++++++++++ Services/Scaling.qml | 14 ++++ Services/Settings.qml | 2 +- Widgets/NBox.qml | 20 ++++++ Widgets/NCard.qml | 18 +++++ Widgets/NCircleStat.qml | 112 ++++++++++++++++++++++++++++++ Widgets/NSystemMonitor.qml | 73 +++++++++++++++++++ 12 files changed, 579 insertions(+), 5 deletions(-) create mode 100644 Modules/SidePanel/MediaCard.qml create mode 100644 Modules/SidePanel/ProfileCard.qml create mode 100644 Modules/SidePanel/SystemCard.qml create mode 100644 Modules/SidePanel/WeatherCard.qml create mode 100644 Widgets/NBox.qml create mode 100644 Widgets/NCard.qml create mode 100644 Widgets/NCircleStat.qml create mode 100644 Widgets/NSystemMonitor.qml diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 62c3959..38a1a86 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -73,8 +73,28 @@ NLoader { // NSlider ColumnLayout { spacing: 16 * scaling - NText { text: "NSlider"; color: Colors.accentSecondary } - NSlider {} + NText { text: "Scaling"; color: Colors.accentSecondary } + RowLayout { + spacing: Style.marginSmall * scaling + NText { text: `${Math.round(Scaling.overrideScale * 100)}%`; Layout.alignment: Qt.AlignVCenter } + NSlider { + id: scaleSlider + from: 0.6 + to: 1.8 + stepSize: 0.01 + value: Scaling.overrideScale + onMoved: function() { Scaling.overrideScale = value } + onPressedChanged: function() { Scaling.overrideEnabled = true } + } + NIconButton { + icon: "restart_alt" + sizeMultiplier: 0.7 + onClicked: function() { + Scaling.overrideEnabled = false + Scaling.overrideScale = 1.0 + } + } + } NDivider { Layout.fillWidth: true } } } diff --git a/Modules/SidePanel/MediaCard.qml b/Modules/SidePanel/MediaCard.qml new file mode 100644 index 0000000..193b579 --- /dev/null +++ b/Modules/SidePanel/MediaCard.qml @@ -0,0 +1,44 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +// Media player area (placeholder until MediaPlayer service is wired) +NBox { + id: root + + readonly property real scaling: Scaling.scale(screen) + + Layout.fillWidth: true + // Let content dictate the height (no hardcoded height here) + // Height can be overridden by parent layout (SidePanel binds it to stats card) + implicitHeight: content.implicitHeight + Style.marginLarge * 2 * scaling + + Column { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginXL * scaling + spacing: Style.marginSmall * scaling + + Item { height: 36 * scaling } + + Text { + text: "music_note" + font.family: "Material Symbols Outlined" + font.pointSize: 28 * scaling + color: Colors.textSecondary + anchors.horizontalCenter: parent.horizontalCenter + } + NText { + text: "No music player detected" + color: Colors.textSecondary + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + } + + Item { height: 36 * scaling } + } +} + diff --git a/Modules/SidePanel/ProfileCard.qml b/Modules/SidePanel/ProfileCard.qml new file mode 100644 index 0000000..c04da96 --- /dev/null +++ b/Modules/SidePanel/ProfileCard.qml @@ -0,0 +1,68 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import QtQuick.Effects +import qs.Services +import qs.Widgets + +// Header card with avatar, user and quick actions +NBox { + id: root + + readonly property real scaling: Scaling.scale(screen) + + Layout.fillWidth: true + // Height driven by content + implicitHeight: content.implicitHeight + Style.marginMedium * 2 * scaling + + RowLayout { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling + + Item { + id: avatarBox + width: 40 * scaling + height: 40 * scaling + + Image { + id: avatarImage + anchors.fill: parent + source: Settings.data.general.avatarImage + fillMode: Image.PreserveAspectCrop + asynchronous: true + } + + // Ensure rounded corners consistently across renderers + MultiEffect { + anchors.fill: avatarImage + source: avatarImage + maskEnabled: true + maskSource: Rectangle { + anchors.fill: parent + color: "white" + radius: Style.radiusMedium * scaling + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 * scaling + NText { text: Quickshell.env("USER") || "user" } + NText { text: "System Uptime: —"; color: Colors.textSecondary } + } + + RowLayout { + spacing: Style.marginSmall * scaling + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Item { Layout.fillWidth: true } + NIconButton { icon: "settings"; sizeMultiplier: 0.8 } + NIconButton { icon: "power_settings_new"; sizeMultiplier: 0.8 } + } + } +} + diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 8068a5c..541f171 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -6,6 +6,7 @@ import Quickshell.Wayland import qs.Services import qs.Widgets + NLoader { id: root @@ -34,6 +35,8 @@ NLoader { id: sidePanel readonly property real scaling: Scaling.scale(screen) + // Single source of truth for spacing between cards (both axes) + property real cardSpacing: Style.marginLarge * scaling // X coordinate from the bar to align this panel under property real anchorX: root.anchorX // Ensure this panel attaches to the intended screen @@ -42,13 +45,19 @@ NLoader { // Ensure panel shows itself once created Component.onCompleted: show() + // Inline helpers moved to dedicated widgets: NCard and NCircleStat + Rectangle { + id: panelBackground color: Colors.backgroundPrimary radius: Style.radiusLarge * scaling border.color: Colors.backgroundTertiary border.width: Math.min(1, Style.borderMedium * scaling) - width: 500 * scaling - height: 400 + layer.enabled: true + width: 460 * scaling + property real innerMargin: sidePanel.cardSpacing + // Height scales to content plus vertical padding + height: content.implicitHeight + innerMargin * 2 // Place the panel just below the bar (overlay content starts below bar due to topMargin) y: Style.marginSmall * scaling // Center horizontally under the anchorX, clamped to the screen bounds @@ -60,6 +69,103 @@ NLoader { // Prevent closing when clicking in the panel bg MouseArea { anchors.fill: parent } + // Content wrapper to ensure childrenRect drives implicit height + Item { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: panelBackground.innerMargin + implicitHeight: layout.implicitHeight + + // Layout content (not vertically anchored so implicitHeight is valid) + ColumnLayout { + id: layout + // Use the same spacing value horizontally and vertically + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: sidePanel.cardSpacing + + // Cards (consistent inter-card spacing via ColumnLayout spacing) + ProfileCard { Layout.topMargin: 0; Layout.bottomMargin: 0 } + WeatherCard { Layout.topMargin: 0; Layout.bottomMargin: 0 } + + // Middle section: media + stats column + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 0 + Layout.bottomMargin: 0 + spacing: sidePanel.cardSpacing + + // Media card + MediaCard { id: mediaCard; Layout.fillWidth: true; implicitHeight: statsCard.implicitHeight } + + // System monitors combined in one card + SystemCard { id: statsCard } + } + + // Bottom actions (two grouped rows of round buttons) + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 0 + Layout.bottomMargin: 0 + spacing: sidePanel.cardSpacing + + // Power Profiles: performance, balanced, eco + NBox { + Layout.fillWidth: true + Layout.preferredWidth: 1 + implicitHeight: powerRow.implicitHeight + Style.marginSmall * 2 * scaling + RowLayout { + id: powerRow + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: sidePanel.cardSpacing + Item { Layout.fillWidth: true } + // Performance + NIconButton { + icon: "speed" + sizeMultiplier: 1.0 + onClicked: function () { /* TODO: hook to power profile */ } + } + // Balanced + NIconButton { + icon: "balance" + sizeMultiplier: 1.0 + onClicked: function () { /* TODO: hook to power profile */ } + } + // Eco + NIconButton { + icon: "eco" + sizeMultiplier: 1.0 + onClicked: function () { /* TODO: hook to power profile */ } + } + Item { Layout.fillWidth: true } + } + } + + // Utilities: record & wallpaper + NBox { + Layout.fillWidth: true + Layout.preferredWidth: 1 + implicitHeight: utilRow.implicitHeight + Style.marginSmall * 2 * scaling + RowLayout { + id: utilRow + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: sidePanel.cardSpacing + Item { Layout.fillWidth: true } + // Record + NIconButton { icon: "fiber_manual_record"; sizeMultiplier: 1.0 } + // Wallpaper + NIconButton { icon: "image"; sizeMultiplier: 1.0 } + Item { Layout.fillWidth: true } + } + } + } + } + } } } } diff --git a/Modules/SidePanel/SystemCard.qml b/Modules/SidePanel/SystemCard.qml new file mode 100644 index 0000000..57e0441 --- /dev/null +++ b/Modules/SidePanel/SystemCard.qml @@ -0,0 +1,40 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +// Unified system card: monitors CPU, temp, memory, disk +NBox { + id: root + + readonly property real scaling: Scaling.scale(screen) + + Layout.preferredWidth: 84 * scaling + implicitHeight: content.implicitHeight + Style.marginTiny * 2 * scaling + + Column { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: Style.marginSmall * scaling + anchors.rightMargin: Style.marginSmall * scaling + anchors.topMargin: Style.marginTiny * scaling + anchors.bottomMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + + // Slight top padding + Item { height: Style.marginTiny * scaling } + + NSystemMonitor { id: sysMon; intervalSeconds: 1 } + + NCircleStat { value: sysMon.cpuUsage || SysInfo.cpuUsage; icon: "speed"; flat: true; contentScale: 0.8; width: 72 * scaling; height: 68 * scaling } + NCircleStat { value: sysMon.cpuTemp || SysInfo.cpuTemp; suffix: "°C"; icon: "device_thermostat"; flat: true; contentScale: 0.8; width: 72 * scaling; height: 68 * scaling } + NCircleStat { value: sysMon.memoryUsagePer || SysInfo.memoryUsagePer; icon: "memory"; flat: true; contentScale: 0.8; width: 72 * scaling; height: 68 * scaling } + NCircleStat { value: sysMon.diskUsage || SysInfo.diskUsage; icon: "data_usage"; flat: true; contentScale: 0.8; width: 72 * scaling; height: 68 * scaling } + + // Extra bottom padding to shift the perceived stack slightly upward + Item { height: Style.marginMedium * scaling } + } +} + diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/WeatherCard.qml new file mode 100644 index 0000000..359f8d1 --- /dev/null +++ b/Modules/SidePanel/WeatherCard.qml @@ -0,0 +1,59 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +// Weather overview card (placeholder data) +NBox { + id: root + + readonly property real scaling: Scaling.scale(screen) + + Layout.fillWidth: true + // Height driven by content + implicitHeight: content.implicitHeight + Style.marginLarge * 2 * scaling + + ColumnLayout { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginSmall * scaling + + RowLayout { + spacing: Style.marginSmall * scaling + Text { + text: "sunny" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: Colors.accentSecondary + } + ColumnLayout { + NText { text: "Dinslaken (GMT+2)" } + NText { text: "26°C"; font.pointSize: (Style.fontSizeXL + 6) * scaling } + } + } + + Rectangle { height: 1; width: parent.width; color: Colors.backgroundTertiary } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginLarge * scaling + Repeater { + model: 5 + delegate: ColumnLayout { + spacing: 2 * scaling + NText { text: ["Sun","Mon","Tue","Wed","Thu"][index] } + Text { + text: index % 2 === 0 ? "wb_sunny" : "cloud" + font.family: "Material Symbols Outlined" + color: Colors.textSecondary + } + NText { text: "26° / 14°"; color: Colors.textSecondary } + } + } + } + } +} + diff --git a/Services/Scaling.qml b/Services/Scaling.qml index 7c15215..b921e14 100644 --- a/Services/Scaling.qml +++ b/Services/Scaling.qml @@ -5,12 +5,26 @@ import Quickshell Singleton { id: root + // Manual override for testing UI scale across the whole shell + // Enable this from the DemoPanel slider + property bool overrideEnabled: false + property real overrideScale: 1.0 + // Design reference resolution (for scale = 1.0) readonly property int designScreenWidth: 2560 readonly property int designScreenHeight: 1440 // Automatic, orientation-agnostic scaling function scale(aScreen) { + // 0) Manual override (for development/testing) + try { + if (overrideEnabled && isFinite(overrideScale)) { + // Clamp to keep UI usable + const clamped = Math.max(0.6, Math.min(1.8, overrideScale)) + return clamped + } + } catch (e) {} + if (typeof aScreen !== 'undefined' & aScreen) { // // 1) Per-monitor override wins diff --git a/Services/Settings.qml b/Services/Settings.qml index feb44b5..61eeafa 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -17,7 +17,7 @@ Singleton { property var data: settingAdapter // Needed to only have one NPanel loaded at a time. - property var openPanel: null + // property var openPanel: null Item { Component.onCompleted: { diff --git a/Widgets/NBox.qml b/Widgets/NBox.qml new file mode 100644 index 0000000..2bc24b0 --- /dev/null +++ b/Widgets/NBox.qml @@ -0,0 +1,20 @@ +import QtQuick +import qs.Services + +// Rounded group container using the variant surface color. +// To be used in side panels and settings panes to group fields or buttons. +Rectangle { + id: root + + readonly property real scaling: Scaling.scale(screen) + + implicitWidth: childrenRect.width + implicitHeight: childrenRect.height + + color: Colors.surfaceVariant + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.min(1, Style.borderThin * scaling) + clip: true +} + diff --git a/Widgets/NCard.qml b/Widgets/NCard.qml new file mode 100644 index 0000000..8901bb2 --- /dev/null +++ b/Widgets/NCard.qml @@ -0,0 +1,18 @@ +import QtQuick +import qs.Services + +// Generic themed card container +Rectangle { + id: root + + readonly property real scaling: Scaling.scale(screen) + + implicitWidth: childrenRect.width + implicitHeight: childrenRect.height + + color: Colors.backgroundSecondary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.min(1, Style.borderThin * scaling) +} + diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml new file mode 100644 index 0000000..84d6a71 --- /dev/null +++ b/Widgets/NCircleStat.qml @@ -0,0 +1,112 @@ +import QtQuick +import qs.Services + +// Compact circular statistic display used in the SidePanel +Rectangle { + id: root + + readonly property real scaling: Scaling.scale(screen) + property real value: 0 // 0..100 (or any range visually mapped) + property string icon: "" + property string suffix: "%" + + // When nested inside a parent group (NBox), you can make it flat + property bool flat: false + // Scales the internal content (labels, gauge, icon) without changing the + // outer width/height footprint of the component + property real contentScale: 1.0 + + width: 68 * scaling + height: 92 * scaling + color: flat ? "transparent" : Colors.backgroundSecondary + radius: Style.radiusSmall * scaling + border.color: flat ? "transparent" : Colors.backgroundTertiary + border.width: flat ? 0 : Math.min(1, Style.borderThin * scaling) + clip: true + + // Repaint gauge when the bound value changes + onValueChanged: gauge.requestPaint() + + Row { + id: innerRow + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling * contentScale + spacing: Style.marginSmall * scaling * contentScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + + // Gauge with percentage label placed inside the open gap (right side) + Item { + id: gaugeWrap + anchors.verticalCenter: innerRow.verticalCenter + width: 68 * scaling * contentScale + height: 68 * scaling * contentScale + + Canvas { + id: gauge + anchors.fill: parent + renderStrategy: Canvas.Cooperative + onPaint: { + const ctx = getContext("2d") + const w = width, h = height + const cx = w / 2, cy = h / 2 + const r = Math.min(w, h) / 2 - 5 * root.scaling * contentScale + // 240° arc with a 120° gap centered on the right side + // Start at 60° and end at 300° → balanced right-side opening + const start = Math.PI / 3 + const endBg = Math.PI * 5 / 3 + ctx.reset() + ctx.lineWidth = 6 * root.scaling * contentScale + // Track uses backgroundPrimary for stronger contrast + ctx.strokeStyle = Colors.backgroundPrimary + ctx.beginPath() + ctx.arc(cx, cy, r, start, endBg) + ctx.stroke() + // Value arc + const ratio = Math.max(0, Math.min(1, root.value / 100)) + const end = start + (endBg - start) * ratio + ctx.strokeStyle = Colors.accentPrimary + ctx.beginPath() + ctx.arc(cx, cy, r, start, end) + ctx.stroke() + } + } + + // Percent centered in the circle + Text { + id: valueLabel + anchors.centerIn: parent + text: `${Math.round(root.value)}${root.suffix}` + font.pointSize: Style.fontSizeMedium * scaling * contentScale + color: Colors.textPrimary + horizontalAlignment: Text.AlignHCenter + } + + // Tiny circular badge for the icon, inside the right-side gap + Rectangle { + id: iconBadge + width: 22 * scaling * contentScale + height: width + radius: width / 2 + color: Colors.backgroundPrimary + border.color: Colors.accentPrimary + border.width: Math.min(1, Style.borderThin * scaling) + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: 4 * scaling * contentScale + anchors.bottomMargin: 4 * scaling * contentScale + + Text { + anchors.centerIn: parent + text: root.icon + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling * contentScale + color: Colors.accentPrimary + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + } + } +} + diff --git a/Widgets/NSystemMonitor.qml b/Widgets/NSystemMonitor.qml new file mode 100644 index 0000000..1cb89da --- /dev/null +++ b/Widgets/NSystemMonitor.qml @@ -0,0 +1,73 @@ +import QtQuick +import Quickshell +import Quickshell.Io + +// Lightweight system monitor using standard Linux interfaces. +// Provides cpu usage %, cpu temperature (°C), and memory usage %. +// No external helpers; uses /proc and /sys via a shell loop. +Item { + id: root + + // Public values + property real cpuUsage: 0 + property real cpuTemp: 0 + property real memoryUsagePer: 0 + property real diskUsage: 0 + + // Interval in seconds between updates + property int intervalSeconds: 1 + + // Background process emitting one JSON line per sample + Process { + id: reader + running: true + command: [ + "sh", "-c", + // Outputs: {"cpu":,"memper":,"cputemp":} + "interval=" + intervalSeconds + "; " + + "while true; do " + + // First /proc/stat snapshot + "read _ u1 n1 s1 id1 iw1 ir1 si1 st1 gs1 < /proc/stat; " + + "t1=$((u1+n1+s1+id1+iw1+ir1+si1+st1)); i1=$((id1+iw1)); " + + "sleep $interval; " + + // Second /proc/stat snapshot + "read _ u2 n2 s2 id2 iw2 ir2 si2 st2 gs2 < /proc/stat; " + + "t2=$((u2+n2+s2+id2+iw2+ir2+si2+st2)); i2=$((id2+iw2)); " + + "dt=$((t2 - t1)); di=$((i2 - i1)); " + + "cpu=$(( (100*(dt - di)) / (dt>0?dt:1) )); " + + // Memory percent via /proc/meminfo (kB) + "mt=$(awk '/MemTotal/ {print $2}' /proc/meminfo); " + + "ma=$(awk '/MemAvailable/ {print $2}' /proc/meminfo); " + + "mm=$((mt - ma)); mp=$(( (100*mm) / (mt>0?mt:1) )); " + + // Temperature: scan hwmon and thermal zones, choose max; convert m°C → °C + "ct=0; " + + "for f in /sys/class/hwmon/hwmon*/temp*_input /sys/class/thermal/thermal_zone*/temp; do " + + "[ -r \"$f\" ] || continue; v=$(cat \"$f\" 2>/dev/null); " + + "[ -z \"$v\" ] && continue; " + + "if [ \"$v\" -gt 1000 ] 2>/dev/null; then v=$((v/1000)); fi; " + + "[ \"$v\" -gt \"$ct\" ] 2>/dev/null && ct=$v; " + + "done; " + + // Disk usage percent for root filesystem + "dp=$(df -P / 2>/dev/null | awk 'NR==2{gsub(/%/,\"\",$5); print $5}'); " + + "[ -z \"$dp\" ] && dp=0; " + + // Emit JSON line + "echo \"{\\\"cpu\\\":$cpu,\\\"memper\\\":$mp,\\\"cputemp\\\":$ct,\\\"diskper\\\":$dp}\"; " + + "done" + ] + + stdout: SplitParser { + onRead: function (line) { + try { + const data = JSON.parse(line) + root.cpuUsage = +data.cpu + root.cpuTemp = +data.cputemp + root.memoryUsagePer = +data.memper + root.diskUsage = +data.diskper + } catch (e) { + // ignore malformed lines + } + } + } + } +} +