36 Commits
Author SHA1 Message Date
zach e6f7742bd8 Merge pull request 'updated components' (#122) from feat/updated-components into main
Reviewed-on: #122
Reviewed-by: AramJonghu <2+aramjonghu@noreply.git.zach-dev.cc>
2026-06-23 11:33:20 +02:00
zach a37f6075d1 GPU plugin
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 18s
Python / test (pull_request) Successful in 29s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m8s
2026-06-22 23:13:18 +02:00
zach 80d5f13663 cursor crosshair + outline of pen width
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 14s
Python / test (pull_request) Successful in 32s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m7s
2026-06-22 11:52:11 +02:00
zach 2f3f41cedd fill icons when relevant
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 16s
Python / test (pull_request) Successful in 30s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m5s
2026-06-22 09:27:13 +02:00
zach faad2dbd09 sidebar toggle buttons
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 15s
Python / test (pull_request) Successful in 31s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m7s
2026-06-21 22:31:10 +02:00
zach 164b48ab6a better spacing of action buttons in notification popups
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 20s
Python / test (pull_request) Successful in 28s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m14s
2026-06-21 17:44:58 +02:00
zach e55d4aa36b clear and exit buttons on drawing popout + pin button
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 19s
Python / test (pull_request) Successful in 29s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m11s
2026-06-21 16:14:36 +02:00
zach 91b4466773 early return in hover handler if drawing
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 17s
Python / test (pull_request) Successful in 28s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m5s
2026-06-20 20:57:41 +02:00
zach 7be81e584e GPU caching for better performance after many strokes
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Python / lint-format (pull_request) Successful in 17s
Python / test (pull_request) Successful in 31s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m6s
2026-06-20 20:55:15 +02:00
zach 8a87f86758 apply curve smoothing to drawn lines
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 17s
Python / test (pull_request) Successful in 29s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m10s
2026-06-20 20:09:03 +02:00
zach e619632077 show hover dot instead of cursor while drawing
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Python / lint-format (pull_request) Successful in 17s
Python / test (pull_request) Successful in 28s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m11s
2026-06-20 19:00:58 +02:00
zach 81eb56e1d0 fixes for drawing
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 9s
Python / lint-format (pull_request) Successful in 14s
Python / test (pull_request) Successful in 29s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m8s
2026-06-20 14:23:15 +02:00
zach a1c148bf70 current color rectangle centered in arc
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 14s
Python / test (pull_request) Successful in 27s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m8s
2026-06-20 13:37:00 +02:00
zach fc9be754fd preset color buttons rework + curve smoothing
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 9s
Python / lint-format (pull_request) Successful in 17s
Python / test (pull_request) Successful in 30s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m6s
2026-06-20 13:24:47 +02:00
zach ca1243f9c7 disable interactible elements when drawing
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 13s
Python / lint-format (pull_request) Successful in 25s
Python / test (pull_request) Successful in 32s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m28s
2026-06-19 23:21:01 +02:00
zach 697058146e notif actions rework
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 9s
Python / lint-format (pull_request) Successful in 17s
Python / test (pull_request) Successful in 29s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m25s
2026-06-19 22:50:52 +02:00
zach d516dd8be4 fix text input
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 17s
Python / test (pull_request) Successful in 30s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m15s
2026-06-19 14:06:31 +02:00
zach b17bc61d80 remove logging
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 15s
Python / test (pull_request) Successful in 30s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m9s
2026-06-18 22:26:52 +02:00
zach e37332fe6b add right click clear
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 18s
Python / test (pull_request) Successful in 28s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m14s
2026-06-18 20:36:28 +02:00
zach c102d0e38f drawing reworked into c++ plugin, moved input handler to Interactions.qml as a PointHandler
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 16s
Python / test (pull_request) Successful in 31s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m17s
2026-06-18 13:48:58 +02:00
zach fbadcbb717 sidebar doesn't reduce height when new notifs come in
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Python / lint-format (pull_request) Successful in 18s
Python / test (pull_request) Successful in 28s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m14s
2026-06-17 21:49:52 +02:00
zach 2cb4d9b739 revert sensors process removal
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 32s
Python / lint-format (pull_request) Successful in 38s
Python / test (pull_request) Successful in 49s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m24s
2026-06-17 18:53:23 +02:00
zach a98a7944b9 show notification in fullscreen
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 18s
Python / test (pull_request) Successful in 27s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m11s
2026-06-17 18:44:50 +02:00
zach 5fb137699f visualizer wave graph instead of lines
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 24s
Python / test (pull_request) Successful in 28s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m13s
2026-06-17 13:19:37 +02:00
zach 866373be48 remove accidental log files
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 21s
Python / test (pull_request) Successful in 29s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m7s
2026-06-17 12:02:22 +02:00
zach b034400df9 hacky english month and day names while using system locale
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 17s
Python / test (pull_request) Successful in 29s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m8s
2026-06-17 11:59:29 +02:00
zach c0b62dfb6c rounded sparkline graph, fixed network usage accumulation spikes
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 9s
Python / lint-format (pull_request) Successful in 19s
Python / test (pull_request) Successful in 29s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m6s
2026-06-16 13:52:43 +02:00
zach ea82b8cf22 calendar month change animation
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 9s
Python / lint-format (pull_request) Successful in 18s
Python / test (pull_request) Successful in 31s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m5s
2026-06-16 10:55:30 +02:00
zach be8b7ae436 config option for min brightness + clipboard config options now in settings
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 9s
Python / lint-format (pull_request) Successful in 16s
Python / test (pull_request) Successful in 31s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m5s
2026-06-16 10:15:57 +02:00
zach e3d3daf807 fix function call in wallpaper cropper
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 9s
Python / lint-format (pull_request) Successful in 18s
Python / test (pull_request) Successful in 27s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m8s
2026-06-16 09:46:36 +02:00
zach 0057830fbc resource popout spacing and larger percent text size
Python / lint-format (pull_request) Successful in 30s
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 52s
Python / test (pull_request) Successful in 1m26s
Lint & Format (Rust) / lint-format (pull_request) Successful in 2m20s
2026-06-15 19:27:16 +02:00
zach eafffdcc8f fix slider inset icons initial position
Python / lint-format (pull_request) Failing after 2m25s
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 3m14s
Python / test (pull_request) Successful in 3m55s
Lint & Format (Rust) / lint-format (pull_request) Successful in 4m49s
2026-06-15 19:20:02 +02:00
zach 578a10c950 clipboard preview leading tab trimming while preserving indentation + proper width calculations
Python / lint-format (pull_request) Successful in 27s
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 30s
Python / test (pull_request) Successful in 32s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m6s
2026-06-15 18:23:56 +02:00
zach 2034f3a8db fixed applying crop being overwritten by multiple write calls, added wallpaper fade anim
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 14s
Python / test (pull_request) Successful in 31s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m6s
2026-06-15 15:52:55 +02:00
zach 7e38f3f428 slight changes to clipboard history preview and item delegate
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 16s
Python / test (pull_request) Successful in 32s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m10s
2026-06-14 23:33:12 +02:00
zach 2a50732bc2 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
2026-06-14 23:09:10 +02:00
110 changed files with 5753 additions and 2446 deletions
-3
View File
@@ -12,11 +12,8 @@ Slider {
readonly property bool isVertical: orientation === Qt.Vertical
property real multiplier: 100
property real oldValue
// Wrapper components can inject their own track visuals here.
property Component trackContent
// Keep current behavior for existing usages.
orientation: Qt.Vertical
background: CustomRect {
+6 -1
View File
@@ -18,6 +18,11 @@ CustomRect {
property color disabledColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1)
property color disabledOnColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38)
property bool fillWidth
property font font: ({
family: Appearance.font.family.sans,
pointSize: Appearance.font.size.larger,
bold: false
})
property real horizontalPadding: padding
readonly property alias hovered: stateLayer.containsMouse
required implicitHeight
@@ -71,7 +76,7 @@ CustomRect {
id: stateLayer
color: root.internalChecked ? root.activeOnColor : root.inactiveOnColor
enabled: enabled
enabled: root.enabled
onClicked: {
if (root.isToggle)
+4 -5
View File
@@ -17,8 +17,8 @@ BusyIndicator {
}
property int animState
property color bgColour: DynamicColors.palette.m3secondaryContainer
property color fgColour: DynamicColors.palette.m3primary
property color bgColor: DynamicColors.palette.m3secondaryContainer
property color fgColor: DynamicColors.palette.m3primary
property real implicitSize: Appearance.font.size.normal * 3
property real internalStrokeWidth: strokeWidth
readonly property alias progress: manager.progress
@@ -31,8 +31,8 @@ BusyIndicator {
contentItem: CircularProgress {
anchors.fill: parent
bgColour: root.bgColour
fgColour: root.fgColour
bgColor: root.bgColor
fgColor: root.fgColor
padding: root.padding
rotation: manager.rotation
startAngle: manager.startFraction * 360
@@ -73,7 +73,6 @@ BusyIndicator {
CircularIndicatorManager {
id: manager
}
NumberAnimation {
+89 -39
View File
@@ -1,66 +1,116 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Shapes
import ZShell.Components
import qs.Config
Shape {
Item {
id: root
readonly property real arcRadius: (size - padding - strokeWidth) / 2
property color bgColour: DynamicColors.palette.m3secondaryContainer
property color fgColour: DynamicColors.palette.m3primary
readonly property real arcRadius: (size - padding - strokeWidth * (1 + waveAmplitude * 2)) / 2
property color bgColor: DynamicColors.palette.m3secondaryContainer
property real clampedVal: Math.max(1 / 360, Math.min(1, isNaN(value) ? 0 : value))
readonly property real dotAngleRad: (startAngle + sweepAngle - gapAngle * (sweepAngle < 360 ? 0 : 1)) * Math.PI / 180
property color fgColor: DynamicColors.palette.m3primary
readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI)
property alias hasEndIndicator: dot.active
property real implicitSize
property int padding: 0
readonly property real size: Math.min(width, height)
property int spacing: Appearance.spacing.small
property int startAngle: -90
property int strokeWidth: Appearance.padding.smaller
readonly property real vValue: value || 1 / 360
property int strokeWidth: Appearance.padding.small
property int sweepAngle: 360
readonly property real thickness: strokeWidth * (1 + waveAmplitude) * 2
property real value
property alias waveAmplitude: wave.amplitudeMultiplier
property alias waveDuration: waveProgAnim.duration
property alias waveFrequency: wave.frequency
property bool wavePaused
property bool wavy: false
asynchronous: true
preferredRendererType: Shape.CurveRenderer
implicitHeight: implicitSize
implicitWidth: implicitSize
ShapePath {
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
fillColor: "transparent"
strokeColor: root.bgColour
strokeWidth: root.strokeWidth
Shape {
asynchronous: true
opacity: Math.min(1, remainingArc.sweepAngle)
preferredRendererType: Shape.CurveRenderer
Behavior on strokeColor {
CAnim {
duration: Appearance.anim.durations.large
ShapePath {
capStyle: ShapePath.RoundCap
fillColor: "transparent"
strokeColor: root.bgColor
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
Behavior on strokeColor {
CAnim {
}
}
}
PathAngleArc {
centerX: root.size / 2
centerY: root.size / 2
radiusX: root.arcRadius
radiusY: root.arcRadius
startAngle: root.startAngle + 360 * root.vValue + root.gapAngle
sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2)
PathAngleArc {
id: remainingArc
centerX: root.size / 2
centerY: root.size / 2
radiusX: root.arcRadius
radiusY: root.arcRadius
startAngle: root.startAngle + root.clampedVal * root.sweepAngle + root.gapAngle
sweepAngle: Math.max(1 / 360, root.sweepAngle * (1 - root.clampedVal) - root.gapAngle * (root.sweepAngle < 360 ? 1 : 2))
}
}
}
ShapePath {
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
fillColor: "transparent"
strokeColor: root.fgColour
strokeWidth: root.strokeWidth
WavyLine {
id: wave
Behavior on strokeColor {
CAnim {
duration: Appearance.anim.durations.large
amplitudeMultiplier: root.wavy ? 0.5 : 0
anchors.fill: parent
anchors.margins: -lineWidth * amplitudeMultiplier
color: root.fgColor
frequency: 8
fullAngle: root.sweepAngle
lineWidth: root.strokeWidth
pathType: WavyLine.Arc
radius: root.arcRadius
startAngle: root.startAngle
value: root.clampedVal
Behavior on amplitudeMultiplier {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on color {
CAnim {
}
}
Anim on waveProgress {
id: waveProgAnim
PathAngleArc {
centerX: root.size / 2
centerY: root.size / 2
radiusX: root.arcRadius
radiusY: root.arcRadius
startAngle: root.startAngle
sweepAngle: 360 * root.vValue
duration: 2000
easing.type: Easing.Linear
from: 0
loops: Animation.Infinite
paused: root.wavePaused || wave.amplitudeMultiplier === 0
running: true
to: 1
}
}
Loader {
id: dot
x: root.size / 2 + root.arcRadius * Math.cos(root.dotAngleRad) - width / 2
y: root.size / 2 + root.arcRadius * Math.sin(root.dotAngleRad) - height / 2
sourceComponent: CustomRect {
color: root.fgColor
implicitHeight: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth)
implicitWidth: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth)
opacity: Math.min(1, remainingArc.sweepAngle)
radius: Appearance.rounding.full
}
}
}
+28 -5
View File
@@ -45,10 +45,10 @@ Item {
}
function syncFromPenColor() {
if (!drawing)
if (!drawing.drawingState)
return;
const c = drawing.penColor;
const c = drawing.drawingState.penColor;
if (c.hsvSaturation > 0) {
currentHue = c.hsvHue;
@@ -85,12 +85,20 @@ Item {
currentHue = relative / arcSweep;
lastChromaticHue = currentHue;
drawing.penColor = Qt.hsva(currentHue, drawing.penColor.hsvSaturation, drawing.penColor.hsvValue, drawing.penColor.a);
drawing.drawingState.penColor = Qt.hsva(currentHue, drawing.drawingState.penColor.hsvSaturation, drawing.drawingState.penColor.hsvValue, drawing.drawingState.penColor.a);
}
implicitHeight: 180
implicitWidth: 220
Behavior on currentHue {
enabled: !root.dragActive
Anim {
type: Anim.StandardLarge
}
}
Component.onCompleted: syncFromPenColor()
onCurrentHueChanged: canvas.requestPaint()
onDrawingChanged: syncFromPenColor()
@@ -103,7 +111,7 @@ Item {
root.syncFromPenColor();
}
target: root.drawing
target: root.drawing.drawingState
}
Canvas {
@@ -141,6 +149,21 @@ Item {
}
}
CustomRect {
anchors.centerIn: parent
color: root.drawing?.drawingState.penColor
implicitHeight: implicitWidth
implicitWidth: canvas.height - root.handleSize - Appearance.padding.extraLarge * 2
radius: Appearance.rounding.full
Behavior on color {
enabled: false
CAnim {
}
}
}
Item {
id: handle
@@ -174,7 +197,7 @@ Item {
Rectangle {
anchors.centerIn: parent
color: root.drawing ? root.drawing.penColor : Qt.hsla(root.currentHue, 1.0, 0.5, 1.0)
color: Qt.hsla(root.currentHue, 1.0, 0.5, 1.0)
height: width
radius: width / 2
width: parent.width - 12
+2
View File
@@ -1,8 +1,10 @@
import QtQuick
import qs.Helpers
Flickable {
id: root
interactive: !Visibilities.getForActive().isDrawing
maximumFlickVelocity: 3000
rebound: Transition {
+2
View File
@@ -1,10 +1,12 @@
import QtQuick
import qs.Helpers
ListView {
id: root
property bool doneFakeFlick
interactive: !Visibilities.getForActive().isDrawing
maximumFlickVelocity: 3000
rebound: Transition {
+237
View File
@@ -0,0 +1,237 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Templates
import ZShell
import ZShell.Components
import ZShell.Internal
import qs.Config
ProgressBar {
id: root
enum IndeterminateAnimState {
Running,
Completing,
Stopped
}
property color bgColor: DynamicColors.palette.m3secondaryContainer
property color fgColor: DynamicColors.palette.m3primary
property int indeterminateAnimState: CustomProgressBar.Stopped
property real waveAmplitude: 0.5
property int waveDuration: 1000
property int waveFrequency: 6
property bool wavePaused
property bool wavy
function toBounds(startFrac: real, endFrac: real, gapSize: real): point {
startFrac = ZUtils.clamp(startFrac, 0, 1);
endFrac = ZUtils.clamp(endFrac, 0, 1);
// Ramp down gap size
const GAP_RAMP_DOWN_THRESHOLD = 0.01;
gapSize += height / 2;
const startGapSize = (gapSize * ZUtils.clamp(startFrac, 0, GAP_RAMP_DOWN_THRESHOLD) / GAP_RAMP_DOWN_THRESHOLD);
const endGapSize = (gapSize * (1 - ZUtils.clamp(endFrac, 1 - GAP_RAMP_DOWN_THRESHOLD, 1)) / GAP_RAMP_DOWN_THRESHOLD);
const start = width * startFrac + startGapSize;
const end = width * endFrac - endGapSize;
return start >= end ? Qt.point(0, 0) : Qt.point(start, end);
}
function updateIAnimState(): void {
if (indeterminate) {
manager.completeEndProgress = 0;
indeterminateAnimState = CustomProgressBar.Running;
} else if (indeterminateAnimState === CustomProgressBar.Running) {
indeterminateAnimState = CustomProgressBar.Completing;
}
}
implicitHeight: 4
implicitWidth: 200
contentItem: Loader {
anchors.fill: parent
asynchronous: true
sourceComponent: root.indeterminate || root.indeterminateAnimState !== CustomProgressBar.Stopped ? indeterminateComp : determinateComp
}
Component.onCompleted: updateIAnimState()
onIndeterminateChanged: updateIAnimState()
LinearIndicatorManager {
id: manager
gap: Appearance.spacing.extraSmall
Anim on completeEndProgress {
duration: manager.completeEndDuration
running: root.indeterminateAnimState === CustomProgressBar.Completing
to: 1
onFinished: {
if (root.indeterminateAnimState === CustomProgressBar.Completing)
root.indeterminateAnimState = CustomProgressBar.Stopped;
}
}
Anim on progress {
duration: manager.duration
easing.type: Easing.Linear
from: 0
loops: Animation.Infinite
running: root.indeterminateAnimState !== CustomProgressBar.Stopped
to: 1
}
}
Component {
id: determinateComp
Item {
Line {
id: remaining
anchors.right: parent.right
implicitWidth: parent.width - wave.implicitWidth - Appearance.spacing.extraSmall
}
Line {
property real implicitSize
anchors.right: parent.right
anchors.rightMargin: (parent.height - implicitHeight) / 2
anchors.verticalCenter: parent.verticalCenter
color: root.fgColor
implicitHeight: implicitSize
implicitWidth: implicitSize
radius: Appearance.rounding.full
Behavior on implicitSize {
Anim {
type: Anim.FastSpatial
}
}
Component.onCompleted: implicitSize = Qt.binding(() => parent.width - wave.implicitWidth < parent.height ? parent.height : 4)
}
Wave {
id: wave
anchors.left: parent.left
Behavior on implicitWidth {
Anim {
}
}
Component.onCompleted: implicitWidth = Qt.binding(() => parent.width * root.visualPosition)
}
}
}
Component {
id: indeterminateComp
Item {
id: content
Line {
bounds: {
const i = manager.activeIndicators[0]; // qmllint disable unresolved-type
return i ? root.toBounds(0, i.startFraction, i.gapSize / 2) : Qt.point(0, 0);
}
}
Line {
bounds: {
const i = manager.activeIndicators[manager.activeIndicators.length - 1]; // qmllint disable unresolved-type
return i ? root.toBounds(i.endFraction, 1, i.gapSize / 2) : Qt.point(0, 0);
}
}
Instantiator {
model: Math.max(manager.activeIndicators.length, 1) - 1 // qmllint disable unresolved-type
delegate: Line {
readonly property LinearIndicatorSegment cur: manager.activeIndicators[index] // qmllint disable unresolved-type
required property int index
readonly property LinearIndicatorSegment next: manager.activeIndicators[index + 1 % manager.activeIndicators.length] // qmllint disable unresolved-type
bounds: root.toBounds(cur.endFraction, next.startFraction, cur.gapSize / 2)
}
onObjectAdded: (_, obj) => content.data.push(obj)
onObjectRemoved: (_, obj) => {
const idx = content.data.indexOf(obj);
if (idx !== -1)
content.data.splice(idx, 1);
}
}
Instantiator {
model: manager.activeIndicators // qmllint disable unresolved-type
delegate: Wave {
readonly property point bounds: root.toBounds(modelData.startFraction, modelData.endFraction, modelData.gapSize / 2)
required property LinearIndicatorSegment modelData
color: root.fgColor
implicitWidth: bounds.y - bounds.x
x: bounds.x
}
onObjectAdded: (_, obj) => content.data.push(obj)
onObjectRemoved: (_, obj) => {
const idx = content.data.indexOf(obj);
if (idx !== -1)
content.data.splice(idx, 1);
}
}
}
}
component Line: CustomRect {
property point bounds
anchors.verticalCenter: parent.verticalCenter
color: root.bgColor
implicitHeight: parent.height
implicitWidth: bounds.y - bounds.x
radius: Appearance.rounding.full
x: bounds.x
}
component Wave: WavyLine {
id: wave
amplitudeMultiplier: root.wavy ? root.waveAmplitude : 0
anchors.verticalCenter: parent.verticalCenter
color: root.fgColor
frequency: root.waveFrequency
fullLength: parent.width
implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth
lineWidth: parent.height
startX: x
Behavior on amplitudeMultiplier {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on color {
CAnim {
}
}
Anim on waveProgress {
duration: root.waveDuration
easing.type: Easing.Linear
from: 0
loops: Animation.Infinite
paused: wave.amplitudeMultiplier === 0 || root.wavePaused
running: true
to: 1
}
}
}
+5 -1
View File
@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Templates
import qs.Helpers
import qs.Config
ScrollBar {
@@ -15,6 +16,7 @@ ScrollBar {
property bool shouldBeActive
readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0
enabled: !Visibilities.getForActive().isDrawing
implicitWidth: Appearance.padding.extraSmall * 2
contentItem: Item {
@@ -59,11 +61,13 @@ ScrollBar {
implicitHeight: root.height * root.effectiveSize
implicitWidth: fullMouse.pressed || fullMouse.containsMouse ? Appearance.padding.extraSmall * 2 : Appearance.padding.extraSmall
opacity: {
if (!root.enabled)
return 0;
if (root.size === 1)
return 0;
if (fullMouse.pressed)
return 1;
if (mouse.containsMouse)
if (fullMouse.containsMouse)
return 0.8;
if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive)
return 0.6;
+4 -2
View File
@@ -91,7 +91,7 @@ Slider {
MaterialIcon {
id: inset
readonly property bool attached: root.pos < 0.1
readonly property bool attached: root.width ? (root.width - handle.implicitWidth - handle.anchors.leftMargin) * root.pos < inset.paintedWidth + Appearance.spacing.extraSmall * 2 : false
property real dockT: attached ? 1 : 0
anchors.bottom: parent.bottom
@@ -157,7 +157,9 @@ Slider {
}
}
Component.onCompleted: filledWidth = Qt.binding(() => (width - handle.implicitWidth - handle.anchors.leftMargin) * pos)
Component.onCompleted: {
filledWidth = Qt.binding(() => (width - handle.implicitWidth - handle.anchors.leftMargin) * pos);
}
Binding {
id: posBinding
+9 -4
View File
@@ -27,8 +27,7 @@ Text {
enabled: root.animate
SequentialAnimation {
Anim {
property: root.animateProp
TAnim {
target: root
to: root.animateFrom
type: Anim.FastEffects
@@ -37,12 +36,18 @@ Text {
PropertyAction {
}
Anim {
property: root.animateProp
TAnim {
target: root
to: root.animateTo
type: Anim.DefaultEffects
}
}
}
component TAnim: Anim {
duration: root.animateDuration / 2
properties: root.animateProp.split(",").length > 1 ? root.animateProp : ""
property: root.animateProp.split(",").length === 1 ? root.animateProp : ""
target: root
}
}
-1
View File
@@ -27,7 +27,6 @@ TextField {
property bool disableBlink
color: DynamicColors.palette.m3primary
height: root.cursorHeight
implicitWidth: 2
radius: Appearance.rounding.normal
+1 -1
View File
@@ -4,7 +4,6 @@ import qs.Config
ButtonBase {
id: root
property alias font: label.font
property alias icon: label.text
readonly property alias label: label
@@ -36,6 +35,7 @@ ButtonBase {
anchors.verticalCenterOffset: 1
color: root.onColor
fill: !root.isToggle || root.internalChecked ? 1 : 0
font.pointSize: root.font.pointSize
Behavior on fill {
Anim {
+70
View File
@@ -0,0 +1,70 @@
import QtQuick
import QtQuick.Layouts
import qs.Config
ButtonBase {
id: root
property alias icon: iconLabel.text
readonly property alias iconLabel: iconLabel
readonly property alias label: label
property alias text: label.text
activeColor: type === TextButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondary
activeOnColor: {
if (type === TextButton.Text)
return DynamicColors.palette.m3primary;
return type === TextButton.Filled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondary;
}
horizontalPadding: Appearance.padding.larger
implicitHeight: row.implicitHeight + verticalPadding * 2
implicitWidth: row.implicitWidth + horizontalPadding * 2
inactiveColor: {
if (!isToggle && type === TextButton.Filled)
return DynamicColors.palette.m3primary;
return type === TextButton.Filled ? DynamicColors.tPalette.m3surfaceContainer : DynamicColors.palette.m3secondaryContainer;
}
inactiveOnColor: {
if (!isToggle && type === TextButton.Filled)
return DynamicColors.palette.m3onPrimary;
if (type === TextButton.Text)
return DynamicColors.palette.m3primary;
return type === TextButton.Filled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer;
}
verticalPadding: Appearance.padding.small
RowLayout {
id: row
anchors.centerIn: parent
spacing: Appearance.spacing.small
MaterialIcon {
id: iconLabel
Layout.alignment: Qt.AlignVCenter
color: root.onColor
fill: root.internalChecked ? 1 : 0
font: {
const f = Qt.font(root.font);
f.pointSize = Math.round(root.font.pointSize * 1.2);
f.family = "Material Symbols Rounded";
return f;
}
Behavior on fill {
Anim {
type: Anim.DefaultEffects
}
}
}
CustomText {
id: label
Layout.alignment: Qt.AlignVCenter
color: root.onColor
font: root.font
}
}
}
+1
View File
@@ -51,6 +51,7 @@ MouseArea {
anchors.fill: parent
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
enabled: parent.enabled && !Visibilities.getForActive().isDrawing
hoverEnabled: true
Behavior on stateOpacity {
+40
View File
@@ -0,0 +1,40 @@
import QtQuick
import qs.Config
ButtonBase {
id: root
readonly property alias label: label
property alias text: label.text
activeColor: type === TextButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondary
activeOnColor: {
if (type === TextButton.Text)
return DynamicColors.palette.m3primary;
return type === TextButton.Filled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondary;
}
horizontalPadding: Appearance.padding.normal
implicitHeight: label.implicitHeight + verticalPadding * 2
implicitWidth: label.implicitWidth + horizontalPadding * 2
inactiveColor: {
if (!isToggle && type === TextButton.Filled)
return DynamicColors.palette.m3primary;
return type === TextButton.Filled ? DynamicColors.tPalette.m3surfaceContainer : DynamicColors.palette.m3secondaryContainer;
}
inactiveOnColor: {
if (!isToggle && type === TextButton.Filled)
return DynamicColors.palette.m3onPrimary;
if (type === TextButton.Text)
return DynamicColors.palette.m3primary;
return type === TextButton.Filled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer;
}
verticalPadding: Appearance.padding.small
CustomText {
id: label
anchors.centerIn: parent
color: root.onColor
font: root.font
}
}
+2
View File
@@ -77,9 +77,11 @@ JsonObject {
}
}
component Padding: JsonObject {
property int extraLarge: 28 * scale
property int extraLargeIncreased: 32 * scale
property int extraSmall: 4 * scale
property int large: 16 * scale
property int largeIncreased: 20 * scale
property int larger: 12 * scale
property int normal: 8 * scale
property real scale: 1
+3 -1
View File
@@ -188,7 +188,7 @@ Singleton {
resourceProgessThickness: dashboard.sizes.resourceProgessThickness,
weatherWidth: dashboard.sizes.weatherWidth,
mediaCoverArtSize: dashboard.sizes.mediaCoverArtSize,
mediaVisualiserSize: dashboard.sizes.mediaVisualiserSize,
mediaVisualizerSize: dashboard.sizes.mediaVisualizerSize,
resourceSize: dashboard.sizes.resourceSize
}
};
@@ -209,6 +209,7 @@ Singleton {
return {
logo: general.logo,
wallpaperPath: general.wallpaperPath,
showOverFullscreen: general.showOverFullscreen,
desktopIcons: general.desktopIcons,
dateFormat: general.dateFormat,
color: {
@@ -330,6 +331,7 @@ Singleton {
weatherLocation: services.weatherLocation,
updates: services.updates,
useFahrenheit: services.useFahrenheit,
minBrightness: services.minBrightness,
ddcutilService: services.ddcutilService,
useTwelveHourClock: services.useTwelveHourClock,
gpuType: services.gpuType,
+1 -1
View File
@@ -25,7 +25,7 @@ JsonObject {
readonly property int mediaCoverArtSize: 150
readonly property int mediaProgressSweep: 180
readonly property int mediaProgressThickness: 8
readonly property int mediaVisualiserSize: 80
readonly property int mediaVisualizerSize: 200
readonly property int mediaWidth: 200
readonly property int resourceProgessThickness: 10
readonly property int resourceSize: 200
+1
View File
@@ -13,6 +13,7 @@ JsonObject {
property Idle idle: Idle {
}
property string logo: ""
property bool showOverFullscreen: true
property string wallpaperPath: Quickshell.env("HOME") + "/Pictures/Wallpapers"
component Apps: JsonObject {
+1
View File
@@ -8,6 +8,7 @@ JsonObject {
property string defaultPlayer: "Spotify"
property string gpuType: ""
property real maxVolume: 1.0
property real minBrightness: 0.01
property list<var> playerAliases: [
{
"from": "com.github.th_ch.youtube_music",
+6
View File
@@ -215,6 +215,10 @@ Singleton {
notif.hasActionIcons = notif.notification.hasActionIcons;
}
function onHintsChanged(): void {
notif.hints = notif.notification.hints;
}
function onImageChanged(): void {
notif.imageSource = notif.notification.image || "";
notif.image = notif.imageSource;
@@ -237,6 +241,7 @@ Singleton {
}
property real expireTimeout: 5
property bool hasActionIcons
property var hints
property string image
property string imageSource
property var locks: new Set()
@@ -333,6 +338,7 @@ Singleton {
appName = notification.appName;
imageSource = notification.image || "";
image = imageSource;
hints = notification.hints;
expireTimeout = notification.expireTimeout;
urgency = notification.urgency;
resident = notification.resident;
+21 -27
View File
@@ -1,36 +1,30 @@
import QtQuick
pragma ComponentBehavior: Bound
Canvas {
import Quickshell
import QtQuick
import ZShell.Internal
Item {
id: root
property color penColor: "white"
property real penWidth: 4
property var points: []
readonly property alias content: contentLoader.item
readonly property PersistentProperties drawingState: PersistentProperties {
property color penColor: "white"
property int penWidth: 4
function clear(): void {
var ctx = getContext('2d');
root.points = [];
ctx.reset();
root.requestPaint();
reloadableId: "drawingState"
}
required property PersistentProperties visibilities
renderStrategy: Canvas.Cooperative
Loader {
id: contentLoader
onPaint: {
if (points.length < 2)
return;
var ctx = root.getContext('2d');
ctx.save();
ctx.lineWidth = root.penWidth;
ctx.strokeStyle = root.penColor;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (var i = 1; i < points.length; i++)
ctx.lineTo(points[i].x, points[i].y);
ctx.stroke();
points = points.slice(points.length - 2);
ctx.restore();
active: root.visibilities.isDrawing
anchors.fill: parent
sourceComponent: StrokeCanvas {
penColor: root.drawingState.penColor
penWidth: root.drawingState.penWidth
}
}
}
-60
View File
@@ -1,60 +0,0 @@
import Quickshell
import QtQuick
import qs.Components
import qs.Config
CustomMouseArea {
id: root
required property var bar
required property Drawing drawing
required property Panels panels
required property var popout
required property PersistentProperties visibilities
function inLeftPanel(panel: Item, x: real, y: real): bool {
return x < panel.x + panel.width + Config.barConfig.border && withinPanelHeight(panel, x, y);
}
function withinPanelHeight(panel: Item, x: real, y: real): bool {
const panelY = panel.y + bar.implicitHeight;
return y >= panelY && y <= panelY + panel.height;
}
acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: z > 0
hoverEnabled: true
visible: root.visibilities.isDrawing
onPositionChanged: event => {
const x = event.x;
const y = event.y;
if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) {
root.drawing.points.push(Qt.point(x, y));
root.drawing.requestPaint();
return;
}
if (!(event.buttons & Qt.LeftButton) && root.inLeftPanel(root.popout, x, y)) {
root.z = -2;
root.panels.drawing.expanded = true;
}
}
onPressed: event => {
const x = event.x;
const y = event.y;
if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) {
root.panels.drawing.expanded = false;
root.drawing.points.push(Qt.point(x, y));
root.drawing.requestPaint();
return;
}
if (event.buttons & Qt.RightButton)
root.drawing.clear();
}
onReleased: {
root.drawing.points = [];
}
}
+56 -6
View File
@@ -9,9 +9,9 @@ Item {
id: root
required property Item bar
required property real borderThickness
property bool dashboardShortcutActive
required property Drawing drawing
required property DrawingInput input
property bool osdShortcutActive
required property Panels panels
required property BarPopouts.Wrapper popouts
@@ -48,7 +48,7 @@ Item {
}
function withinPanelWidth(panel: Item, x: real, y: real): bool {
const panelX = panel.x;
const panelX = panel.x + root.borderThickness;
return x >= panelX && x <= panelX + panel.width;
}
@@ -59,6 +59,7 @@ Item {
cursorShape: (active && centroid.pressPosition.y < root.bar.implicitHeight) ? Qt.ClosedHandCursor : undefined
dragThreshold: 0
enabled: !root.visibilities.isDrawing
grabPermissions: PointerHandler.CanTakeOverFromHandlersOfSameType | PointerHandler.ApprovesTakeOverByAnything
maximumPointCount: 1
minimumPointCount: 1
@@ -93,7 +94,7 @@ Item {
if (centroid.pressPosition.y > root.screen.height - Config.barConfig.border && centroid.pressPosition.x < root.screen.width / 5 && dragY < -50)
root.visibilities.clipboard = true;
if (!Config.dock.hoverToReveal && centroid.pressPosition.y > root.screen.height - root.bar.implicitHeight && centroid.pressPosition.x > root.screen.width / 5)
if (!Config.dock.hoverToReveal && centroid.pressPosition.y > root.screen.height - root.bar.implicitHeight && centroid.pressPosition.x > root.screen.width / 5 && !root.visibilities.launcher)
if (dragY < -10) {
root.visibilities.dock = true;
root.singleGestureTriggered = true;
@@ -116,9 +117,52 @@ Item {
}
}
PointHandler {
id: drawingHandler
property bool setInitialPoint: false
acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: root.visibilities.isDrawing && (!root.inLeftPanel(root.panels.drawing, hoverHandler.point.position.x, hoverHandler.point.position.y) || !root.panels.drawing.expanded)
onActiveChanged: {
if (!active) {
setInitialPoint = false;
root.drawing.content.endStroke();
} else {
root.panels.drawing.collapse();
}
}
onPointChanged: {
if (!active)
return;
const x = point.position.x;
const y = point.position.y;
const origX = point.pressPosition.x;
const origY = point.pressPosition.y;
if (point.pressedButtons & Qt.RightButton) {
root.drawing.content.clear();
return;
}
if (x === 0 && y === 0 && origX === 0 && origY === 0)
return;
if (!setInitialPoint) {
setInitialPoint = true;
root.drawing.content.beginStroke(origX, origY);
return;
}
root.drawing.content.appendPoint(x, y);
}
}
HoverHandler {
id: hoverHandler
cursorShape: root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, point.position.x, point.position.y) ? Qt.BlankCursor : undefined
onHoveredChanged: {
if (!hovered) {
if (!root.osdShortcutActive) {
@@ -138,9 +182,15 @@ Item {
const x = point.position.x;
const y = point.position.y;
if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) {
root.input.z = 2;
root.panels.drawing.expanded = false;
if (root.visibilities.isDrawing) {
if (root.inLeftPanel(root.panels.drawing, x, y) && !(drawingHandler.point.pressedButtons & Qt.LeftButton)) {
root.panels.drawing.expand();
root.drawing.content.hideHover();
return;
}
root.drawing.content.showHover(x, y);
return;
}
if (!root.visibilities.bar && Config.barConfig.autoHide && y < root.bar.implicitHeight)
+4 -2
View File
@@ -20,12 +20,13 @@ Item {
id: root
required property Item bar
required property real borderThickness
readonly property alias clipboard: clipboard
readonly property alias dashboard: dashboard
readonly property alias dashboardWrapper: dashboardWrapper
readonly property alias dock: dock
readonly property alias drawing: drawing
required property Canvas drawingItem
required property var drawingItem
readonly property alias launcher: launcher
readonly property alias notifications: notifications
readonly property alias osd: osd
@@ -43,7 +44,7 @@ Item {
required property PersistentProperties visibilities
anchors.fill: parent
anchors.margins: Config.barConfig.border
anchors.margins: borderThickness
anchors.topMargin: bar.implicitHeight
Item {
@@ -99,6 +100,7 @@ Item {
id: popouts
anchors.top: parent.top
borderThickness: root.borderThickness
screen: root.screen
}
+99 -47
View File
@@ -18,18 +18,56 @@ CustomWindow {
id: root
readonly property alias bar: bar
readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2)
readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.barConfig.border
readonly property real borderRounding: Config.barConfig.rounding * (1 - fsTransitionProg)
readonly property real borderThickness: Config.barConfig.border * (1 - fsTransitionProg)
readonly property int dragMaskPadding: {
if (focusGrab.active)
return 0;
if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace.lastIpcObject.windows > 0)
return 0;
return 100;
}
property real fsTransitionProg: hasFullscreen ? 1 : 0
readonly property bool hasFullscreen: {
if (hasSpecialWorkspace) {
const specialName = monitor?.lastIpcObject.specialWorkspace?.name;
if (!specialName)
return false;
const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName);
return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false;
}
return hasFullscreenOnNormalWs;
}
readonly property bool hasFullscreenOnNormalWs: monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false
readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject.specialWorkspace?.name.length ?? 0) > 0
readonly property alias interactionWrapper: interactions
readonly property alias menuRegion: menuPopoutRegion
readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)
property var root: Quickshell.shellDir
readonly property real sdfBorderOffset: 2 * fsTransitionProg
readonly property real shadowOpacity: 0.7 * (1 - fsTransitionProg)
property color surfaceColor: DynamicColors.tPalette.m3surface
WlrLayershell.exclusionMode: ExclusionMode.Ignore
// WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.settings ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top
color: "transparent"
contentItem.focus: true
mask: visibilities.isDrawing ? null : region
mask: visibilities.isDrawing ? null : (hasFullscreen ? emptyRegion : region)
name: "Bar"
Behavior on fsTransitionProg {
Anim {
}
}
Behavior on surfaceColor {
CAnim {
}
}
contentItem.Keys.onEscapePressed: {
if (Config.barConfig.autoHide)
visibilities.bar = false;
@@ -61,15 +99,24 @@ CustomWindow {
intersection: Intersection.Subtract
}
Region {
id: emptyRegion
height: panels.notifications.height
width: panels.notifications.width
x: panels.notifications.x + root.borderThickness
y: panels.notifications.y + bar.implicitHeight
}
Region {
id: region
height: root.height - bar.implicitHeight - Config.barConfig.border
height: root.height - bar.implicitHeight - root.borderThickness - root.dragMaskPadding * 2
intersection: Intersection.Xor
regions: [...popoutRegions.instances, menuPopoutRegion]
width: root.width - Config.barConfig.border * 2
x: Config.barConfig.border
y: bar.implicitHeight
width: root.width - root.borderThickness * 2 - root.dragMaskPadding * 2
x: root.borderThickness + root.dragMaskPadding
y: bar.implicitHeight + root.dragMaskPadding
}
anchors {
@@ -90,7 +137,7 @@ CustomWindow {
height: modelData.height
intersection: Intersection.Subtract
width: modelData.width
x: modelData.x + Config.barConfig.border
x: modelData.x + root.borderThickness
y: modelData.y + bar.implicitHeight
}
}
@@ -148,37 +195,34 @@ CustomWindow {
}
Item {
id: surface
anchors.fill: parent
layer.enabled: true
opacity: Appearance.transparency.enabled ? DynamicColors.transparency.base : 1
opacity: root.surfaceColor.a
layer.effect: MultiEffect {
blurMax: 32
shadowColor: Qt.alpha(DynamicColors.palette.m3shadow, 1)
shadowColor: Qt.alpha(DynamicColors.palette.m3shadow, Math.max(0, root.shadowOpacity))
shadowEnabled: true
}
BlobGroup {
id: blobGroup
color: DynamicColors.palette.m3surface
color: root.surfaceColor
smoothing: Config.barConfig.smoothing
Behavior on color {
CAnim {
}
}
}
BlobInvertedRect {
anchors.fill: parent
anchors.margins: -50
borderBottom: Config.barConfig.border - anchors.margins
borderLeft: Config.barConfig.border - anchors.margins
borderRight: Config.barConfig.border - anchors.margins
borderTop: bar.implicitHeight - anchors.margins
borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset
borderLeft: root.borderThickness - anchors.margins - root.sdfBorderOffset
borderRight: root.borderThickness - anchors.margins - root.sdfBorderOffset
borderTop: bar.implicitHeight - anchors.margins - root.sdfBorderOffset
group: blobGroup
radius: Config.barConfig.rounding
radius: root.borderRounding
}
PanelBg {
@@ -191,7 +235,7 @@ CustomWindow {
implicitWidth: panels.dashboard.width
panel: panels.dashboardWrapper
radius: Appearance.rounding.normal
x: panels.dashboardWrapper.x + panels.dashboard.x + Config.barConfig.border
x: panels.dashboardWrapper.x + panels.dashboard.x + root.borderThickness
y: panels.dashboardWrapper.y + panels.dashboard.y + bar.implicitHeight - panels.dashboard.height * extraHeight
}
@@ -225,7 +269,7 @@ CustomWindow {
implicitWidth: panels.osd.width
panel: panels.osdWrapper
radius: 20
x: panels.osdWrapper.x + panels.osd.x + Config.barConfig.border
x: panels.osdWrapper.x + panels.osd.x + root.borderThickness
y: panels.osdWrapper.y + panels.osd.y + bar.implicitHeight
}
@@ -255,7 +299,7 @@ CustomWindow {
implicitWidth: panels.popouts.width
panel: panels.popoutsWrapper
radius: panels.popouts.current?.panelRadius ?? Appearance.rounding.normal
x: panels.popoutsWrapper.x + panels.popouts.x + Config.barConfig.border
x: panels.popoutsWrapper.x + panels.popouts.x + root.borderThickness
y: panels.popoutsWrapper.y + panels.popouts.y + bar.implicitHeight - panels.popouts.height * extraHeight
Behavior on extraHeight {
@@ -271,8 +315,8 @@ CustomWindow {
implicitHeight: panels.resources.height
implicitWidth: panels.resources.width
panel: panels.resourcesWrapper
radius: Appearance.rounding.normal
x: panels.resourcesWrapper.x + panels.resources.x + Config.barConfig.border
radius: Appearance.rounding.large
x: panels.resourcesWrapper.x + panels.resources.x + root.borderThickness
y: panels.resourcesWrapper.y + panels.resources.y + bar.implicitHeight
}
@@ -288,7 +332,7 @@ CustomWindow {
radius: Appearance.rounding.large
topLeftRadius: Appearance.rounding.large + Appearance.padding.smaller
topRightRadius: Appearance.rounding.large + Appearance.padding.smaller
x: panels.settingsWrapper.x + panels.settings.x + Config.barConfig.border
x: panels.settingsWrapper.x + panels.settings.x + root.borderThickness
y: panels.settingsWrapper.y + panels.settings.y + bar.implicitHeight - panels.settings.height * extraHeight
}
@@ -317,32 +361,37 @@ CustomWindow {
}
}
Loader {
id: drawingLoader
Drawing {
id: drawing
active: visibilities.isDrawing
anchors.fill: parent
layer.enabled: true
visibilities: visibilities
z: 2
sourceComponent: Drawing {
id: drawing
layer.effect: MultiEffect {
maskEnabled: true
maskInverted: true
maskSource: maskSource
}
}
Loader {
id: inputLoader
Item {
id: maskSource
active: visibilities.isDrawing
anchors.fill: parent
z: 2
layer.enabled: true
visible: false
sourceComponent: DrawingInput {
id: input
CustomRect {
readonly property int extraWidth: radius
bar: bar
drawing: drawingLoader.item
panels: panels
popout: panels.drawing
visibilities: visibilities
color: "white"
implicitHeight: panels.drawing.height
implicitWidth: panels.drawing.width + extraWidth
radius: drawingBg.radius
x: -extraWidth + root.borderThickness + panels.drawing.x
y: panels.drawing.y + bar.implicitHeight
}
}
@@ -351,9 +400,9 @@ CustomWindow {
anchors.fill: parent
bar: bar
drawing: drawingLoader.item
borderThickness: root.borderLayoutThickness
drawing: drawing
enabled: true
input: inputLoader.item
panels: panels
popouts: panels.popouts
screen: root.screen
@@ -363,7 +412,8 @@ CustomWindow {
id: panels
bar: bar
drawingItem: drawingLoader.item
borderThickness: root.borderThickness
drawingItem: drawing
screen: root.screen
visibilities: visibilities
@@ -407,6 +457,8 @@ CustomWindow {
anchors.left: parent.left
anchors.right: parent.right
enabled: !visibilities.isDrawing
fullscreen: root.hasFullscreen
popouts: panels.popouts
popoutsWrapper: panels.popoutsWrapper
screen: root.screen
@@ -423,7 +475,7 @@ CustomWindow {
implicitHeight: panel.height
implicitWidth: panel.width
radius: Appearance.rounding.smallest
x: panel.x + Config.barConfig.border
x: panel.x + root.borderThickness
y: panel.y + bar.implicitHeight
}
}
+1 -1
View File
@@ -232,7 +232,7 @@ Singleton {
}
function setBrightness(value: real): void {
value = Math.max(0, Math.min(1, value));
value = Math.max(Config.services.minBrightness, Math.min(1, value));
const rounded = Math.round(value * 100);
if (Math.round(brightness * 100) === rounded)
return;
+30 -114
View File
@@ -20,10 +20,8 @@ Singleton {
}))
property string previewImageFile: "/tmp/qs-cliphist-preview.img"
property string previewImageSource: ""
property bool previewIsCode: looksLikeCode(previewText)
property bool previewIsImage: false
readonly property string previewMarkup: previewIsCode ? generateHighlightedMarkup(previewText) : previewText
property string previewText: ""
property list<var> previewText: []
property int previewToken: 0
property real scoreThreshold: 0.2
@@ -59,119 +57,37 @@ Singleton {
});
}
function generateHighlightedMarkup(text): string {
const raw = String(text ?? "");
const lines = raw.split("\n").map(line => highlightCodeLine(line));
return `<div style="font-family:monospace; white-space:pre-wrap;">${lines.join("<br>")}</div>`;
}
function highlightCodeLine(rawLine): string {
const line = String(rawLine ?? "");
const kwColor = DynamicColors.palette.m3primary;
const strColor = DynamicColors.palette.m3tertiary;
const comColor = Qt.alpha(DynamicColors.palette.m3onSurface, 0.55);
const keywordRe = /\b(function|class|import|const|let|var|if|else|for|while|return|switch|case|break|continue|try|catch|throw|async|await|new|null|true|false|public|private|protected|static|extends|struct|enum)\b/g;
let out = "";
let i = 0;
while (i < line.length) {
const ch = line[i];
// Line comment
if (line.slice(i, i + 2) === "//") {
out += `<span style="color:${comColor};">${escapeHtml(line.slice(i))}</span>`;
break;
}
// Shell/Python-style comment
if (ch === "#" && i === 0) {
out += `<span style="color:${comColor};">${escapeHtml(line.slice(i))}</span>`;
break;
}
// Quoted string
if (ch === "'" || ch === '"' || ch === "`") {
const quote = ch;
let j = i + 1;
while (j < line.length) {
if (line[j] === "\\") {
j += 2;
continue;
}
if (line[j] === quote) {
j += 1;
break;
}
j += 1;
}
out += `<span style="color:${strColor};">${escapeHtml(line.slice(i, j))}</span>`;
i = j;
continue;
}
// Plain code text until next special token
let j = i;
while (j < line.length) {
const two = line.slice(j, j + 2);
const c = line[j];
if (two === "//")
break;
if (c === "'" || c === '"' || c === "`")
break;
if (c === "#" && j === 0)
break;
j += 1;
}
let segment = escapeHtml(line.slice(i, j));
segment = segment.replace(keywordRe, `<span style="color:${kwColor}; font-weight:600;">$1</span>`);
out += segment;
i = j;
}
return out === "" ? "&nbsp;" : out;
}
function looksLikeCode(text): bool {
const t = String(text ?? "").trim();
if (t === "")
return false;
const lines = t.split("\n");
if (lines.length < 2)
return false;
let score = 0;
for (const line of lines) {
if (/^\s{4,}|\t/.test(line))
score += 2;
if (/[{}()[\];]/.test(line))
score += 1;
if (/\b(function|class|import|const|let|var|if|else|for|while|return|switch|case|try|catch|async|await)\b/.test(line))
score += 2;
if (/=>|:=/.test(line))
score += 1;
}
return score >= 4;
}
function paste(entry): void {
Quickshell.execDetached(["bash", "-c", `printf '${shellSingleQuoteEscape(entry)}' | ${root.cliphistBinary} decode | wl-copy && wl-paste`]);
}
function processPreviewLines(text: string): var {
if (text === "")
return [];
const lines = text.split("\n");
let minIndent = Infinity;
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(/^(\s*)/);
const indent = m ? m[1].length : 0;
if (lines[i].trim().length > 0 && indent < minIndent)
minIndent = indent;
}
if (minIndent === Infinity)
minIndent = 0;
const result = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const indentMatch = line.match(/^(\s*)/);
const indentLen = indentMatch ? indentMatch[1].length : 0;
const stripped = line.slice(Math.min(minIndent, indentLen));
result.push({
line: i + 1,
text: stripped
});
}
return result;
}
function refresh(): void {
readProc.buffer = [];
readProc.running = true;
@@ -182,7 +98,7 @@ Singleton {
const token = previewToken;
if (!currentEntry) {
previewText = "";
previewText = [];
previewImageSource = "";
previewIsImage = false;
return;
@@ -269,7 +185,7 @@ Singleton {
onStreamFinished: {
if (previewTextProc.token !== root.previewToken)
return;
root.previewText = this.text;
root.previewText = root.processPreviewLines(this.text);
}
}
}
+21 -1
View File
@@ -39,6 +39,21 @@ Singleton {
readonly property real uploadSpeed: _uploadSpeed
readonly property real uploadTotal: _uploadTotal
function resetSampling(): void {
netDevFile.reload();
const content = netDevFile.text();
if (!content)
return;
const data = root.parseNetDev(content);
const now = Date.now();
root._prevRxBytes = data.rx;
root._prevTxBytes = data.tx;
root._prevTimestamp = now;
}
function formatBytes(bytes: real): var {
// Handle negative or invalid values
if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {
@@ -156,10 +171,15 @@ Singleton {
Timer {
interval: Config.dashboard.resourceUpdateInterval
repeat: true
running: root.refCount > 0
running: true
triggeredOnStart: true
onTriggered: {
if (root.refCount <= 0) {
root.resetSampling();
return;
}
netDevFile.reload();
const content = netDevFile.text();
if (!content)
-464
View File
@@ -1,464 +0,0 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
import qs.Config
Singleton {
id: root
property string autoGpuType: "NONE"
property string cpuName: ""
property real cpuPerc
property real cpuTemp
// Individual disks: Array of { mount, used, total, free, perc }
property var disks: []
property real gpuMemTotal: 0
property real gpuMemUsed
property string gpuName
property real gpuPerc
property real gpuTemp
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
property real lastCpuIdle
property real lastCpuTotal
readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
property real memTotal
property real memUsed
property int refCount
readonly property real storagePerc: {
let totalUsed = 0;
let totalSize = 0;
for (const disk of disks) {
totalUsed += disk.used;
totalSize += disk.total;
}
return totalSize > 0 ? totalUsed / totalSize : 0;
}
function cleanCpuName(name: string): string {
return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/CPU/gi, "").replace(/\d+th Gen /gi, "").replace(/\d+nd Gen /gi, "").replace(/\d+rd Gen /gi, "").replace(/\d+st Gen /gi, "").replace(/Core /gi, "").replace(/Processor/gi, "").replace(/\s+/g, " ").trim();
}
function cleanGpuName(name: string): string {
return name.replace(/NVIDIA GeForce /gi, "").replace(/NVIDIA /gi, "").replace(/AMD Radeon /gi, "").replace(/AMD /gi, "").replace(/Intel /gi, "").replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim();
}
function formatKib(kib: real): var {
const mib = 1024;
const gib = 1024 ** 2;
const tib = 1024 ** 3;
if (kib >= tib)
return {
value: kib / tib,
unit: "TiB"
};
if (kib >= gib)
return {
value: kib / gib,
unit: "GiB"
};
if (kib >= mib)
return {
value: kib / mib,
unit: "MiB"
};
return {
value: kib,
unit: "KiB"
};
}
Timer {
interval: Config.dashboard.resourceUpdateInterval
repeat: true
running: root.refCount > 0
triggeredOnStart: true
onTriggered: {
stat.reload();
meminfo.reload();
if (root.gpuType === "GENERIC")
gpuUsage.running = true;
if (root.gpuType === "GENERIC" && root.gpuMemTotal === 0)
oneshotMemAmd.running = true;
}
}
Timer {
interval: 60000 * 120
repeat: true
running: true
triggeredOnStart: true
onTriggered: {
storage.running = true;
}
}
Timer {
interval: Config.dashboard.resourceUpdateInterval * 5
repeat: true
running: root.refCount > 0
triggeredOnStart: true
onTriggered: {
sensors.running = true;
}
}
FileView {
id: cpuinfoInit
path: "/proc/cpuinfo"
onLoaded: {
const nameMatch = text().match(/model name\s*:\s*(.+)/);
if (nameMatch)
root.cpuName = root.cleanCpuName(nameMatch[1]);
}
}
FileView {
id: stat
path: "/proc/stat"
onLoaded: {
const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/);
if (data) {
const stats = data.slice(1).map(n => parseInt(n, 10));
const total = stats.reduce((a, b) => a + b, 0);
const idle = stats[3] + (stats[4] ?? 0);
const totalDiff = total - root.lastCpuTotal;
const idleDiff = idle - root.lastCpuIdle;
const newCpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;
root.lastCpuTotal = total;
root.lastCpuIdle = idle;
if (Math.abs(newCpuPerc - root.cpuPerc) >= 0.01)
root.cpuPerc = newCpuPerc;
}
}
}
FileView {
id: meminfo
path: "/proc/meminfo"
onLoaded: {
const data = text();
const total = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
const used = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;
if (root.memTotal !== total)
root.memTotal = total;
if (Math.abs(used - root.memUsed) >= 16384)
root.memUsed = used;
}
}
Process {
id: storage
command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"]
stdout: StdioCollector {
onStreamFinished: {
const diskMap = {}; // Map disk name -> { name, totalSize, used, fsTotal }
const lines = text.trim().split("\n");
for (const line of lines) {
if (line.trim() === "")
continue;
const nameMatch = line.match(/NAME="([^"]+)"/);
const sizeMatch = line.match(/SIZE="([^"]+)"/);
const typeMatch = line.match(/TYPE="([^"]+)"/);
const fsusedMatch = line.match(/FSUSED="([^"]*)"/);
const fssizeMatch = line.match(/FSSIZE="([^"]*)"/);
if (!nameMatch || !typeMatch)
continue;
const name = nameMatch[1];
const type = typeMatch[1];
const size = parseInt(sizeMatch?.[1] || "0", 10);
const fsused = parseInt(fsusedMatch?.[1] || "0", 10);
const fssize = parseInt(fssizeMatch?.[1] || "0", 10);
if (type === "disk") {
// Skip zram (swap) devices
if (name.startsWith("zram"))
continue;
// Initialize disk entry
if (!diskMap[name]) {
diskMap[name] = {
name: name,
totalSize: size,
used: 0,
fsTotal: 0
};
}
} else if (type === "part") {
// Find parent disk (remove trailing numbers/p+numbers)
let parentDisk = name.replace(/p?\d+$/, "");
// For nvme devices like nvme0n1p1, parent is nvme0n1
if (name.match(/nvme\d+n\d+p\d+/))
parentDisk = name.replace(/p\d+$/, "");
// Aggregate partition usage to parent disk
if (diskMap[parentDisk]) {
diskMap[parentDisk].used += fsused;
diskMap[parentDisk].fsTotal += fssize;
}
}
}
const diskList = [];
let totalUsed = 0;
let totalSize = 0;
for (const diskName of Object.keys(diskMap).sort()) {
const disk = diskMap[diskName];
// Use filesystem total if available, otherwise use disk size
const total = disk.fsTotal > 0 ? disk.fsTotal : disk.totalSize;
const used = disk.used;
const perc = total > 0 ? used / total : 0;
// Convert bytes to KiB for consistency with formatKib
diskList.push({
mount: disk.name // Using 'mount' property for compatibility
,
used: used / 1024,
total: total / 1024,
free: (total - used) / 1024,
perc: perc
});
totalUsed += used;
totalSize += total;
}
root.disks = diskList;
}
}
}
Process {
id: gpuNameDetect
command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || lspci 2>/dev/null | grep -i 'vga\\|3d\\|display' | head -1"]
running: true
stdout: StdioCollector {
onStreamFinished: {
const output = text.trim();
if (!output)
return;
// Check if it's from nvidia-smi (clean GPU name)
if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) {
root.gpuName = root.cleanGpuName(output);
} else {
// Parse lspci output: extract name from brackets or after colon
const bracketMatch = output.match(/\[([^\]]+)\]/);
if (bracketMatch) {
root.gpuName = root.cleanGpuName(bracketMatch[1]);
} else {
const colonMatch = output.match(/:\s*(.+)/);
if (colonMatch)
root.gpuName = root.cleanGpuName(colonMatch[1]);
}
}
}
}
}
Process {
id: gpuTypeCheck
command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"]
running: !Config.services.gpuType
stdout: StdioCollector {
onStreamFinished: root.autoGpuType = text.trim()
}
}
Process {
id: oneshotMem
command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"]
running: root.gpuType === "NVIDIA" && root.gpuMemTotal === 0
stdout: StdioCollector {
onStreamFinished: {
root.gpuMemTotal = Number(this.text.trim());
oneshotMem.running = false;
}
}
}
Process {
id: oneshotMemAmd
command: ["sh", "-c", "cat /sys/class/drm/card*/device/mem_info_vram_total"]
running: root.gpuType === "GENERIC" && root.gpuMemTotal === 0
stdout: StdioCollector {
onStreamFinished: {
const values = text.trim().split("\n").map(v => parseInt(v, 10)).filter(v => Number.isFinite(v));
if (values.length > 0) {
const totalBytes = values.reduce((a, b) => a + b, 0);
root.gpuMemTotal = totalBytes / (1024 * 1024);
}
oneshotMemAmd.running = false;
}
}
}
Process {
id: gpuUsageNvidia
command: ["/usr/bin/nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits", "-lms", "1000"]
running: root.refCount > 0 && root.gpuType === "NVIDIA"
stdout: SplitParser {
onRead: data => {
const parts = String(data).trim().split(/\s*,\s*/);
if (parts.length < 3)
return;
const usageRaw = parseInt(parts[0], 10);
const tempRaw = parseInt(parts[1], 10);
const memRaw = parseInt(parts[2], 10);
if (!Number.isFinite(usageRaw) || !Number.isFinite(tempRaw) || !Number.isFinite(memRaw))
return;
const newGpuPerc = Math.max(0, Math.min(1, usageRaw / 100));
const newGpuTemp = tempRaw;
const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memRaw / root.gpuMemTotal)) : 0;
// Only publish meaningful changes to avoid needless binding churn / repaints
if (Math.abs(root.gpuPerc - newGpuPerc) >= 0.01)
root.gpuPerc = newGpuPerc;
if (Math.abs(root.gpuTemp - newGpuTemp) >= 1)
root.gpuTemp = newGpuTemp;
if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01)
root.gpuMemUsed = newGpuMemUsed;
}
}
}
Process {
id: gpuUsage
command: root.gpuType === "GENERIC" ? ["sh", "-c", "paste -d ' ' /sys/class/drm/card*/device/gpu_busy_percent /sys/class/drm/card*/device/mem_info_vram_used"] : ["echo"]
stdout: StdioCollector {
onStreamFinished: {
if (root.gpuType === "GENERIC") {
const lines = text.trim().split("\n");
let percSum = 0;
let memSum = 0;
let count = 0;
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length < 2)
continue;
const gpuBusy = parseInt(parts[0], 10);
const memUsed = parseInt(parts[1], 10);
if (!Number.isFinite(gpuBusy) || !Number.isFinite(memUsed))
continue;
percSum += gpuBusy;
memSum += memUsed;
count++;
}
if (count > 0) {
// GPU usage %
root.gpuPerc = (percSum / count) / 100;
// VRAM usage (bytes → MiB → normalized)
const memUsedMiB = memSum / (1024 * 1024);
const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memUsedMiB / root.gpuMemTotal)) : 0;
if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01)
root.gpuMemUsed = newGpuMemUsed;
}
} else {
root.gpuPerc = 0;
root.gpuTemp = 0;
}
}
}
}
Process {
id: sensors
command: ["sensors"]
environment: ({
LANG: "C.UTF-8",
LC_ALL: "C.UTF-8"
})
stdout: StdioCollector {
onStreamFinished: {
let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/);
if (!cpuTemp)
// If AMD Tdie pattern failed, try fallback on Tctl
cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/);
if (cpuTemp && Math.abs(parseFloat(cpuTemp[1]) - root.cpuTemp) >= 0.5)
root.cpuTemp = parseFloat(cpuTemp[1]);
if (root.gpuType !== "GENERIC")
return;
let eligible = false;
let sum = 0;
let count = 0;
for (const line of text.trim().split("\n")) {
if (line === "Adapter: PCI adapter")
eligible = true;
else if (line === "")
eligible = false;
else if (eligible) {
let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
if (!match)
// Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs)
match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
if (match) {
sum += parseFloat(match[2]);
count++;
}
}
}
root.gpuTemp = count > 0 ? sum / count : 0;
}
}
}
}
+15 -12
View File
@@ -32,33 +32,26 @@ Searcher {
showPreview = true;
}
function setCrop(screen: string, rect: rect, scaledRect: rect, zoom: real): void {
let updated = Object.assign({}, root.crops);
function setCrop(screen: string, rect: rect, zoom: real): void {
if (zoom <= 0)
zoom = 1.0;
else if (zoom > 5.0)
zoom = 5.0;
updated[screen] = {
root.crops[screen] = {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
scaledX: scaledRect.x,
scaledY: scaledRect.y,
scaledWidth: scaledRect.width,
scaledHeight: scaledRect.height,
zoom: zoom
};
root.crops = updated;
// root.crops = updated;
}
function setWallpaper(path: string): void {
actualCurrent = path;
WallpaperPath.currentWallpaperPath = path;
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), Qt.rect(0, 0, 0, 0), 1.0));
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0));
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
if (Config.general.color.schemeGeneration)
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]);
@@ -91,7 +84,7 @@ Searcher {
path: `${Paths.state}/wallpaper-crops.json`
watchChanges: true
onAdapterUpdated: writeAdapter()
onAdapterUpdated: cropWriteDelay.restart()
onFileChanged: reload()
JsonAdapter {
@@ -101,6 +94,16 @@ Searcher {
}
}
Timer {
id: cropWriteDelay
interval: 100
repeat: false
running: false
onTriggered: monitorCrops.writeAdapter()
}
FileSystemModel {
id: wallpapers
+10
View File
@@ -18,6 +18,7 @@ RowLayout {
required property Wrapper popouts
required property ClipWrapper popoutsWrapper
required property ShellScreen screen
required property bool fullscreen
readonly property int vPadding: 6
required property PersistentProperties visibilities
@@ -91,6 +92,7 @@ RowLayout {
delegate: WrappedLoader {
sourceComponent: HyprsunsetWidget {
visible: !root.fullscreen
}
}
}
@@ -109,6 +111,7 @@ RowLayout {
delegate: WrappedLoader {
sourceComponent: Workspaces {
screen: root.screen
visible: !root.fullscreen
}
}
}
@@ -120,6 +123,7 @@ RowLayout {
sourceComponent: TrayWidget {
loader: root
popouts: root.popouts
visible: !root.fullscreen
}
}
}
@@ -130,6 +134,7 @@ RowLayout {
delegate: WrappedLoader {
sourceComponent: Resources {
visibilities: root.visibilities
visible: !root.fullscreen
}
}
}
@@ -139,6 +144,7 @@ RowLayout {
delegate: WrappedLoader {
sourceComponent: UpdatesWidget {
visible: !root.fullscreen
}
}
}
@@ -150,6 +156,7 @@ RowLayout {
sourceComponent: NotifBell {
popouts: root.popouts
visibilities: root.visibilities
visible: !root.fullscreen
}
}
}
@@ -162,6 +169,7 @@ RowLayout {
loader: root
popouts: root.popouts
visibilities: root.visibilities
visible: !root.fullscreen
}
}
}
@@ -172,6 +180,7 @@ RowLayout {
delegate: WrappedLoader {
sourceComponent: WindowTitle {
bar: root
visible: !root.fullscreen
}
}
}
@@ -190,6 +199,7 @@ RowLayout {
delegate: WrappedLoader {
sourceComponent: MediaWidget {
visible: !root.fullscreen
}
}
}
+4 -2
View File
@@ -17,12 +17,13 @@ Item {
readonly property int contentHeight: Config.barConfig.height + padding * 2
readonly property int exclusiveZone: Config.barConfig.autoHide ? Config.barConfig.border : contentHeight
required property bool fullscreen
property bool isHovered
readonly property int padding: Math.max(Appearance.padding.smaller, Config.barConfig.border)
required property Wrapper popouts
required property ClipWrapper popoutsWrapper
required property ShellScreen screen
readonly property bool shouldBeVisible: (!Config.barConfig.autoHide || visibilities.bar || isHovered)
readonly property bool shouldBeVisible: !fullscreen && (!Config.barConfig.autoHide || visibilities.bar || isHovered)
readonly property int vPadding: 6
required property PersistentProperties visibilities
@@ -30,7 +31,7 @@ Item {
content.item?.checkPopout(x);
}
implicitHeight: Config.barConfig.border
implicitHeight: fullscreen ? 0 : Config.barConfig.border
visible: height > Config.barConfig.border
states: State {
@@ -75,6 +76,7 @@ Item {
anchors.right: parent.right
sourceComponent: Bar {
fullscreen: root.fullscreen
height: root.contentHeight
popouts: root.popouts
popoutsWrapper: root.popoutsWrapper
+2 -1
View File
@@ -8,6 +8,7 @@ import qs.Config
Item {
id: root
required property real borderThickness
readonly property alias content: content
property real offsetScale: y > 0 || content.hasCurrent ? 0 : 1
required property ShellScreen screen
@@ -17,7 +18,7 @@ Item {
implicitWidth: content.implicitWidth
visible: width > 0 && height > 0
x: {
const off = content.currentCenter - Config.barConfig.border - content.nonAnimWidth / 2;
const off = content.currentCenter - borderThickness - content.nonAnimWidth / 2;
const diff = parent.width - Math.floor(off + content.nonAnimWidth);
if (diff < 0)
return off + diff;
+172 -92
View File
@@ -71,7 +71,7 @@ Item {
anchors.leftMargin: Appearance.spacing.normal
anchors.top: parent.top
color: DynamicColors.tPalette.m3surfaceContainer
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + imagePreview.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreview.paintedWidth + textPreview.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth)
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Appearance.padding.large * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreviewColumn.width + textPreviewColumn.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth)
radius: 25
Behavior on implicitWidth {
@@ -79,25 +79,111 @@ Item {
}
}
CustomText {
id: textPreview
Column {
id: textPreviewColumn
anchors.left: parent.left
anchors.margins: Appearance.padding.large
anchors.leftMargin: 0
anchors.margins: Appearance.padding.normal
anchors.top: parent.top
font.family: ClipHistory.previewIsCode ? Appearance.font.family.mono : Appearance.font.family.sans
text: ClipHistory.previewMarkup
textFormat: ClipHistory.previewIsCode ? Text.RichText : Text.PlainText
visible: !ClipHistory.previewIsImage
width: Config.clipboard.sizes.previewWidth - anchors.margins * 2
wrapMode: Text.Wrap
Repeater {
id: processedLines
model: ClipHistory.previewText
Row {
id: lineRow
required property int index
required property var modelData
spacing: Appearance.spacing.normal
CustomRect {
color: lineRow.index % 2 ? DynamicColors.tPalette.m3surfaceContainer : "transparent"
implicitHeight: lineText.paintedHeight + Appearance.padding.extraSmall * 2
implicitWidth: 50
CustomText {
id: number
anchors.margins: Appearance.padding.large
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: lineRow.modelData.line
}
}
Repeater {
model: lineRow.modelData.text.split("\t").slice(1)
RowLayout {
height: lineText.height
width: lineText.height + Appearance.spacing.extraSmall
Item {
Layout.fillHeight: true
Layout.fillWidth: true
CustomRect {
anchors.centerIn: parent
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: parent.height / 8
implicitWidth: parent.height / 8
radius: Appearance.rounding.full
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
CustomRect {
anchors.centerIn: parent
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: parent.height / 8
implicitWidth: parent.height / 8
radius: Appearance.rounding.full
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
CustomRect {
anchors.centerIn: parent
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: parent.height / 8
implicitWidth: parent.height / 8
radius: Appearance.rounding.full
}
}
}
}
CustomText {
id: lineText
color: DynamicColors.palette.m3onSurface
font.family: ClipHistory.previewIsCode ? Appearance.font.family.mono : Appearance.font.family.sans
height: lineText.paintedHeight + Appearance.padding.extraSmall * 2
text: lineRow.modelData.text.trim()
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
}
}
}
Image {
id: imagePreview
anchors.fill: parent
anchors.margins: Appearance.padding.large
anchors.centerIn: parent
asynchronous: true
cache: false
fillMode: Image.PreserveAspectFit
@@ -106,6 +192,7 @@ Item {
smooth: true
source: ClipHistory.previewImageSource
visible: ClipHistory.previewIsImage && ClipHistory.previewImageSource !== ""
width: Math.min(sourceSize.width, Config.clipboard.sizes.previewWidth)
}
}
@@ -133,111 +220,103 @@ Item {
flickable: view
minimumSize: 0.1
}
delegate: RowLayout {
delegate: CustomRect {
id: clipItem
readonly property bool isImage: ClipHistory.entryIsImage(modelData)
required property string modelData
height: root.itemHeight
spacing: Appearance.spacing.small
width: view.width
implicitHeight: root.itemHeight
implicitWidth: view.width
radius: textLayer.pressed ? (Appearance.rounding.small / 2) : Appearance.rounding.small
CustomClippingRect {
id: textRect
Layout.fillHeight: true
Layout.fillWidth: true
radius: textLayer.pressed ? (Appearance.rounding.small / 2) : Appearance.rounding.small
Behavior on Layout.preferredWidth {
Anim {
type: Anim.FastEffects
}
}
Behavior on radius {
Anim {
type: Anim.FastEffects
}
Behavior on radius {
Anim {
type: Anim.FastEffects
}
}
Item {
id: textWrapper
RowLayout {
anchors.fill: parent
spacing: Appearance.spacing.small
anchors.fill: parent
layer.enabled: true
CustomClippingRect {
id: textRect
layer.effect: OpacityMask {
maskSource: fadeMask
}
Layout.fillHeight: true
Layout.fillWidth: true
MaterialIcon {
id: icon
Item {
id: textWrapper
anchors.left: parent.left
anchors.margins: Appearance.padding.normal
anchors.verticalCenter: parent.verticalCenter
font.pointSize: Appearance.font.size.large
text: clipItem.isImage ? "image" : "text_fields"
}
anchors.fill: parent
layer.enabled: true
CustomText {
id: text
anchors.left: icon.right
anchors.margins: Appearance.spacing.normal
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
text: clipItem.isImage ? qsTr("Image") : ClipHistory.displayText(clipItem.modelData)
}
}
CustomRect {
id: fadeMask
anchors.fill: parent
layer.enabled: true
visible: false
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
color: Qt.rgba(1, 1, 1, 1.0)
position: 0.85
layer.effect: OpacityMask {
maskSource: fadeMask
}
GradientStop {
color: Qt.rgba(1, 1, 1, 0)
position: 1.0
MaterialIcon {
id: icon
anchors.left: parent.left
anchors.margins: Appearance.padding.normal
anchors.verticalCenter: parent.verticalCenter
font.pointSize: Appearance.font.size.large
text: clipItem.isImage ? "image" : "text_fields"
}
CustomText {
id: text
anchors.left: icon.right
anchors.margins: Appearance.spacing.normal
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
text: clipItem.isImage ? qsTr("Image") : ClipHistory.displayText(clipItem.modelData)
}
}
CustomRect {
id: fadeMask
anchors.fill: parent
layer.enabled: true
visible: false
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
color: Qt.rgba(1, 1, 1, 1.0)
position: 0.85
}
GradientStop {
color: Qt.rgba(1, 1, 1, 0)
position: 1.0
}
}
}
}
StateLayer {
id: textLayer
IconButton {
Layout.fillHeight: true
Layout.margins: Appearance.padding.normal
Layout.preferredWidth: height
icon: "delete"
inactiveColor: Qt.alpha(DynamicColors.palette.m3error, 0.8)
inactiveOnColor: DynamicColors.palette.m3onError
isToggle: false
onClicked: ClipHistory.copy(clipItem.modelData)
onClicked: ClipHistory.deleteEntry(clipItem.modelData)
}
}
IconButton {
Layout.bottomMargin: Appearance.padding.normal
Layout.fillHeight: true
Layout.preferredWidth: height
Layout.topMargin: Appearance.padding.normal
icon: "content_copy"
isToggle: false
}
StateLayer {
id: textLayer
IconButton {
Layout.fillHeight: true
Layout.margins: Appearance.padding.normal
Layout.preferredWidth: height
icon: "delete"
inactiveColor: Qt.alpha(DynamicColors.palette.m3error, 0.8)
inactiveOnColor: DynamicColors.palette.m3onError
isToggle: false
onClicked: ClipHistory.copy(clipItem.modelData)
}
}
highlight: CustomRect {
@@ -266,6 +345,7 @@ Item {
ClipHistory.currentEntry = currentItem.modelData;
ClipHistory.refreshPreview();
}
onVisibleChanged: currentIndex = 0
CustomClippingWrapperRect {
anchors.fill: parent
+1 -2
View File
@@ -11,7 +11,7 @@ Item {
property int contentHeight
property real offsetScale: shouldBeActive ? 0 : 1
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.clipboard
readonly property bool shouldBeActive: visibilities.clipboard && Config.clipboard.enabled
required property PersistentProperties visibilities
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
@@ -32,7 +32,6 @@ Item {
active: root.shouldBeActive || root.visible
anchors.centerIn: parent
asynchronous: true
sourceComponent: Content {
screen: root.screen
+2 -2
View File
@@ -10,9 +10,9 @@ import qs.Components
Item {
id: root
required property PersistentProperties dashState
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
required property PersistentProperties state
required property PersistentProperties visibilities
implicitHeight: nonAnimHeight
@@ -59,7 +59,7 @@ Item {
index: 0
sourceComponent: Dash {
state: root.state
dashState: root.dashState
visibilities: root.visibilities
}
}
+3 -3
View File
@@ -10,9 +10,9 @@ import qs.Modules.Dashboard.Dash
GridLayout {
id: root
required property PersistentProperties dashState
readonly property bool dashboardVisible: visibilities.dashboard
property int radius: Appearance.rounding.smallest
required property PersistentProperties state
required property PersistentProperties visibilities
columnSpacing: Appearance.spacing.smaller
@@ -60,7 +60,7 @@ GridLayout {
User {
id: user
state: root.state
dashState: root.dashState
}
}
@@ -86,7 +86,7 @@ GridLayout {
Calendar {
id: calendar
state: root.state
dashState: root.dashState
}
}
+290 -146
View File
@@ -1,25 +1,50 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Helpers
import qs.Config
import qs.Modules
CustomMouseArea {
id: root
readonly property int currMonth: state.currentDate.getMonth()
readonly property int currYear: state.currentDate.getFullYear()
required property var state
property int activeGrid: 1
required property PersistentProperties dashState
property bool initialized: false
property int month1
property int month2
readonly property int realCurrMonth: dashState.currentDate.getMonth()
readonly property int realCurrYear: dashState.currentDate.getFullYear()
property int year1
property int year2
function handleDateChange() {
const currM = activeGrid === 1 ? month1 : month2;
const currY = activeGrid === 1 ? year1 : year2;
if (realCurrMonth !== currM || realCurrYear !== currY) {
monthChangeAnim.direction = (realCurrYear > currY || (realCurrYear === currY && realCurrMonth > currM)) ? -1 : 1;
if (activeGrid === 1) {
month2 = realCurrMonth;
year2 = realCurrYear;
activeGrid = 2;
} else {
month1 = realCurrMonth;
year1 = realCurrYear;
activeGrid = 1;
}
if (initialized)
monthChangeAnim.restart();
}
}
function onWheel(event: WheelEvent): void {
if (event.angleDelta.y > 0)
root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1);
root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1);
else if (event.angleDelta.y < 0)
root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1);
root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1);
}
acceptedButtons: Qt.MiddleButton
@@ -27,102 +52,160 @@ CustomMouseArea {
anchors.right: parent.right
implicitHeight: inner.implicitHeight + inner.anchors.margins * 2
onClicked: root.state.currentDate = new Date()
Component.onCompleted: {
month1 = realCurrMonth;
year1 = realCurrYear;
month2 = realCurrMonth;
year2 = realCurrYear;
initialized = true;
}
onClicked: root.dashState.currentDate = new Date()
onRealCurrMonthChanged: handleDateChange()
onRealCurrYearChanged: handleDateChange()
SequentialAnimation {
id: monthChangeAnim
property int direction: 0
ScriptAction {
script: {
if (activeGrid === 1) {
titleTranslate1.x = -monthChangeAnim.direction * titleClip.width;
grid1Translate.x = -monthChangeAnim.direction * gridClip.width;
titleTranslate2.x = 0;
grid2Translate.x = 0;
} else {
titleTranslate2.x = -monthChangeAnim.direction * titleClip.width;
grid2Translate.x = -monthChangeAnim.direction * gridClip.width;
titleTranslate1.x = 0;
grid1Translate.x = 0;
}
}
}
ParallelAnimation {
Anim {
property: "x"
target: titleTranslate1
to: root.activeGrid === 1 ? 0 : monthChangeAnim.direction * titleClip.width
type: Anim.DefaultSpatial
}
Anim {
property: "x"
target: grid1Translate
to: root.activeGrid === 1 ? 0 : monthChangeAnim.direction * gridClip.width
type: Anim.DefaultSpatial
}
Anim {
property: "x"
target: titleTranslate2
to: root.activeGrid === 2 ? 0 : monthChangeAnim.direction * titleClip.width
type: Anim.DefaultSpatial
}
Anim {
property: "x"
target: grid2Translate
to: root.activeGrid === 2 ? 0 : monthChangeAnim.direction * gridClip.width
type: Anim.DefaultSpatial
}
}
}
ColumnLayout {
id: inner
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
spacing: Appearance.spacing.extraSmall
RowLayout {
id: monthNavigationRow
Layout.fillWidth: true
spacing: Appearance.spacing.small
spacing: Appearance.spacing.extraSmall
Item {
implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2
implicitWidth: implicitHeight
IconButton {
icon: "chevron_left"
padding: Appearance.padding.small
type: IconButton.Text
StateLayer {
id: prevMonthStateLayer
radius: Appearance.rounding.full
onClicked: {
root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1);
}
}
MaterialIcon {
id: prevMonthText
anchors.centerIn: parent
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
font.weight: 700
text: "chevron_left"
}
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1)
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2
implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2
implicitHeight: monthYearDisplay1.implicitHeight + Appearance.padding.extraSmall * 2
implicitWidth: monthYearDisplay1.implicitWidth + Appearance.padding.large * 2
StateLayer {
anchors.fill: monthYearDisplay
anchors.leftMargin: -Appearance.padding.normal
anchors.margins: -Appearance.padding.small
anchors.rightMargin: -Appearance.padding.normal
color: DynamicColors.palette.m3primary
enabled: {
const now = new Date();
return root.currMonth !== now.getMonth() || root.currYear !== now.getFullYear();
return root.realCurrMonth !== now.getMonth() || root.realCurrYear !== now.getFullYear();
}
radius: Appearance.rounding.full
radius: pressed ? Appearance.rounding.small : Appearance.rounding.large
onClicked: {
root.state.currentDate = new Date();
Behavior on radius {
Anim {
type: Anim.DefaultEffects
}
}
onClicked: root.dashState.currentDate = new Date()
}
CustomText {
id: monthYearDisplay
Item {
id: titleClip
anchors.centerIn: parent
color: DynamicColors.palette.m3primary
font.capitalization: Font.Capitalize
font.pointSize: Appearance.font.size.normal
font.weight: 500
text: grid.title
anchors.fill: parent
clip: true
CustomText {
id: monthYearDisplay1
anchors.centerIn: parent
// qmllint enable missing-property
color: DynamicColors.palette.m3primary
// qmllint disable missing-property
text: grid1.item ? grid1.item.title : ""
visible: root.activeGrid === 1 || monthChangeAnim.running
transform: Translate {
id: titleTranslate1
}
}
CustomText {
id: monthYearDisplay2
anchors.centerIn: parent
// qmllint enable missing-property
color: DynamicColors.palette.m3primary
// qmllint disable missing-property
text: grid2.item ? grid2.item.title : ""
visible: root.activeGrid === 2 || monthChangeAnim.running
transform: Translate {
id: titleTranslate2
}
}
}
}
Item {
implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2
implicitWidth: implicitHeight
IconButton {
icon: "chevron_right"
padding: Appearance.padding.small
type: IconButton.Text
StateLayer {
id: nextMonthStateLayer
radius: Appearance.rounding.full
onClicked: {
root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1);
}
}
MaterialIcon {
id: nextMonthText
anchors.centerIn: parent
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
font.weight: 700
text: "chevron_right"
}
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1)
}
}
@@ -130,109 +213,170 @@ CustomMouseArea {
id: daysRow
Layout.fillWidth: true
locale: grid.locale
locale: Qt.locale()
delegate: CustomText {
required property var model
color: (model.day === 0) ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurfaceVariant
font.weight: 500
color: (model.day === 0) ? DynamicColors.palette.m3tertiary : DynamicColors.palette.m3onSurface
horizontalAlignment: Text.AlignHCenter
text: model.shortName
text: Qt.locale("en_US").standaloneDayName(model.day, Locale.ShortFormat)
}
}
Item {
id: gridClip
Layout.fillWidth: true
implicitHeight: grid.implicitHeight
clip: true
implicitHeight: grid1.implicitHeight
MonthGrid {
id: grid
Component {
id: gridComp
anchors.fill: parent
locale: Qt.locale("en_SE")
month: root.currMonth
spacing: 3
year: root.currYear
Item {
id: internalGridContainer
delegate: Item {
id: dayItem
property int month
property alias title: internalGrid.title
property int year
required property var model
implicitHeight: internalGrid.implicitHeight
implicitHeight: text.implicitHeight + Appearance.padding.small * 2
implicitWidth: implicitHeight
MonthGrid {
id: internalGrid
CustomText {
id: text
anchors.fill: parent
locale: daysRow.locale
month: internalGridContainer.month
spacing: 3
title: `${Qt.locale("en_US").standaloneMonthName(month, Locale.LongFormat)} ${year}`
year: internalGridContainer.year
anchors.centerIn: parent
color: {
const dayOfWeek = dayItem.model.date.getUTCDay();
if (dayOfWeek === 6)
return DynamicColors.palette.m3secondary;
delegate: Item {
id: dayItem
return DynamicColors.palette.m3onSurfaceVariant;
required property var model
implicitHeight: text.implicitHeight + Appearance.padding.normal * 2
implicitWidth: implicitHeight
CustomText {
id: text
anchors.centerIn: parent
color: {
const dayOfWeek = dayItem.model.date.getDay();
if (dayOfWeek === 0)
return DynamicColors.palette.m3tertiary;
return DynamicColors.palette.m3onSurfaceVariant;
}
horizontalAlignment: Text.AlignHCenter
opacity: dayItem.model.today || dayItem.model.month === internalGrid.month ? 1 : 0.4
text: internalGrid.locale.toString(dayItem.model.day)
}
}
}
CustomRect {
id: todayIndicator
property Item today
readonly property Item todayItem: internalGrid.contentItem.children.find(c => c.model.today) ?? null
clip: true
color: DynamicColors.palette.m3primary
implicitHeight: width
implicitWidth: today ? (Math.max(today.implicitWidth, today.implicitHeight) - Appearance.padding.extraSmall) : 0
opacity: todayItem ? 1 : 0
radius: Appearance.rounding.full
scale: todayItem ? 1 : 0.7
x: today ? today.x + (today.width - implicitWidth) / 2 : 0
y: today ? today.y + Appearance.padding.extraSmall / 2 : 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on scale {
Anim {
type: Anim.FastSpatial
}
}
Behavior on x {
Anim {
}
}
Behavior on y {
Anim {
}
}
onTodayItemChanged: {
if (todayItem)
today = todayItem;
}
Coloriser {
colorizationColor: DynamicColors.palette.m3onPrimary
implicitHeight: internalGrid.height
implicitWidth: internalGrid.width
source: internalGrid
sourceColor: DynamicColors.palette.m3onSurface
x: -todayIndicator.x
y: -todayIndicator.y
}
font.pointSize: Appearance.font.size.normal
font.weight: 500
horizontalAlignment: Text.AlignHCenter
opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4
text: grid.locale.toString(dayItem.model.day)
}
}
}
CustomRect {
id: todayIndicator
Loader {
id: grid1
property Item today
readonly property Item todayItem: grid.contentItem.children.find(c => c.model.today) ?? null
anchors.fill: parent
sourceComponent: gridComp
visible: root.activeGrid === 1 || monthChangeAnim.running
clip: true
color: DynamicColors.palette.m3primary
implicitHeight: today?.implicitHeight ?? 0
implicitWidth: today?.implicitWidth ?? 0
opacity: todayItem ? 1 : 0
radius: Appearance.rounding.full
scale: todayItem ? 1 : 0.7
x: today ? today.x + (today.width - implicitWidth) / 2 : 0
y: today?.y ?? 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on x {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on y {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
transform: Translate {
id: grid1Translate
}
onTodayItemChanged: {
if (todayItem)
today = todayItem;
Binding {
property: "month"
target: grid1.item
value: root.month1
}
Coloriser {
colorizationColor: DynamicColors.palette.m3onPrimary
implicitHeight: grid.height
implicitWidth: grid.width
source: grid
sourceColor: DynamicColors.palette.m3onSurface
x: -todayIndicator.x
y: -todayIndicator.y
Binding {
property: "year"
target: grid1.item
value: root.year1
}
}
Loader {
id: grid2
anchors.fill: parent
sourceComponent: gridComp
visible: root.activeGrid === 2 || monthChangeAnim.running
transform: Translate {
id: grid2Translate
}
Binding {
property: "month"
target: grid2.item
value: root.month2
}
Binding {
property: "year"
target: grid2.item
value: root.year2
}
}
}
+65 -28
View File
@@ -4,6 +4,7 @@ import Quickshell
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import QtQuick.Effects
import ZShell.Services
import qs.Daemons
import qs.Components
@@ -45,49 +46,85 @@ Item {
Shape {
id: visualizer
readonly property real barW: Math.max(0, (width - gap * (bars - 1)) / bars)
readonly property int bars: Config.services.visualizerBars
property color color: DynamicColors.palette.m3primary
readonly property real gap: Appearance.spacing.small
property color color: DynamicColors.palette.m3tertiary
property color fillColor: Qt.alpha(color, 0.25)
anchors.fill: layout
anchors.leftMargin: -(shape.strokeWidth / 2)
layer.enabled: true
asynchronous: true
data: visualizerBars.instances
preferredRendererType: Shape.CurveRenderer
}
Variants {
id: visualizerBars
model: Array.from({
length: Config.services.visualizerBars
}, (_, i) => i)
ShapePath {
id: visualizerBar
readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize
required property int modelData
readonly property real value: Math.max(1e-3, Audio.cava.values[modelData])
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
startX: (visualizer.barW / 2) + modelData * (visualizer.barW + visualizer.gap)
startY: layout.y + layout.height
id: shape
strokeColor: visualizer.color
strokeWidth: visualizer.barW
fillColor: visualizer.fillColor
strokeWidth: 2
Behavior on strokeColor {
CAnim {
PathSvg {
path: shape.curvePath
}
property string curvePath: {
const values = Audio.cava.values;
const n = values.length;
if (n < 2)
return "";
const h = layout.height + shape.strokeWidth / 2;
const w = layout.width + shape.strokeWidth;
function x(i) {
return i * w / (n - 1);
}
}
PathLine {
relativeX: 0
relativeY: -visualizerBar.magnitude
function y(i) {
return h - values[i] * Config.dashboard.sizes.mediaVisualizerSize;
}
let d = `M 0 ${h} `;
d += `L ${x(0)} ${y(0)} `;
for (let i = 0; i < n - 1; ++i) {
const x0 = x(i);
const y0 = y(i);
const x1 = x(i + 1);
const y1 = y(i + 1);
const prev = Math.max(0, i - 1);
const next = Math.min(n - 1, i + 2);
const c1x = x0 + (x1 - x(prev)) / 6;
const c1y = y0 + (y1 - y(prev)) / 6;
const c2x = x1 - (x(next) - x0) / 6;
const c2y = y1 - (y(next) - y0) / 6;
d += `C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x1} ${y1} `;
}
d += `L ${w} ${h} `;
d += `L 0 ${h} Z`;
return d;
}
}
}
CustomRect {
anchors.fill: visualizer
color: Qt.alpha(DynamicColors.palette.m3shadow, 0.2)
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: visualizer
}
}
Shape {
preferredRendererType: Shape.CurveRenderer
+56 -59
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
Ref {
service: SystemUsage
ServiceRef {
service: Storage
}
Resource {
color: DynamicColors.palette.m3primary
icon: "memory"
value: SystemUsage.cpuPerc
ServiceRef {
service: Memory
}
Resource {
color: DynamicColors.palette.m3secondary
icon: "memory_alt"
value: SystemUsage.memPerc
ServiceRef {
service: Cpu
}
Resource {
color: DynamicColors.palette.m3tertiary
icon: "gamepad"
value: SystemUsage.gpuPerc
ServiceRef {
service: Gpu
}
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: Gpu.percentage
}
Resource {
fgColor: DynamicColors.palette.m3primary
icon: "host"
value: Gpu.memoryUsed / Gpu.memoryTotal
}
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
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ import QtQuick
Row {
id: root
required property PersistentProperties state
required property PersistentProperties dashState
padding: 20
spacing: 12
+1 -1
View File
@@ -32,7 +32,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
sourceComponent: Content {
state: root.dashState
dashState: root.dashState
visibilities: root.visibilities
}
}
+102 -46
View File
@@ -1,33 +1,35 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Components
import qs.Config
import qs.Components
Item {
id: root
readonly property var colors: ["#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#a855f7", "#ec4899", "#ffffff", "#000000"]
required property Canvas drawing
readonly property var colors1: ["#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4"]
readonly property var colors2: ["#3b82f6", "#a855f7", "#ec4899", "#ffffff", "#000000"]
required property var drawing
required property var visibilities
required property Wrapper wrapper
function syncFromPenColor() {
if (!drawing)
return;
if (!saturationSlider.pressed)
saturationSlider.value = drawing.penColor.hsvSaturation;
saturationSlider.value = drawing.drawingState.penColor.hsvSaturation;
if (!brightnessSlider.pressed)
brightnessSlider.value = drawing.penColor.hsvValue;
brightnessSlider.value = drawing.drawingState.penColor.hsvValue;
}
function updatePenColorFromHsv() {
if (!drawing)
return;
drawing.penColor = Qt.hsva(huePicker.currentHue, saturationSlider.value, brightnessSlider.value, drawing.penColor.a);
drawing.drawingState.penColor = Qt.hsva(huePicker.currentHue, saturationSlider.value, brightnessSlider.value, drawing.drawingState.penColor.a);
}
implicitHeight: column.height + Appearance.padding.larger * 2
@@ -40,7 +42,7 @@ Item {
root.syncFromPenColor();
}
target: root.drawing
target: root.drawing.drawingState
}
Column {
@@ -58,13 +60,14 @@ Item {
GradientSlider {
id: saturationSlider
anchors.left: parent.left
anchors.right: parent.right
brightness: brightnessSlider.value
channel: "saturation"
from: 0
hue: huePicker.currentHue
icon: "\ue40a"
implicitHeight: 30
implicitWidth: palette.width
orientation: Qt.Horizontal
to: 1
@@ -74,12 +77,13 @@ Item {
GradientSlider {
id: brightnessSlider
anchors.left: parent.left
anchors.right: parent.right
channel: "brightness"
from: 0
hue: huePicker.currentHue
icon: "\ue1ac"
implicitHeight: 30
implicitWidth: palette.width
orientation: Qt.Horizontal
saturation: saturationSlider.value
to: 1
@@ -87,65 +91,117 @@ Item {
onMoved: root.updatePenColorFromHsv()
}
GridLayout {
id: palette
ButtonRow {
id: row1
anchors.left: parent.left
anchors.right: parent.right
columns: 5
rowSpacing: 8
rows: 2
spacing: Appearance.spacing.normal
Repeater {
model: root.colors
model: root.colors1
delegate: Item {
id: colorCircle
delegate: ColorButton {
row: row1
}
}
}
required property color modelData
readonly property bool selected: Qt.colorEqual(root.drawing.penColor, modelData)
ButtonRow {
id: row2
Layout.fillWidth: true
height: 28
anchors.left: parent.left
anchors.right: parent.right
spacing: Appearance.spacing.normal
CustomRect {
anchors.centerIn: parent
border.color: Qt.rgba(0, 0, 0, 0.25)
border.width: Qt.colorEqual(modelData, "#ffffff") ? 1 : 0
color: colorCircle.modelData
height: 20
radius: width / 2
width: 20
}
Repeater {
model: root.colors2
CustomRect {
anchors.centerIn: parent
border.color: selected ? "#ffffff" : Qt.rgba(1, 1, 1, 0.28)
border.width: selected ? 3 : 1
color: "transparent"
height: parent.height
radius: width / 2
width: parent.height
StateLayer {
onClicked: root.drawing.penColor = colorCircle.modelData
}
}
delegate: ColorButton {
row: row2
}
}
}
FilledSlider {
anchors.left: parent.left
anchors.right: parent.right
from: 1
icon: "border_color"
implicitHeight: 30
implicitWidth: palette.width
multiplier: 1
orientation: Qt.Horizontal
to: 45
value: root.drawing.penWidth
value: root.drawing.drawingState.penWidth
onMoved: root.drawing.penWidth = value
onMoved: root.drawing.drawingState.penWidth = value
}
ButtonRow {
anchors.left: parent.left
anchors.right: parent.right
spacing: Appearance.spacing.small
IconTextButton {
fillWidth: true
font.pointSize: Appearance.font.size.normal
icon: "close"
inactiveColor: DynamicColors.palette.m3error
inactiveOnColor: DynamicColors.palette.m3onError
isRound: true
shapeMorph: true
text: "Exit"
onClicked: root.visibilities.isDrawing = false
}
IconTextButton {
fillWidth: true
font.pointSize: Appearance.font.size.normal
icon: "ink_eraser"
inactiveColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: DynamicColors.palette.m3onSurfaceVariant
isRound: true
shapeMorph: true
text: "Clear"
onClicked: root.drawing.content.clear()
}
}
}
IconButton {
anchors.margins: Appearance.padding.normal
anchors.right: parent.right
anchors.top: parent.top
checked: root.wrapper.pinned
icon: "keep"
isToggle: true
shapeMorph: true
onClicked: {
root.wrapper.togglePinned();
}
}
component ColorButton: IconButton {
id: colorButton
readonly property real buttonSize: (row.width - row.spacing * 4) / 5
required property color modelData
required property ButtonRow row
fillWidth: false
font.pointSize: Appearance.font.size.normal
icon: ""
implicitHeight: buttonSize
implicitWidth: buttonSize
inactiveColor: modelData
inactiveOnColor: DynamicColors.on(modelData)
isRound: true
shapeMorph: true
shapeMorphExpansion: pressed ? 12 : 0
onClicked: root.drawing.drawingState.penColor = modelData
}
}
+27 -3
View File
@@ -10,13 +10,36 @@ import qs.Daemons
Item {
id: root
required property Canvas drawing
required property var drawing
property bool expanded: true
property real offsetScale: shouldBeActive ? 0 : 1
property bool pinned: false
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.isDrawing
required property var visibilities
function collapse(): void {
if (pinned)
return;
expanded = false;
}
function expand(): void {
expanded = true;
}
function toggleExpanded(): void {
if (pinned)
return;
expanded = !expanded;
}
function togglePinned(): void {
pinned = !pinned;
}
anchors.leftMargin: (-implicitWidth - 5) * offsetScale
implicitHeight: content.implicitHeight
implicitWidth: root.expanded ? content.implicitWidth : icon.implicitWidth
@@ -44,7 +67,7 @@ Item {
Loader {
id: icon
active: root.shouldBeActive || root.visible
active: (root.shouldBeActive || root.visible) && opacity > 0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
asynchronous: true
@@ -63,7 +86,7 @@ Item {
Loader {
id: content
active: root.shouldBeActive || root.visible
active: (root.shouldBeActive || root.visible) && opacity > 0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
asynchronous: true
@@ -76,6 +99,7 @@ Item {
sourceComponent: Content {
drawing: root.drawing
visibilities: root.visibilities
wrapper: root
}
}
}
+6
View File
@@ -24,6 +24,12 @@ CustomRect {
anchors.centerIn: parent
animate: true
color: root.tempEnabled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
fill: root.tempEnabled ? 1 : 0
text: root.tempEnabled ? "lightbulb" : "light_off"
Behavior on fill {
Anim {
}
}
}
}
+15 -11
View File
@@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Layouts
import qs.Modules
import ZShell.Services
import qs.Components
import qs.Helpers
import qs.Config
@@ -16,30 +16,34 @@ GridLayout {
rowSpacing: Appearance.spacing.large
rows: 1
Ref {
service: SystemUsage
ServiceRef {
service: Memory
}
ServiceRef {
service: Cpu
}
Resource {
Layout.bottomMargin: Appearance.padding.large
Layout.topMargin: Appearance.padding.large
colour: DynamicColors.palette.m3primary
fgColor: DynamicColors.palette.m3primary
icon: "memory"
value: SystemUsage.cpuPerc
value: Cpu.percentage
}
Resource {
Layout.bottomMargin: Appearance.padding.large
Layout.topMargin: Appearance.padding.large
colour: DynamicColors.palette.m3secondary
fgColor: DynamicColors.palette.m3secondary
icon: "memory_alt"
value: SystemUsage.memPerc
value: Memory.percentage
}
component Resource: CustomRect {
id: res
required property color colour
required property color fgColor
required property string icon
required property real value
@@ -58,8 +62,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.fgColor
padding: Appearance.padding.large * 3
strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
value: res.value
@@ -69,7 +73,7 @@ GridLayout {
id: icon
anchors.centerIn: parent
color: res.colour
color: res.fgColor
font.pointSize: (circ.arcRadius * 0.7) || 1
font.weight: 600
text: res.icon
+5
View File
@@ -21,6 +21,7 @@ CustomRect {
anchors.centerIn: parent
color: root.visibilities.sidebar ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
fill: root.visibilities.sidebar ? 1 : 0
font.family: "Material Symbols Rounded"
font.pointSize: Appearance.font.size.larger
text: HasNotifications.hasNotifications ? "\uf4fe" : "\ue7f4"
@@ -29,6 +30,10 @@ CustomRect {
CAnim {
}
}
Behavior on fill {
Anim {
}
}
}
StateLayer {
+94 -91
View File
@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import qs.Components
import qs.Config
import qs.Daemons
@@ -20,28 +22,26 @@ Item {
if (count === 0)
return 0;
let height = (count - 1) * 8;
let height = (count - 1) * Appearance.spacing.small;
for (let i = 0; i < count; i++)
height += list.itemAtIndex(i)?.nonAnimHeight ?? 0;
height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0;
if (visibilities && panels) {
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x || visibilities.dashboard)
return 0;
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x || visibilities.dashboard)
return 0;
if (visibilities.osd) {
const h = panels.osd.y - 8 * 2 - padding * 2;
if (height > h)
height = h;
}
if (visibilities.session) {
const h = panels.session.y - 8 * 2 - padding * 2;
if (height > h)
height = h;
}
if (visibilities.osd) {
const h = panels.osd.y - Appearance.spacing.small * 2 - padding * 2;
if (height > h)
height = h;
}
return Math.min((QsWindow.window?.screen?.height ?? 0) - 1 * 2, height + padding * 2);
if (visibilities.session) {
const h = panels.session.y - Appearance.spacing.small * 2 - padding * 2;
if (height > h)
height = h;
}
return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - 1 * 2, height + padding * 2);
}
implicitWidth: Config.notifs.sizes.width + padding * 2
@@ -60,83 +60,11 @@ Item {
id: list
anchors.fill: parent
cacheBuffer: QsWindow.window?.screen.height ?? 0
cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0
orientation: Qt.Vertical
spacing: 0
delegate: Item {
id: wrapper
property int idx
required property int index
required property NotifServer.Notif modelData
readonly property alias nonAnimHeight: notif.nonAnimHeight
implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.small)
implicitWidth: notif.implicitWidth
ListView.onRemove: removeAnim.start()
onIndexChanged: {
if (index !== -1)
idx = index;
}
SequentialAnimation {
id: removeAnim
PropertyAction {
property: "ListView.delayRemove"
target: wrapper
value: true
}
PropertyAction {
property: "enabled"
target: wrapper
value: false
}
PropertyAction {
property: "implicitHeight"
target: wrapper
value: 0
}
PropertyAction {
property: "z"
target: wrapper
value: 1
}
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
property: "x"
target: notif
to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2
}
PropertyAction {
property: "ListView.delayRemove"
target: wrapper
value: false
}
}
ClippingRectangle {
anchors.top: parent.top
anchors.topMargin: wrapper.idx === 0 ? 0 : 8
color: "transparent"
implicitHeight: notif.implicitHeight
implicitWidth: notif.implicitWidth
radius: Appearance.rounding.smallest / 2
Notification {
id: notif
modelData: wrapper.modelData
}
}
delegate: NotifWrapper {
}
displaced: Transition {
Anim {
@@ -158,4 +86,79 @@ Item {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
component NotifWrapper: Item {
id: wrapper
property int idx
required property int index
required property NotifServer.Notif modelData
readonly property alias nonAnimHeight: notif.nonAnimHeight
implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.small)
implicitWidth: notif.implicitWidth
ListView.onRemove: removeAnim.start()
onIndexChanged: {
if (index !== -1)
idx = index;
}
SequentialAnimation {
id: removeAnim
PropertyAction {
property: "ListView.delayRemove"
target: wrapper
value: true
}
PropertyAction {
property: "enabled"
target: wrapper
value: false
}
PropertyAction {
property: "implicitHeight"
target: wrapper
value: 0
}
PropertyAction {
property: "z"
target: wrapper
value: 1
}
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
property: "x"
target: notif
to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2
}
PropertyAction {
property: "ListView.delayRemove"
target: wrapper
value: false
}
}
ClippingRectangle {
anchors.top: parent.top
anchors.topMargin: wrapper.idx === 0 ? 0 : 8
color: "transparent"
implicitHeight: notif.implicitHeight
implicitWidth: notif.implicitWidth
radius: Appearance.rounding.small
Notification {
id: notif
implicitWidth: root.implicitWidth - root.padding * 2
modelData: wrapper.modelData
}
}
}
}
+163 -116
View File
@@ -1,15 +1,14 @@
pragma ComponentBehavior: Bound
import qs.Components
import qs.Config
import qs.Modules
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Services.Notifications
import ZShell.Components
import qs.Daemons
import qs.Helpers
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import QtQuick
import QtQuick.Layouts
import qs.Config
import qs.Components
CustomRect {
id: root
@@ -18,17 +17,16 @@ CustomRect {
readonly property bool hasAppIcon: modelData.appIcon.length > 0
readonly property bool hasImage: modelData.image.length > 0
required property NotifServer.Notif modelData
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? Appearance.spacing.extraSmall * 2 + appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondaryContainer : DynamicColors.tPalette.m3surfaceContainer
implicitHeight: inner.implicitHeight
implicitWidth: Config.notifs.sizes.width
radius: 6
x: Config.notifs.sizes.width
radius: Appearance.rounding.small
x: implicitWidth
Behavior on x {
Anim {
easing.bezierCurve: MaterialEasing.expressiveEffects
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
}
}
@@ -54,7 +52,7 @@ CustomRect {
return;
const actions = root.modelData.actions;
if (actions?.length === 1)
if (actions.length === 1)
actions[0].invoke();
}
onEntered: root.modelData.timer.stop()
@@ -79,7 +77,7 @@ CustomRect {
if (!containsMouse)
root.modelData.timer.start();
if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold)
if (Math.abs(root.x) < root.implicitWidth * Config.notifs.clearThreshold)
root.x = 0;
else
root.modelData.popup = false;
@@ -89,15 +87,13 @@ CustomRect {
id: inner
anchors.left: parent.left
anchors.margins: 8
anchors.margins: Appearance.padding.normal
anchors.right: parent.right
anchors.top: parent.top
implicitHeight: root.nonAnimHeight
Behavior on implicitHeight {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
@@ -112,7 +108,8 @@ CustomRect {
visible: root.hasImage || root.hasAppIcon
width: Config.notifs.sizes.image
sourceComponent: ClippingRectangle {
sourceComponent: CustomClippingRect {
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) : DynamicColors.palette.m3secondaryContainer
implicitHeight: Config.notifs.sizes.image
implicitWidth: Config.notifs.sizes.image
radius: Appearance.rounding.full
@@ -122,8 +119,11 @@ CustomRect {
asynchronous: true
cache: false
fillMode: Image.PreserveAspectCrop
mipmap: true
source: Qt.resolvedUrl(root.modelData.image)
sourceSize: {
const size = Config.notifs.sizes.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
return Qt.size(size, size);
}
}
}
}
@@ -153,8 +153,9 @@ CustomRect {
height: Math.round(parent.width * 0.6)
width: Math.round(parent.width * 0.6)
sourceComponent: CustomIcon {
sourceComponent: ColoredIcon {
anchors.fill: parent
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer
layer.enabled: root.modelData.appIcon.endsWith("symbolic")
source: Quickshell.iconPath(root.modelData.appIcon)
}
@@ -163,34 +164,67 @@ CustomRect {
Loader {
active: !root.hasAppIcon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -18 * 0.02
anchors.verticalCenterOffset: 18 * 0.02
anchors.verticalCenterOffset: 1
asynchronous: true
sourceComponent: MaterialIcon {
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer
font.pointSize: 18
text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency)
}
}
}
}
Shape {
id: progressIndicator
anchors.centerIn: appIcon
height: appIcon.implicitHeight + progressShape.strokeWidth * 2
preferredRendererType: Shape.CurveRenderer
width: appIcon.implicitWidth + progressShape.strokeWidth * 2
ShapePath {
id: progressShape
capStyle: ShapePath.RoundCap
fillColor: "transparent"
strokeColor: DynamicColors.palette.m3primary
strokeWidth: 2
PathAngleArc {
id: progressArc
centerX: progressIndicator.width / 2
centerY: progressIndicator.height / 2
radiusX: progressIndicator.width / 2 - Appearance.padding.extraSmall / 2
radiusY: progressIndicator.height / 2 - Appearance.padding.extraSmall / 2
startAngle: -90
sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360
Behavior on sweepAngle {
Anim {
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
}
}
}
}
}
CustomText {
id: appName
anchors.left: image.right
anchors.leftMargin: 10
anchors.leftMargin: Appearance.spacing.small
anchors.top: parent.top
animate: true
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 10
maximumLineCount: 1
opacity: root.expanded ? 1 : 0
text: appNameMetrics.elidedText
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
@@ -199,9 +233,8 @@ CustomRect {
id: appNameMetrics
elide: Text.ElideRight
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3
font.family: appName.font.family
font.pointSize: appName.font.pointSize
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3
font: appName.font
text: root.modelData.appName
}
@@ -209,9 +242,10 @@ CustomRect {
id: summary
anchors.left: image.right
anchors.leftMargin: 10
anchors.leftMargin: Appearance.spacing.small
anchors.top: parent.top
animate: true
font.pointSize: Appearance.font.size.small
height: implicitHeight
maximumLineCount: 1
text: summaryMetrics.elidedText
@@ -225,6 +259,9 @@ CustomRect {
when: root.expanded
PropertyChanges {
body.anchors.topMargin: Appearance.spacing.extraSmall
bodyPreview.anchors.topMargin: Appearance.spacing.extraSmall
summary.anchors.topMargin: Appearance.spacing.extraSmall
summary.maximumLineCount: undefined
}
@@ -239,10 +276,11 @@ CustomRect {
target: summary
}
AnchorAnimation {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
easing.type: Easing.BezierSpline
Anim {
property: "topMargin"
}
AnchorAnim {
}
}
}
@@ -251,9 +289,8 @@ CustomRect {
id: summaryMetrics
elide: Text.ElideRight
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3
font.family: summary.font.family
font.pointSize: summary.font.pointSize
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3
font: summary.font
text: root.modelData.summary
}
@@ -261,10 +298,9 @@ CustomRect {
id: timeSep
anchors.left: summary.right
anchors.leftMargin: 7
anchors.leftMargin: Appearance.spacing.small
anchors.top: parent.top
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 10
text: "•"
states: State {
@@ -277,10 +313,7 @@ CustomRect {
}
}
transitions: Transition {
AnchorAnimation {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
easing.type: Easing.BezierSpline
AnchorAnim {
}
}
}
@@ -289,11 +322,11 @@ CustomRect {
id: time
anchors.left: timeSep.right
anchors.leftMargin: 7
anchors.leftMargin: Appearance.spacing.small
anchors.top: parent.top
animate: true
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 10
font.pointSize: Appearance.font.size.small
horizontalAlignment: Text.AlignLeft
text: root.modelData.timeStr
}
@@ -303,25 +336,33 @@ CustomRect {
anchors.right: parent.right
anchors.top: parent.top
implicitHeight: expandIcon.height
implicitWidth: expandIcon.height
anchors.topMargin: -Appearance.padding.extraSmall
implicitHeight: expandIcon.implicitHeight
implicitWidth: expandIcon.implicitHeight
StateLayer {
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
radius: Appearance.rounding.full
onClicked: {
root.expanded = !root.expanded;
}
onClicked: root.expanded = !root.expanded
}
MaterialIcon {
id: expandIcon
anchors.centerIn: parent
animate: true
font.pointSize: 13
text: root.expanded ? "expand_less" : "expand_more"
anchors.verticalCenterOffset: root.expanded ? -1 : 1
rotation: root.expanded ? 180 : 0
text: "expand_more"
Behavior on anchors.verticalCenterOffset {
Anim {
}
}
Behavior on rotation {
Anim {
}
}
}
}
@@ -330,17 +371,18 @@ CustomRect {
anchors.left: summary.left
anchors.right: expandBtn.left
anchors.rightMargin: 7
anchors.rightMargin: Appearance.spacing.small
anchors.top: summary.bottom
animate: true
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 10
font.pointSize: Appearance.font.size.small
opacity: root.expanded ? 0 : 1
text: bodyPreviewMetrics.elidedText
textFormat: Text.MarkdownText
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
@@ -350,8 +392,7 @@ CustomRect {
elide: Text.ElideRight
elideWidth: bodyPreview.width
font.family: bodyPreview.font.family
font.pointSize: bodyPreview.font.pointSize
font: bodyPreview.font
text: root.modelData.body
}
@@ -360,11 +401,10 @@ CustomRect {
anchors.left: summary.left
anchors.right: expandBtn.left
anchors.rightMargin: 7
anchors.rightMargin: Appearance.spacing.small
anchors.top: summary.bottom
animate: true
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 10
height: text ? implicitHeight : 0
opacity: root.expanded ? 1 : 0
text: root.modelData.body
@@ -373,6 +413,7 @@ CustomRect {
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
@@ -385,83 +426,89 @@ CustomRect {
}
}
RowLayout {
ButtonRow {
id: actions
anchors.horizontalCenter: parent.horizontalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.top: body.bottom
anchors.topMargin: 7
anchors.topMargin: Appearance.spacing.small
opacity: root.expanded ? 1 : 0
spacing: 10
spacing: Appearance.spacing.extraSmall
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
Action {
modelData: QtObject {
readonly property string text: qsTr("Close")
IconButton {
enabled: root.expanded
fillWidth: root.modelData.actions.length === 0
icon: "close"
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant
isRound: true
padding: Appearance.padding.extraSmall
shapeMorph: true
function invoke(): void {
root.modelData.close();
}
}
onClicked: root.modelData.close()
}
Repeater {
model: root.modelData.actions
delegate: Component {
Action {
TextButton {
required property var modelData
enabled: root.expanded
fillWidth: true
font: {
const f = Qt.font(font);
f.pointSize = Appearance.font.size.small;
return f;
}
implicitWidth: label.implicitWidth
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant
isRound: true
label.anchors.centerIn: undefined
label.anchors.left: left
label.anchors.margins: Appearance.padding.normal
label.anchors.right: right
label.anchors.verticalCenter: verticalCenter
label.elide: Text.ElideRight
label.horizontalAlignment: Text.AlignHCenter
shapeMorph: true
text: modelData.text
onClicked: modelData.invoke()
}
}
IconButton {
enabled: root.expanded
fillWidth: root.modelData.actions.length === 0
icon: copyTimer.running ? "inventory" : "content_copy"
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant
isRound: true
label.animate: true
padding: Appearance.padding.extraSmall
shapeMorph: true
onClicked: {
Quickshell.clipboardText = root.modelData.body;
copyTimer.restart();
}
Timer {
id: copyTimer
interval: 3000
}
}
}
}
}
component Action: CustomRect {
id: action
required property var modelData
Layout.preferredHeight: actionText.height + 4 * 2
Layout.preferredWidth: actionText.width + 8 * 2
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
implicitHeight: actionText.height + 4 * 2
implicitWidth: actionText.width + 8 * 2
radius: Appearance.rounding.full
StateLayer {
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurface
radius: Appearance.rounding.full
onClicked: {
action.modelData.invoke();
}
}
CustomText {
id: actionText
anchors.centerIn: parent
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 10
text: actionTextMetrics.elidedText
}
TextMetrics {
id: actionTextMetrics
elide: Text.ElideRight
elideWidth: {
const numActions = root.modelData.actions.length + 1;
return (inner.width - actions.spacing * (numActions - 1)) / numActions - 8 * 2;
}
font.family: actionText.font.family
font.pointSize: actionText.font.pointSize
text: action.modelData.text
}
}
}
+2 -2
View File
@@ -13,7 +13,7 @@ Item {
id: layout
anchors.fill: parent
spacing: 8
spacing: Appearance.spacing.small
CustomRect {
Layout.fillHeight: true
@@ -29,7 +29,7 @@ Item {
CustomRect {
Layout.fillWidth: true
Layout.topMargin: 8 - layout.spacing
Layout.topMargin: Appearance.padding.normal - layout.spacing
color: DynamicColors.tPalette.m3outlineVariant
implicitHeight: 1
}
+2 -2
View File
@@ -17,7 +17,7 @@ Item {
required property var visibilities
anchors.fill: parent
anchors.margins: 8
anchors.margins: Appearance.padding.normal
Component.onCompleted: NotifServer.list.forEach(n => n.popup = false)
@@ -156,7 +156,7 @@ Item {
Loader {
active: opacity > 0
anchors.bottom: parent.bottom
anchors.margins: 8
anchors.margins: Appearance.padding.normal
anchors.right: parent.right
opacity: root.notifCount > 0 ? 1 : 0
scale: root.notifCount > 0 ? 1 : 0.5
@@ -2,9 +2,9 @@ import Quickshell.Bluetooth
import Quickshell.Networking as QSNetwork
import QtQuick
import QtQuick.Layouts
import ZShell.Components
import qs.Components
import qs.Config
import qs.Modules
import qs.Helpers
import qs.Daemons
@@ -19,74 +19,69 @@ CustomRect {
implicitHeight: layout.implicitHeight + 18 * 2
radius: Appearance.rounding.smallest
ColumnLayout {
ButtonRow {
id: layout
anchors.fill: parent
anchors.margins: 18
spacing: 10
spacing: Appearance.spacing.extraSmall
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 7
Toggle {
checked: Network.wifiEnabled
icon: Network.wifiEnabled ? "wifi" : "wifi_off"
visible: QSNetwork.Networking.devices.values.some(n => n.type === QSNetwork.DeviceType.Wifi)
Toggle {
checked: Network.wifiEnabled
icon: Network.wifiEnabled ? "wifi" : "wifi_off"
visible: QSNetwork.Networking.devices.values.some(n => n.type === QSNetwork.DeviceType.Wifi)
onClicked: Network.toggleWifi()
}
onClicked: Network.toggleWifi()
Toggle {
id: toggle
checked: !NotifServer.dnd
icon: NotifServer.dnd ? "notifications_off" : "notifications"
onClicked: NotifServer.dnd = !NotifServer.dnd
}
Toggle {
checked: !Audio.sourceMuted
icon: Audio.sourceMuted ? "mic_off" : "mic"
onClicked: {
const audio = Audio.source?.audio;
if (audio)
audio.muted = !audio.muted;
}
}
Toggle {
id: toggle
Toggle {
checked: !Audio.muted
icon: Audio.muted ? "volume_off" : "volume_up"
checked: !NotifServer.dnd
icon: NotifServer.dnd ? "notifications_off" : "notifications"
onClicked: NotifServer.dnd = !NotifServer.dnd
onClicked: {
const audio = Audio.sink?.audio;
if (audio)
audio.muted = !audio.muted;
}
}
Toggle {
checked: !Audio.sourceMuted
icon: Audio.sourceMuted ? "mic_off" : "mic"
Toggle {
checked: Bluetooth.defaultAdapter?.enabled ?? false
icon: Bluetooth.defaultAdapter?.enabled ? "bluetooth" : "bluetooth_disabled"
visible: Bluetooth.defaultAdapter ?? false
onClicked: {
const audio = Audio.source?.audio;
if (audio)
audio.muted = !audio.muted;
}
onClicked: {
const adapter = Bluetooth.defaultAdapter;
if (adapter)
adapter.enabled = !adapter.enabled;
}
}
Toggle {
checked: !Audio.muted
icon: Audio.muted ? "volume_off" : "volume_up"
Toggle {
checked: GameMode.enabled
icon: GameMode.enabled ? "videogame_asset" : "videogame_asset_off"
onClicked: {
const audio = Audio.sink?.audio;
if (audio)
audio.muted = !audio.muted;
}
}
Toggle {
checked: Bluetooth.defaultAdapter?.enabled ?? false
icon: Bluetooth.defaultAdapter?.enabled ? "bluetooth" : "bluetooth_disabled"
visible: Bluetooth.defaultAdapter ?? false
onClicked: {
const adapter = Bluetooth.defaultAdapter;
if (adapter)
adapter.enabled = !adapter.enabled;
}
}
Toggle {
checked: GameMode.enabled
icon: GameMode.enabled ? "videogame_asset" : "videogame_asset_off"
onClicked: GameMode.enabled = !GameMode.enabled
}
onClicked: GameMode.enabled = !GameMode.enabled
}
}
@@ -99,19 +94,9 @@ CustomRect {
}
component Toggle: IconButton {
Layout.fillWidth: true
Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? 18 : internalChecked ? 7 : 0)
inactiveColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
fillWidth: true
isRound: true
isToggle: true
radius: stateLayer.pressed ? 6 / 2 : internalChecked ? 6 : 8
radiusAnim.duration: MaterialEasing.expressiveEffectsTime
radiusAnim.easing.bezierCurve: MaterialEasing.expressiveEffects
Behavior on Layout.preferredWidth {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
shapeMorph: true
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ Item {
required property var visibilities
implicitHeight: content.implicitHeight
implicitWidth: Math.max(sidebarPanel.width * (1 - sidebarPanel.offsetScale), content.implicitWidth)
implicitWidth: content.implicitWidth
visible: height > 0
Content {
+2
View File
@@ -97,7 +97,9 @@ Item {
FilledSlider {
anchors.fill: parent
from: Config.services.minBrightness
icon: `brightness_${(Math.round(value * 6) + 1)}`
to: 1.0
value: root.brightness
onPressedChanged: {
+21 -11
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,11 +30,20 @@ CustomRect {
id: rowLayout
anchors.centerIn: parent
anchors.horizontalCenterOffset: -2
implicitHeight: root.implicitHeight
spacing: Appearance.spacing.smaller
Ref {
service: SystemUsage
ServiceRef {
service: Gpu
}
ServiceRef {
service: Cpu
}
ServiceRef {
service: Memory
}
Resource {
@@ -41,8 +51,8 @@ CustomRect {
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,16 +69,16 @@ 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
percentage: SystemUsage.gpuPerc
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3tertiary
percentage: Gpu.percentage
}
Resource {
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
percentage: SystemUsage.gpuMemUsed
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3primary
percentage: Gpu.memoryUsed / Gpu.memoryTotal
}
}
}
+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) + "%"
}
}
}
+86
View File
@@ -0,0 +1,86 @@
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
font.pointSize: Appearance.font.size.large
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}`;
}
}
}
}
+198
View File
@@ -0,0 +1,198 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Internal
import qs.Modules.Resources
import qs.Helpers
import qs.Components
import qs.Config
CustomRect {
id: root
required property Wrapper wrapper
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 bool initialized: false
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
slideProgress: 1
Behavior on smoothMax {
enabled: sparkline.initialized
Anim {
}
}
Component.onCompleted: {
sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024);
sparkline.smoothMax = Qt.binding(() => sparkline.targetMax);
sparkline.initialized = true;
}
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";
}
}
}
}
}
+128
View File
@@ -0,0 +1,128 @@
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
font.pointSize: Appearance.font.size.large
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
}
}
+74 -743
View File
@@ -1,42 +1,27 @@
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
required property Wrapper wrapper
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
@@ -45,748 +30,94 @@ Item {
spacing: Appearance.spacing.normal
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE")
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
active: Config.dashboard.performance.showCpu
sourceComponent: HeroCard {
accent: DynamicColors.palette.m3primary
icon: "memory"
label: qsTr("CPU")
subLabel: Cpu.name
temperature: Cpu.temperature
usage: Cpu.percentage
ServiceRef {
service: Cpu
}
}
}
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"
WrappedLoader {
id: gpuCard
active: Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None
sourceComponent: HeroCard {
accent: DynamicColors.palette.m3secondary
icon: "desktop_windows"
label: qsTr("GPU")
subLabel: Gpu.name
temperature: Gpu.temperature
usage: Gpu.percentage
ServiceRef {
service: Gpu
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork
visible: storageCard.active || networkCard.active || memoryCard.active
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}`;
WrappedLoader {
id: storageCard
active: Config.dashboard.performance.showStorage
sourceComponent: StorageCard {
}
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
WrappedLoader {
id: memoryCard
active: Config.dashboard.performance.showMemory
sourceComponent: MemoryCard {
}
}
NetworkCard {
Layout.fillWidth: true
Layout.minimumWidth: 200
Layout.preferredHeight: 220
visible: Config.dashboard.performance.showNetwork
WrappedLoader {
id: networkCard
active: Config.dashboard.performance.showNetwork
sourceComponent: NetworkCard {
wrapper: root.wrapper
}
}
}
}
BatteryTank {
Layout.preferredHeight: mainColumn.implicitHeight
Layout.preferredWidth: 120
visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery
WrappedLoader {
Layout.fillWidth: false
active: Battery.isLaptop && Config.dashboard.performance.showBattery
sourceComponent: BatteryTank {
}
}
}
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";
}
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")
}
}
Item {
Layout.fillHeight: true
}
// Bottom Info Section
ColumnLayout {
Layout.fillWidth: true
spacing: -4
CustomText {
Layout.alignment: Qt.AlignRight
color: batteryTank.accentColor
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: `${Math.round(batteryTank.percentage * 100)}%`
}
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");
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`;
}
}
}
}
}
component CardHeader: RowLayout {
property color accentColor: DynamicColors.palette.m3primary
property string icon
property string title
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
}
}
+1 -2
View File
@@ -34,8 +34,7 @@ Item {
anchors.centerIn: parent
sourceComponent: Content {
padding: Appearance.padding.normal
visibilities: root.visibilities
wrapper: root
}
}
}
+1 -1
View File
@@ -162,7 +162,7 @@ Item {
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.normal
Flickable {
CustomFlickable {
id: clayout
anchors.fill: parent
+13 -1
View File
@@ -92,6 +92,18 @@ SettingsPage {
Separator {
}
SettingSpinBox {
max: 1.0
min: 0
name: "Minimum brightness"
object: Config.services
setting: "minBrightness"
step: 0.01
}
Separator {
}
SettingSpinBox {
max: 5
min: 0
@@ -115,7 +127,7 @@ SettingsPage {
SettingSpinBox {
min: 1
name: "Visualizer bars"
name: "Visualizer resolution"
object: Config.services
setting: "visualizerBars"
step: 1
+69
View File
@@ -46,6 +46,75 @@ SettingsPage {
}
}
SettingsSection {
sectionId: "Clipboard"
SettingsHeader {
name: "Clipboard"
}
SettingSwitch {
name: "Enable clipboard history viewer"
object: Config.clipboard
setting: "enabled"
}
Separator {
}
SettingSpinBox {
max: 20
min: 1
name: "Max entries visible"
object: Config.clipboard
setting: "maxEntriesShown"
}
Separator {
}
SettingSpinBox {
max: 80
min: 30
name: "Entry height"
object: Config.clipboard.sizes
setting: "itemHeight"
}
Separator {
}
SettingSpinBox {
max: 700
min: 300
name: "Entry width"
object: Config.clipboard.sizes
setting: "width"
}
Separator {
}
SettingSpinBox {
max: 1800
min: 50
name: "Minimum preview width"
object: Config.clipboard.sizes
setting: "minPreviewWidth"
}
Separator {
}
SettingSpinBox {
max: 1800
min: 50
name: "Maximum preview width"
object: Config.clipboard.sizes
setting: "previewWidth"
}
}
SettingsSection {
sectionId: "Toasts"
+1 -2
View File
@@ -181,7 +181,7 @@ Item {
}
}
CustomRect {
CustomClippingRect {
id: categoryContent
anchors.bottom: parent.bottom
@@ -197,7 +197,6 @@ Item {
id: stack
anchors.fill: parent
anchors.margins: Appearance.padding.extraSmall
initialItem: general
popEnter: Transition {
+6 -7
View File
@@ -23,18 +23,15 @@ Item {
return false;
}
CustomClippingWrapperRect {
anchors.fill: parent
child: flickable
radius: Appearance.rounding.normal - Appearance.padding.extraSmall
}
CustomFlickable {
id: flickable
anchors.fill: parent
// for future:
// anchors.leftMargin: Appearance.padding.extraLarge
// anchors.rightMargin: Appearance.padding.extraLarge
clip: true
contentHeight: clayout.implicitHeight
contentHeight: clayout.implicitHeight + clayout.anchors.margins * 2
CustomScrollBar.vertical: CustomScrollBar {
flickable: flickable
@@ -64,7 +61,9 @@ Item {
id: clayout
anchors.left: parent.left
anchors.margins: Appearance.padding.extraSmall
anchors.right: parent.right
anchors.top: parent.top
spacing: Appearance.spacing.small
// move: Transition {
@@ -14,7 +14,7 @@ CustomRect {
anchors.right: parent.right
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: layout.height + contentPadding * 2
radius: Appearance.rounding.normal - Appearance.padding.smaller
radius: Appearance.rounding.normal - Appearance.padding.extraSmall
Behavior on implicitHeight {
Anim {
@@ -94,7 +94,7 @@ Item {
const finalRect = Qt.rect(cropXPercent, cropYPercent, cropWidthPercent, cropHeightPercent);
// We just pass the percentages directly to the backend
Wallpapers.setCrop(delegate.modelData.name, finalRect, finalRect, cropRect.zoom);
Wallpapers.setCrop(delegate.modelData.name, finalRect, cropRect.zoom);
}
function zoomClipRect(zoom: real): void {
@@ -165,7 +165,7 @@ Item {
anchors.top: parent.top
asynchronous: true
fillMode: Image.PreserveAspectFit
// retainWhileLoading: true
retainWhileLoading: true
source: Wallpapers.current
sourceSize.height: parent.height
sourceSize.width: parent.width
@@ -200,7 +200,7 @@ Item {
Loader {
id: cropRectLoader
active: scaledImg.paintedWidth > 0 && scaledImg.status == Image.Ready
active: scaledImg.paintedWidth > 0
sourceComponent: Component {
CustomRect {
+77 -61
View File
@@ -11,82 +11,98 @@ import ZShell.Internal
Item {
id: root
property bool completed
property real cropHeight: displayData.height ?? 1.0
property real cropWidth: displayData.width ?? 1.0
property real cropX: displayData.x ?? 0.0
property real cropY: displayData.y ?? 0.0
property WallpaperImage current
readonly property var displayData: Wallpapers.getCrop(screen.name)
required property ShellScreen screen
property size screenResolution: Qt.size(screen.width * screenScale, screen.height * screenScale)
property real screenScale: Hyprland.monitorFor(screen).scale
property string source: Wallpapers.current
function refreshData(): void {
Hyprland.refreshMonitors();
let scale = Hyprland.monitorFor(root.screen).scale;
if (scale <= 0)
scale = 1.0; // Fallback to avoid zeroes on initialization
if (root.screen.width > 0 && root.screen.height > 0) {
img.screenResolution = Qt.size(root.screen.width * scale, root.screen.height * scale);
}
const displayData = Wallpapers.getCrop(root.screen.name);
if (displayData) {
img.cropX = displayData.x !== undefined ? displayData.x : 0.0;
img.cropY = displayData.y !== undefined ? displayData.y : 0.0;
img.cropWidth = (displayData.width !== undefined && displayData.width > 0) ? displayData.width : 1.0;
img.cropHeight = (displayData.height !== undefined && displayData.height > 0) ? displayData.height : 1.0;
}
}
anchors.fill: parent
Component.onCompleted: root.refreshData()
Component.onCompleted: {
Hyprland.refreshMonitors();
Connections {
function onHeightChanged() {
root.refreshData();
}
function onWidthChanged() {
root.refreshData();
}
target: root.screen
if (source)
Qt.callLater(() => {
current = imgComp.createObject(this, {
source
});
completed = true;
});
}
onSourceChanged: {
if (!source)
current = null;
else
current = imgComp.createObject(this, {
source: source
});
}
WallpaperImage {
id: img
Component {
id: imgComp
anchors.fill: parent
source: root.source
WallpaperImage {
id: img
Behavior on cropHeight {
Anim {
}
}
Behavior on cropWidth {
Anim {
}
}
Behavior on cropX {
Anim {
}
}
Behavior on cropY {
Anim {
}
}
Behavior on zoom {
Anim {
}
}
anchors.fill: parent
cropHeight: root.cropHeight
cropWidth: root.cropWidth
cropX: root.cropX
cropY: root.cropY
opacity: 0
screenResolution: root.screenResolution
source: root.source
Connections {
function onAdapterUpdated(): void {
root.refreshData();
Behavior on cropHeight {
Anim {
id: heightAnim
}
}
Behavior on cropWidth {
Anim {
id: widthAnim
}
}
Behavior on cropX {
Anim {
id: xAnim
}
}
Behavior on cropY {
Anim {
id: yAnim
}
}
Anim on opacity {
id: anim
from: 0
running: false
to: 1
type: Anim.SlowEffects
}
function onLoaded(): void {
root.refreshData();
onStatusChanged: {
if (status === Image.Ready) {
anim.start();
}
}
target: Wallpapers.monitorCrops
Timer {
id: destroyTimer
interval: anim.duration * 2
running: root.current !== img && root.current?.status === Image.Ready
onTriggered: Qt.callLater(() => img.destroy())
}
}
}
}
+12 -1
View File
@@ -1,14 +1,25 @@
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus)
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus CanvasPainter)
find_package(PkgConfig REQUIRED)
find_library(SENSORS_LIBRARY NAMES sensors REQUIRED)
find_path(SENSORS_INCLUDE_DIR NAMES sensors/sensors.h REQUIRED)
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0)
if(NOT Cava_FOUND)
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
endif()
if(NOT TARGET Sensors::Sensors)
add_library(Sensors::Sensors UNKNOWN IMPORTED)
set_target_properties(Sensors::Sensors PROPERTIES
IMPORTED_LOCATION "${SENSORS_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${SENSORS_INCLUDE_DIR}"
)
endif()
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
qt_standard_project_setup(REQUIRES 6.9)
+1
View File
@@ -3,6 +3,7 @@ qml_module(ZShell-components
SOURCES
lazylistview.hpp lazylistview.cpp
wavyline.hpp wavyline.cpp
buttonrow.hpp buttonrow.cpp
LIBRARIES
Qt::Quick
)
+132
View File
@@ -0,0 +1,132 @@
#include "buttonrow.hpp"
namespace ZShell::components {
ButtonRow::ButtonRow(QQuickItem* parent)
: QQuickItem(parent)
, m_dirty(false)
, m_spacing(0.0) {
setFlag(QQuickItem::ItemHasContents, true);
QObject::connect(this, &ButtonRow::widthChanged, this, &ButtonRow::invalidate);
}
qreal ButtonRow::spacing() const {
return m_spacing;
}
void ButtonRow::setSpacing(qreal spacing) {
if (qFuzzyCompare(m_spacing + 1.0, spacing + 1.0))
return;
m_spacing = spacing;
emit spacingChanged();
invalidate();
}
void ButtonRow::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& data) {
if (change == QQuickItem::ItemChildAddedChange) {
auto* const child = data.item;
QObject::connect(child, &QQuickItem::implicitWidthChanged, this, &ButtonRow::invalidate);
QObject::connect(child, &QQuickItem::implicitHeightChanged, this, &ButtonRow::invalidate);
QObject::connect(child, &QQuickItem::visibleChanged, this, &ButtonRow::invalidate);
const auto* childMeta = child->metaObject();
const auto morphSignalIdx = childMeta->indexOfSignal("shapeMorphExpansionChanged()");
if (morphSignalIdx != -1)
QObject::connect(child, childMeta->method(morphSignalIdx), this,
metaObject()->method(metaObject()->indexOfSlot("invalidate()")));
invalidate();
} else if (change == QQuickItem::ItemChildRemovedChange) {
QObject::disconnect(data.item, nullptr, this, nullptr);
invalidate();
}
QQuickItem::itemChange(change, data);
}
void ButtonRow::updatePolish() {
if (m_dirty) {
relayout();
m_dirty = false;
}
}
void ButtonRow::invalidate() {
m_dirty = true;
polish();
}
void ButtonRow::relayout() {
const auto allChildren = childItems();
QList<QQuickItem*> validChildren;
for (auto* const child : allChildren) {
if (child->isVisible() && !child->inherits("QQuickRepeater"))
validChildren.append(child);
}
const auto nChildren = validChildren.size();
const auto totalSpacing = static_cast<qreal>(nChildren - 1) * m_spacing;
qreal reservedWidth = 0;
qreal unreservedWidth = 0;
int fillWidthCount = 0;
qreal maxHeight = 0;
for (auto* const child : validChildren) {
maxHeight = qMax(maxHeight, child->implicitHeight());
const auto prop = child->property("fillWidth");
if (!prop.isValid())
continue;
if (prop.toBool()) {
fillWidthCount++;
unreservedWidth += child->implicitWidth();
} else {
reservedWidth += child->implicitWidth();
}
}
if (fillWidthCount == 0)
fillWidthCount = 1; // Avoid divide by 0
const auto widthPerItem = (width() - totalSpacing - reservedWidth) / static_cast<qreal>(fillWidthCount);
QList<qreal> baseWidths;
baseWidths.reserve(nChildren);
for (auto* const child : validChildren)
baseWidths.append(child->property("fillWidth").toBool() ? widthPerItem : child->implicitWidth());
qreal accX = 0;
for (int i = 0; i < nChildren; ++i) {
auto* const child = validChildren[i];
// clang-format off
auto prevExtraWidth = i > 0 ? getMorphExpansion(validChildren[i - 1]) : 0.0;
auto nextExtraWidth = i < nChildren - 1 ? getMorphExpansion(validChildren[i + 1]) : 0.0;
// clang-format on
// Items at edges push by full amount, items in middle push by half
if (i > 1)
prevExtraWidth /= 2;
if (i < nChildren - 2)
nextExtraWidth /= 2;
child->setWidth(baseWidths[i] + getMorphExpansion(child) - prevExtraWidth - nextExtraWidth);
child->setHeight(maxHeight);
child->setX(accX);
child->setY(0);
accX += child->width() + m_spacing;
}
setImplicitWidth(reservedWidth + unreservedWidth + totalSpacing);
setImplicitHeight(maxHeight);
}
qreal ButtonRow::getMorphExpansion(const QQuickItem* item) {
return item->property("shapeMorphExpansion").toReal();
}
} // namespace ZShell::components
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <qquickitem.h>
namespace ZShell::components {
class ButtonRow : public QQuickItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
public:
explicit ButtonRow(QQuickItem* parent = nullptr);
[[nodiscard]] qreal spacing() const;
void setSpacing(qreal spacing);
signals:
void spacingChanged();
protected:
void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& data) override;
void updatePolish() override;
private slots:
void invalidate();
private:
void relayout();
static qreal getMorphExpansion(const QQuickItem* item);
bool m_dirty;
qreal m_spacing;
};
} // namespace ZShell::components
+15 -10
View File
@@ -1,20 +1,25 @@
qml_module(ZShell-internal
URI ZShell.Internal
SOURCES
hyprextras.hpp hyprextras.cpp
hyprdevices.hpp hyprdevices.cpp
cachingimagemanager.hpp cachingimagemanager.cpp
URI ZShell.Internal
SOURCES
hyprextras.hpp hyprextras.cpp
hyprdevices.hpp hyprdevices.cpp
cachingimagemanager.hpp cachingimagemanager.cpp
circularindicatormanager.hpp circularindicatormanager.cpp
circularbuffer.hpp circularbuffer.cpp
sparklineitem.hpp sparklineitem.cpp
arcgauge.hpp arcgauge.cpp
wallpaperimage.hpp wallpaperimage.cpp
lidwatcher.hpp lidwatcher.cpp
LIBRARIES
Qt::Gui
Qt::Quick
Qt::Concurrent
Qt::Core
visualizerbars.hpp visualizerbars.cpp
linearindicatormanager.hpp linearindicatormanager.cpp
strokecanvasitem.hpp strokecanvasitem.cpp
strokecanvasrenderer.hpp strokecanvasrenderer.cpp
LIBRARIES
Qt::Gui
Qt::Quick
Qt::Concurrent
Qt::Core
Qt::Network
Qt::DBus
Qt::CanvasPainter
)
@@ -0,0 +1,118 @@
#include "linearindicatormanager.hpp"
#include <qpoint.h>
namespace {
constexpr int TOTAL_DURATION_IN_MS = 1800;
constexpr std::array DURATION_TO_MOVE_SEGMENT_ENDS = { 533, 567, 850, 750 };
constexpr std::array DELAY_TO_MOVE_SEGMENT_ENDS = { 1267, 1000, 333, 0 };
QEasingCurve curve(const QPointF& c1, const QPointF& c2) {
QEasingCurve curve(QEasingCurve::BezierSpline);
curve.addCubicBezierSegment(c1, c2, { 1.0, 1.0 });
return curve;
}
qreal getFractionInRange(qreal playtime, int start, int duration) {
const auto fraction = static_cast<qreal>(playtime - start) / duration;
return std::clamp(fraction, 0.0, 1.0);
}
} // namespace
namespace ZShell::controls {
LinearIndicatorSegment::LinearIndicatorSegment(int gap, QObject* parent)
: QObject(parent)
, m_startFraction(0)
, m_endFraction(0)
, m_gapSize(gap) {
}
qreal LinearIndicatorSegment::startFraction() const {
return m_startFraction;
}
qreal LinearIndicatorSegment::endFraction() const {
return m_endFraction;
}
int LinearIndicatorSegment::gapSize() const {
return m_gapSize;
}
LinearIndicatorManager::LinearIndicatorManager(QObject* parent)
: QObject(parent)
, m_interpolators({
curve({ 0.2, 0.0 }, { 0.8, 1.0 }),
curve({ 0.4, 0.0 }, { 1.0, 1.0 }),
curve({ 0.0, 0.0 }, { 0.65, 1.0 }),
curve({ 0.1, 0.0 }, { 0.45, 1.0 }),
})
, m_progress(0)
, m_completeEndProgress(0)
, m_gap(4)
, m_activeIndicators({
new LinearIndicatorSegment(m_gap, this),
new LinearIndicatorSegment(m_gap, this),
}) {
for (auto el : m_activeIndicators)
QObject::connect(this, &LinearIndicatorManager::updated, el, &LinearIndicatorSegment::updated);
}
QList<LinearIndicatorSegment*> LinearIndicatorManager::activeIndicators() const {
return { m_activeIndicators.cbegin(), m_activeIndicators.cend() };
}
qreal LinearIndicatorManager::progress() const {
return m_progress;
}
qreal LinearIndicatorManager::completeEndProgress() const {
return m_completeEndProgress;
}
int LinearIndicatorManager::gap() const {
return m_gap;
}
void LinearIndicatorManager::setGap(int gap) {
m_gap = gap;
for (auto el : m_activeIndicators)
el->m_gapSize = m_gap;
update(m_progress);
}
int LinearIndicatorManager::duration() const {
return TOTAL_DURATION_IN_MS;
}
int LinearIndicatorManager::completeEndDuration() const {
return TOTAL_DURATION_IN_MS;
}
void LinearIndicatorManager::update(qreal progress) {
const auto playtime = progress * TOTAL_DURATION_IN_MS;
for (size_t i = 0; i < SEGMENTS; i++) {
const auto di = i * 2;
auto* const indicator = m_activeIndicators[i];
auto fraction = getFractionInRange(playtime, DELAY_TO_MOVE_SEGMENT_ENDS[di], DURATION_TO_MOVE_SEGMENT_ENDS[di]);
indicator->m_startFraction = std::clamp(m_interpolators[di].valueForProgress(fraction), 0.0, 1.0);
fraction =
getFractionInRange(playtime, DELAY_TO_MOVE_SEGMENT_ENDS[di + 1], DURATION_TO_MOVE_SEGMENT_ENDS[di + 1]);
indicator->m_endFraction = std::clamp(m_interpolators[di + 1].valueForProgress(fraction), 0.0, 1.0);
}
m_progress = progress;
emit updated();
}
void LinearIndicatorManager::updateCompleteEndProgress(qreal progress) {
m_completeEndProgress = progress;
update(m_progress);
}
} // namespace ZShell::controls
@@ -0,0 +1,86 @@
#pragma once
#include <qcolor.h>
#include <qeasingcurve.h>
#include <qobject.h>
#include <qqmlengine.h>
#include <qqmlintegration.h>
namespace ZShell::controls {
class LinearIndicatorManager;
class LinearIndicatorSegment : public QObject {
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("LinearIndicatorSegments can only be retrieved from a "
"LinearIndicatorManager.")
Q_PROPERTY(qreal startFraction READ startFraction NOTIFY updated FINAL)
Q_PROPERTY(qreal endFraction READ endFraction NOTIFY updated FINAL)
Q_PROPERTY(int gapSize READ gapSize NOTIFY updated FINAL)
public:
explicit LinearIndicatorSegment(int gap, QObject* parent = nullptr);
qreal startFraction() const;
qreal endFraction() const;
int gapSize() const;
signals:
void updated();
private:
qreal m_startFraction;
qreal m_endFraction;
int m_gapSize;
friend LinearIndicatorManager;
};
class LinearIndicatorManager : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
QList<ZShell::controls::LinearIndicatorSegment*> activeIndicators READ activeIndicators CONSTANT FINAL)
Q_PROPERTY(qreal progress READ progress WRITE update NOTIFY updated FINAL)
Q_PROPERTY(qreal completeEndProgress READ completeEndProgress WRITE updateCompleteEndProgress NOTIFY updated FINAL)
Q_PROPERTY(int gap READ gap WRITE setGap NOTIFY updated FINAL)
Q_PROPERTY(qreal duration READ duration CONSTANT FINAL)
Q_PROPERTY(qreal completeEndDuration READ completeEndDuration CONSTANT FINAL)
public:
explicit LinearIndicatorManager(QObject* parent = nullptr);
QList<LinearIndicatorSegment*> activeIndicators() const;
qreal progress() const;
qreal completeEndProgress() const;
int gap() const;
void setGap(int gap);
int duration() const;
int completeEndDuration() const;
void update(qreal progress);
void updateCompleteEndProgress(qreal progress);
signals:
void updated();
private:
static constexpr int SEGMENTS = 2;
std::array<QEasingCurve, 4> m_interpolators;
qreal m_progress;
qreal m_completeEndProgress;
int m_gap;
std::array<LinearIndicatorSegment*, SEGMENTS> m_activeIndicators;
};
} // namespace ZShell::controls
+36 -10
View File
@@ -1,5 +1,6 @@
#include "sparklineitem.hpp"
#include <qmath.h>
#include <qpainter.h>
#include <qpainterpath.h>
#include <qpen.h>
@@ -33,19 +34,45 @@ void SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QC
const qreal w = width();
const qreal h = height();
const int len = buffer->count();
if (len < 2 || m_maxValue <= 0.0)
return;
const qreal stepX = w / static_cast<qreal>(m_historyLength - 1);
const qreal startX = w - (len - 1) * stepX - stepX * m_slideProgress + stepX;
// Build line path
QPainterPath linePath;
linePath.moveTo(startX, h - (buffer->at(0) / m_maxValue) * h);
for (int i = 1; i < len; ++i) {
const qreal strokePad = qCeil(m_lineWidth / 2);
const qreal curvePad = 3.0;
const qreal topPad = strokePad + curvePad;
const qreal bottomPad = strokePad;
const qreal plotTop = topPad;
const qreal plotBottom = h - bottomPad;
const qreal fillBottom = h;
const qreal plotH = qMax<qreal>(1.0, plotBottom - plotTop);
QVector<QPointF> points;
points.reserve(len);
for (int i = 0; i < len; ++i) {
const qreal x = startX + i * stepX;
const qreal y = h - (buffer->at(i) / m_maxValue) * h;
linePath.lineTo(x, y);
const qreal value = qBound<qreal>(0.0, buffer->at(i), m_maxValue);
const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH;
points.append(QPointF(x, y));
}
QPainterPath linePath;
linePath.moveTo(points[0]);
for (int i = 0; i < points.size() - 1; ++i) {
const QPointF& p0 = points[i];
const QPointF& p1 = points[i + 1];
const qreal ctrlX = (p0.x() + p1.x()) * 0.5;
const QPointF c1(ctrlX, p0.y());
const QPointF c2(ctrlX, p1.y());
linePath.cubicTo(c1, c2, p1);
}
// Stroke the line
QPen pen(color, m_lineWidth);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);
@@ -53,10 +80,9 @@ void SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QC
painter->setBrush(Qt::NoBrush);
painter->drawPath(linePath);
// Fill under the line
QPainterPath fillPath = linePath;
fillPath.lineTo(startX + (len - 1) * stepX, h);
fillPath.lineTo(startX, h);
fillPath.lineTo(startX + (len - 1) * stepX, fillBottom);
fillPath.lineTo(startX, fillBottom);
fillPath.closeSubpath();
QColor fillColor = color;
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <QColor>
#include <QPointF>
#include <QVector>
#include <QCanvasPath>
namespace ZShell::internal {
struct Stroke {
QVector<QPointF> points;
QCanvasPath path;
QColor color;
float width;
int groupId = -1;
bool isSinglePoint = false;
};
};
@@ -0,0 +1,193 @@
#include "strokecanvasitem.hpp"
#include "strokecanvasrenderer.hpp"
#include <qcanvaspainter.h>
#include <qnamespace.h>
#include <qpoint.h>
namespace ZShell::internal {
StrokeCanvasItem::StrokeCanvasItem(QQuickItem *parent) : QCanvasPainterItem(parent) {
setFillColor(Qt::transparent);
setAlphaBlending(true);
}
QCanvasPainterItemRenderer *StrokeCanvasItem::createItemRenderer() const {
return new StrokeCanvasRenderer;
}
static bool shouldAddPoint(
const QVector<QPointF> &points,
const QPointF &p,
qreal minDistance)
{
if (points.isEmpty())
return true;
const QPointF delta = p - points.last();
return QPointF::dotProduct(delta, delta)
>= minDistance * minDistance;
}
static QCanvasPath buildStrokePath(const QVector<QPointF> &points, float width) {
QCanvasPath path;
if (points.size() == 1) {
path.circle(points[0], width * 0.5f);
return path;
}
auto catmullToBezier = [](
const QPointF &p0, const QPointF &p1,
const QPointF &p2, const QPointF &p3,
float tension,
QPointF &cp1, QPointF &cp2)
{
cp1 = p1 + (p2 - p0) * tension / 3.0f;
cp2 = p2 - (p3 - p1) * tension / 3.0f;
};
const float tension = 0.5f;
path.moveTo(points[0]);
for (int i = 0; i < points.size() - 1; ++i) {
const QPointF &p0 = points[qMax(i - 1, 0)];
const QPointF &p1 = points[i];
const QPointF &p2 = points[i + 1];
const QPointF &p3 = points[qMin(i + 2, points.size() - 1)];
QPointF cp1, cp2;
catmullToBezier(p0, p1, p2, p3, tension, cp1, cp2);
path.bezierCurveTo(cp1, cp2, p2);
}
return path;
}
void StrokeCanvasItem::setPenColor(const QColor &color) {
if (m_penColor == color)
return;
m_penColor = color;
update();
emit penColorChanged();
}
void StrokeCanvasItem::setHoverVisible(bool visible) {
if (m_hoverVisible == visible)
return;
m_hoverVisible = visible;
update();
emit hoverVisibleChanged();
}
void StrokeCanvasItem::setHoverPoint(const QPointF &point) {
if (m_hoverPoint == point)
return;
m_hoverPoint = point;
update();
emit hoverPointChanged();
}
void StrokeCanvasItem::showHover(qreal x, qreal y) {
const QPointF newPoint{x, y};
const bool pointChanged = (m_hoverPoint != newPoint);
const bool visibleChanged = !m_hoverVisible;
if (pointChanged || visibleChanged) {
m_hoverVisible = true;
m_hoverPoint = newPoint;
if (pointChanged) emit hoverPointChanged();
if (visibleChanged) emit hoverVisibleChanged();
update();
}
}
void StrokeCanvasItem::hideHover() {
if (!m_hoverVisible)
return;
m_hoverVisible = false;
update();
emit hoverVisibleChanged();
}
void StrokeCanvasItem::setPenWidth(float width) {
if (qFuzzyCompare(m_penWidth, width))
return;
m_penWidth = width;
update();
emit penWidthChanged();
}
void StrokeCanvasItem::beginStroke(qreal x, qreal y) {
m_isDrawing = true;
m_currentStroke.points.clear();
m_currentStroke.points.append({x, y});
m_currentStroke.color = m_penColor;
m_currentStroke.width = m_penWidth;
update();
}
void StrokeCanvasItem::appendPoint(qreal x, qreal y) {
const QPointF incoming{x, y};
if (!shouldAddPoint(m_currentStroke.points, incoming, 2.0))
return;
QPointF smoothed;
if (m_currentStroke.points.isEmpty()) {
smoothed = incoming;
} else {
const QPointF last = m_currentStroke.points.last();
const QPointF delta = incoming - last;
const qreal dist = std::sqrt(QPointF::dotProduct(delta, delta));
constexpr qreal minDist = 6.0;
constexpr qreal maxDist = 20.0;
constexpr qreal minAlpha = 0.1;
constexpr qreal maxAlpha = 0.3;
const qreal t = std::clamp((dist - minDist) / (maxDist - minDist), 0.0, 1.0);
const qreal alpha = minAlpha + t * (maxAlpha - minAlpha);
smoothed = last * (1.0 - alpha) + incoming * alpha;
}
m_currentStroke.points.append(smoothed);
update();
}
void StrokeCanvasItem::endStroke() {
m_isDrawing = false;
if (m_currentStroke.points.isEmpty())
return;
m_currentStroke.isSinglePoint = (m_currentStroke.points.size() == 1);
m_currentStroke.path = buildStrokePath(m_currentStroke.points, m_currentStroke.width);
m_currentStroke.groupId = m_nextGroupId++;
m_currentStroke.points.clear();
m_strokes.append(m_currentStroke);
m_currentStroke = {};
update();
}
void StrokeCanvasItem::clear() {
m_strokes.clear();
m_currentStroke = {};
update();
}
};
@@ -0,0 +1,80 @@
#pragma once
#include <QCanvasPainterItem>
#include <QColor>
#include <QPointF>
#include <QVector>
#include <qcolor.h>
#include <qcontainerfwd.h>
#include "stroke.hpp"
namespace ZShell::internal {
class StrokeCanvasRenderer;
class StrokeCanvasItem : public QCanvasPainterItem {
Q_OBJECT
QML_NAMED_ELEMENT(StrokeCanvas)
Q_PROPERTY(QColor penColor READ penColor WRITE setPenColor NOTIFY penColorChanged)
Q_PROPERTY(bool hoverVisible READ hoverVisible WRITE setHoverVisible NOTIFY hoverVisibleChanged)
Q_PROPERTY(QPointF hoverPoint READ hoverPoint WRITE setHoverPoint NOTIFY hoverPointChanged)
Q_PROPERTY(qreal penWidth READ penWidth WRITE setPenWidth NOTIFY penWidthChanged)
public:
explicit StrokeCanvasItem(QQuickItem *parent = nullptr);
[[nodiscard]] bool hoverVisible() const {
return m_hoverVisible;
}
[[nodiscard]] QPointF hoverPoint() const {
return m_hoverPoint;
}
void setHoverVisible(bool visible);
void setHoverPoint(const QPointF &point);
Q_INVOKABLE void showHover(qreal x, qreal y);
Q_INVOKABLE void hideHover();
[[nodiscard]] QColor penColor() const {
return m_penColor;
}
[[nodiscard]] float penWidth() const {
return m_penWidth;
}
void setPenColor(const QColor &color);
void setPenWidth(float width);
Q_INVOKABLE void clear();
Q_INVOKABLE void beginStroke(qreal x, qreal y);
Q_INVOKABLE void appendPoint(qreal x, qreal y);
Q_INVOKABLE void endStroke();
signals:
void penColorChanged();
void penWidthChanged();
void hoverVisibleChanged();
void hoverPointChanged();
protected:
[[nodiscard]] QCanvasPainterItemRenderer *createItemRenderer() const override;
private:
friend class StrokeCanvasRenderer;
bool m_hoverVisible = false;
QPointF m_hoverPoint;
QColor m_penColor = Qt::white;
float m_penWidth = 4.f;
bool m_isDrawing = false;
int m_nextGroupId = 0;
QVector<Stroke> m_strokes;
Stroke m_currentStroke;
};
};
@@ -0,0 +1,183 @@
#include "strokecanvasrenderer.hpp"
#include "strokecanvasitem.hpp"
#include <qcolor.h>
namespace ZShell::internal {
static void drawStroke(
QCanvasPainter *painter,
const QVector<QPointF> &points,
const QColor &color,
float width) {
if (points.isEmpty())
return;
painter->setStrokeStyle(color);
painter->setFillStyle(color);
painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
painter->setLineJoin(QCanvasPainter::LineJoin::Round);
if (points.size() == 1) {
painter->beginPath();
painter->circle(points.front(), width * 0.5f);
painter->fill();
return;
}
auto catmullToBezier = [](
const QPointF &p0, const QPointF &p1,
const QPointF &p2, const QPointF &p3,
float tension,
QPointF &cp1, QPointF &cp2)
{
cp1 = p1 + (p2 - p0) * tension / 3.0f;
cp2 = p2 - (p3 - p1) * tension / 3.0f;
};
const float tension = 0.5f; // increase toward 1.0 for tighter curves
painter->beginPath();
painter->moveTo(points[0]);
for (int i = 0; i < points.size() - 1; ++i) {
const QPointF &p0 = points[qMax(i - 1, 0)];
const QPointF &p1 = points[i];
const QPointF &p2 = points[i + 1];
const QPointF &p3 = points[qMin(i + 2, points.size() - 1)];
QPointF cp1, cp2;
catmullToBezier(p0, p1, p2, p3, tension, cp1, cp2);
painter->bezierCurveTo(cp1, cp2, p2);
}
painter->stroke();
}
static void drawHoverCursor(
QCanvasPainter *painter,
const QPointF &point,
float penWidth,
QColor penColor,
bool isDrawing)
{
const float radius = penWidth * 0.5f;
if (isDrawing) {
painter->setFillStyle(penColor);
painter->beginPath();
painter->circle(point, radius);
painter->fill();
}
const float lineWidth = 1.5f;
const float crosshairSize = 6.0f;
const bool useDashes = penWidth > 10.0f;
auto drawOutline = [&](const QColor &color, float width) {
painter->setStrokeStyle(color);
painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
if (useDashes) {
const int dashCount = 12;
const float fullAngle = 2.0f * M_PI;
const float dashAngle = fullAngle / dashCount * 0.5f;
const float gapAngle = fullAngle / dashCount * 0.5f;
float angle = 0.0f;
for (int i = 0; i < dashCount; ++i) {
painter->beginPath();
painter->arc(point, radius, angle, angle + dashAngle,
QCanvasPainter::PathWinding::ClockWise,
QCanvasPainter::PathConnection::NotConnected);
painter->stroke();
angle += dashAngle + gapAngle;
}
} else {
painter->beginPath();
painter->circle(point, radius);
painter->stroke();
}
};
auto drawCrosshair = [&](const QColor &color, float width) {
painter->setStrokeStyle(color);
painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
const float inner = radius + 3.0f;
const float outer = radius + 3.0f + crosshairSize;
painter->beginPath();
painter->moveTo(point + QPointF(0, -outer));
painter->lineTo(point + QPointF(0, -inner));
painter->moveTo(point + QPointF(0, outer));
painter->lineTo(point + QPointF(0, inner));
painter->moveTo(point + QPointF(-outer, 0));
painter->lineTo(point + QPointF(-inner, 0));
painter->moveTo(point + QPointF( outer, 0));
painter->lineTo(point + QPointF( inner, 0));
painter->stroke();
};
drawOutline(QColor(0, 0, 0, 160), lineWidth + 1.0f);
drawCrosshair(QColor(0, 0, 0, 160), lineWidth + 1.0f);
drawOutline(QColor(255, 255, 255, 220), lineWidth);
drawCrosshair(QColor(255, 255, 255, 220), lineWidth);
}
void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem *item) {
auto *canvas = static_cast<StrokeCanvasItem *>(item);
m_penColor = canvas->m_penColor;
m_penWidth = canvas->m_penWidth;
while (m_strokes.size() < canvas->m_strokes.size())
m_strokes.append(canvas->m_strokes[m_strokes.size()]);
if (canvas->m_strokes.isEmpty() && !m_strokes.isEmpty()) {
for (const auto &stroke : m_strokes)
if (stroke.groupId >= 0)
m_pendingGroupRemovals.append(stroke.groupId);
m_strokes.clear();
}
m_currentStroke = canvas->m_currentStroke;
m_hoverVisible = canvas->m_hoverVisible;
m_hoverPoint = canvas->m_hoverPoint;
m_isDrawing = canvas->m_isDrawing;
}
void StrokeCanvasRenderer::paint(QCanvasPainter *painter) {
for (int id : m_pendingGroupRemovals)
painter->removePathGroup(id);
m_pendingGroupRemovals.clear();
painter->clearRect(0, 0, width(), height());
for (const auto &stroke : m_strokes) {
painter->setStrokeStyle(stroke.color);
painter->setFillStyle(stroke.color);
painter->setLineWidth(stroke.width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
painter->setLineJoin(QCanvasPainter::LineJoin::Round);
if (stroke.isSinglePoint) {
painter->fill(stroke.path, stroke.groupId);
} else {
painter->stroke(stroke.path, stroke.groupId);
}
}
drawStroke(painter, m_currentStroke.points, m_currentStroke.color, m_currentStroke.width);
if (m_hoverVisible)
drawHoverCursor(painter, m_hoverPoint, m_penWidth, m_penColor, m_isDrawing);
}
};
@@ -0,0 +1,28 @@
#pragma once
#include <QCanvasPainterItemRenderer>
#include <qcontainerfwd.h>
#include "stroke.hpp"
namespace ZShell::internal {
class StrokeCanvasRenderer final : public QCanvasPainterItemRenderer {
public:
void synchronizeData(QCanvasPainterItem *item) override;
void paint(QCanvasPainter *painter) override;
private:
QColor m_penColor;
float m_penWidth = 4.f;
bool m_hoverVisible = false;
QPointF m_hoverPoint;
bool m_isDrawing = false;
QVector<Stroke> m_strokes;
Stroke m_currentStroke;
QVector<int> m_pendingGroupRemovals;
};
};
+198
View File
@@ -0,0 +1,198 @@
#include "visualizerbars.hpp"
#include <algorithm>
#include <cmath>
#include <qbrush.h>
#include <qpainter.h>
#include <qpainterpath.h>
#include <qpen.h>
namespace ZShell::internal {
VisualizerBars::VisualizerBars(QQuickItem* parent)
: QQuickPaintedItem(parent) {
setAntialiasing(true);
}
void VisualizerBars::advance(qreal dt) {
if (m_displayValues.isEmpty() || m_settled)
return;
// dt is in seconds (from FrameAnimation.frameTime), convert to ms
const qreal dtMs = dt * 1000.0;
const qreal tau = m_animationDuration / 3.0;
const qreal alpha = 1.0 - std::exp(-dtMs / tau);
bool allSettled = true;
for (qsizetype i = 0; i < m_displayValues.size(); ++i) {
const double diff = m_targetValues[i] - m_displayValues[i];
if (std::abs(diff) > 0.001) {
m_displayValues[i] += diff * alpha;
allSettled = false;
} else {
m_displayValues[i] = m_targetValues[i];
}
}
update();
if (allSettled && !m_settled) {
m_settled = true;
emit settledChanged();
}
}
void VisualizerBars::paint(QPainter* painter) {
if (m_displayValues.isEmpty())
return;
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setPen(Qt::NoPen);
const qreal h = height();
const qreal maxBarHeight = h * 0.4;
QLinearGradient gradient(0, h - maxBarHeight, 0, h);
gradient.setColorAt(0, m_primaryColor);
gradient.setColorAt(1, m_secondaryColor);
painter->setBrush(gradient);
drawSide(painter, false);
drawSide(painter, true);
}
void VisualizerBars::drawSide(QPainter* painter, bool rightSide) {
const qreal w = width();
const qreal h = height();
const auto count = m_displayValues.size();
if (count == 0)
return;
const qreal sideWidth = w * 0.4;
const qreal slotWidth = sideWidth / static_cast<qreal>(count);
const qreal barWidth = slotWidth - m_spacing;
if (barWidth <= 0)
return;
const qreal sideOffset = rightSide ? w * 0.6 : 0;
const qreal maxBarHeight = h * 0.4;
for (qsizetype i = 0; i < count; ++i) {
const qsizetype valueIndex = rightSide ? i : (count - i - 1);
const qreal value = std::clamp(m_displayValues[valueIndex], 0.0, 1.0);
const qreal barHeight = value * maxBarHeight;
if (barHeight <= 0)
continue;
const qreal x = static_cast<qreal>(i) * slotWidth + sideOffset;
const qreal y = h - barHeight;
const qreal r = std::min({ m_rounding, barWidth / 2.0, barHeight });
QPainterPath path;
path.moveTo(x, h);
path.lineTo(x, y + r);
if (r > 0) {
path.arcTo(x, y, r * 2, r * 2, 180, -90);
path.lineTo(x + barWidth - r, y);
path.arcTo(x + barWidth - r * 2, y, r * 2, r * 2, 90, -90);
} else {
path.lineTo(x, y);
path.lineTo(x + barWidth, y);
}
path.lineTo(x + barWidth, h);
path.closeSubpath();
painter->drawPath(path);
}
}
QVector<double> VisualizerBars::values() const {
return m_targetValues;
}
void VisualizerBars::setValues(const QVector<double>& values) {
m_targetValues = values;
if (m_displayValues.size() != values.size()) {
m_displayValues.resize(values.size(), 0.0);
}
if (m_settled) {
m_settled = false;
emit settledChanged();
}
emit valuesChanged();
}
bool VisualizerBars::settled() const {
return m_settled;
}
QColor VisualizerBars::primaryColor() const {
return m_primaryColor;
}
void VisualizerBars::setPrimaryColor(const QColor& color) {
if (m_primaryColor == color)
return;
m_primaryColor = color;
emit primaryColorChanged();
update();
}
QColor VisualizerBars::secondaryColor() const {
return m_secondaryColor;
}
void VisualizerBars::setSecondaryColor(const QColor& color) {
if (m_secondaryColor == color)
return;
m_secondaryColor = color;
emit secondaryColorChanged();
update();
}
qreal VisualizerBars::rounding() const {
return m_rounding;
}
void VisualizerBars::setRounding(qreal rounding) {
if (qFuzzyCompare(m_rounding, rounding))
return;
m_rounding = rounding;
emit roundingChanged();
update();
}
qreal VisualizerBars::spacing() const {
return m_spacing;
}
void VisualizerBars::setSpacing(qreal spacing) {
if (qFuzzyCompare(m_spacing, spacing))
return;
m_spacing = spacing;
emit spacingChanged();
update();
}
int VisualizerBars::animationDuration() const {
return m_animationDuration;
}
void VisualizerBars::setAnimationDuration(int duration) {
if (m_animationDuration == duration)
return;
m_animationDuration = duration;
emit animationDurationChanged();
}
} // namespace ZShell::internal
@@ -0,0 +1,72 @@
#pragma once
#include <qcolor.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickpainteditem.h>
#include <qvector.h>
namespace ZShell::internal {
class VisualizerBars : public QQuickPaintedItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QVector<double> values READ values WRITE setValues NOTIFY valuesChanged)
Q_PROPERTY(QColor primaryColor READ primaryColor WRITE setPrimaryColor NOTIFY primaryColorChanged)
Q_PROPERTY(QColor secondaryColor READ secondaryColor WRITE setSecondaryColor NOTIFY secondaryColorChanged)
Q_PROPERTY(qreal rounding READ rounding WRITE setRounding NOTIFY roundingChanged)
Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
Q_PROPERTY(int animationDuration READ animationDuration WRITE setAnimationDuration NOTIFY animationDurationChanged)
Q_PROPERTY(bool settled READ settled NOTIFY settledChanged)
public:
explicit VisualizerBars(QQuickItem* parent = nullptr);
void paint(QPainter* painter) override;
Q_INVOKABLE void advance(qreal dt);
[[nodiscard]] QVector<double> values() const;
void setValues(const QVector<double>& values);
[[nodiscard]] QColor primaryColor() const;
void setPrimaryColor(const QColor& color);
[[nodiscard]] QColor secondaryColor() const;
void setSecondaryColor(const QColor& color);
[[nodiscard]] qreal rounding() const;
void setRounding(qreal rounding);
[[nodiscard]] qreal spacing() const;
void setSpacing(qreal spacing);
[[nodiscard]] int animationDuration() const;
void setAnimationDuration(int duration);
[[nodiscard]] bool settled() const;
signals:
void valuesChanged();
void primaryColorChanged();
void secondaryColorChanged();
void roundingChanged();
void spacingChanged();
void animationDurationChanged();
void settledChanged();
private:
void drawSide(QPainter* painter, bool rightSide);
QVector<double> m_targetValues;
QVector<double> m_displayValues;
QColor m_primaryColor;
QColor m_secondaryColor;
qreal m_rounding = 0.0;
qreal m_spacing = 0.0;
int m_animationDuration = 200;
bool m_settled = true;
};
} // namespace ZShell::internal
+107 -75
View File
@@ -7,6 +7,7 @@
#include <QtConcurrent>
#include <QSGImageNode>
#include <QQuickWindow>
#include <set>
namespace ZShell::internal {
@@ -21,10 +22,20 @@ WallpaperImage::~WallpaperImage() {
if (m_texture) delete m_texture;
}
void WallpaperImage::setStatus(const Status s) {
if (m_status == s)
return;
m_status = s;
emit statusChanged();
}
void WallpaperImage::setSource(const QUrl &source) {
if (m_source == source) return;
m_source = source;
emit sourceChanged();
setStatus(Loading);
loadImage();
}
@@ -57,28 +68,27 @@ void WallpaperImage::setCropY(qreal y) {
}
void WallpaperImage::setCropWidth(qreal w) {
if (w <= 0.0) w = 1.0;
if (qFuzzyCompare(m_cropWidth, w)) return;
m_cropWidth = w;
emit cropWidthChanged();
update();
if (w <= 0.0) w = 1.0;
if (qFuzzyCompare(m_cropWidth, w)) return;
m_cropWidth = w;
emit cropWidthChanged();
update();
}
void WallpaperImage::setCropHeight(qreal h) {
if (h <= 0.0) h = 1.0;
if (qFuzzyCompare(m_cropHeight, h)) return;
m_cropHeight = h;
emit cropHeightChanged();
update();
if (h <= 0.0) h = 1.0;
if (qFuzzyCompare(m_cropHeight, h)) return;
m_cropHeight = h;
emit cropHeightChanged();
update();
}
QString WallpaperImage::getCacheFilePath() const {
if (m_source.isEmpty() || m_screenResolution.isEmpty()) return QString();
QString cachePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/zshell/imagecache";
QString cachePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/zshell/imagecache";
QDir().mkpath(cachePath);
// Hash the source URL + resolution
QString id = m_source.toString() + "_" + QString::number(m_screenResolution.width()) + "x" + QString::number(m_screenResolution.height());
QByteArray hash = QCryptographicHash::hash(id.toUtf8(), QCryptographicHash::Md5).toHex();
@@ -86,52 +96,53 @@ QString WallpaperImage::getCacheFilePath() const {
}
void WallpaperImage::loadImage() {
if (m_source.isEmpty()) return;
if (m_source.isEmpty()) {
setStatus(Null);
return;
}
QString cacheFile = getCacheFilePath();
QString sourceFile = m_source.isLocalFile() ? m_source.toLocalFile() : m_source.toString();
// Qt resource path correction if passed as a standard URL string
if (sourceFile.startsWith("qrc:/")) {
sourceFile = sourceFile.mid(3); // Converts "qrc:/" to ":/"
}
QSize targetRes = m_screenResolution;
QString cacheFile = getCacheFilePath();
QString sourceFile = m_source.isLocalFile() ? m_source.toLocalFile() : m_source.toString();
// Run off the main thread to avoid blocking the UI
QFuture<QImage> future = QtConcurrent::run([sourceFile, cacheFile, targetRes]() -> QImage {
if (!targetRes.isEmpty() && !cacheFile.isEmpty() && QFileInfo::exists(cacheFile)) {
QImage cached(cacheFile);
if (!cached.isNull()) return cached;
}
if (sourceFile.startsWith("qrc:/")) {
sourceFile = sourceFile.mid(3);
}
QImage original(sourceFile);
if (original.isNull()) return QImage();
QSize targetRes = m_screenResolution;
if (targetRes.isEmpty()) {
// Screen resolution not set yet by QML, return the unscaled original for now to prevent a black screen
return original;
}
QFuture<QImage> future = QtConcurrent::run([sourceFile, cacheFile, targetRes]() -> QImage {
if (!targetRes.isEmpty() && !cacheFile.isEmpty() && QFileInfo::exists(cacheFile)) {
QImage cached(cacheFile);
if (!cached.isNull()) return cached;
}
// Check if original is strictly larger than screen resolution
if (original.width() > targetRes.width() || original.height() > targetRes.height()) {
QImage scaled = original.scaled(targetRes, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
if (!cacheFile.isEmpty()) scaled.save(cacheFile, "PNG");
return scaled;
}
QImage original(sourceFile);
if (original.isNull()) return QImage();
// Otherwise just cache and return the original
if (!cacheFile.isEmpty()) original.save(cacheFile, "PNG");
return original;
});
if (targetRes.isEmpty()) {
return original;
}
m_imageWatcher.setFuture(future);
if (original.width() > targetRes.width() || original.height() > targetRes.height()) {
QImage scaled = original.scaled(targetRes, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
if (!cacheFile.isEmpty()) scaled.save(cacheFile, "PNG");
return scaled;
}
if (!cacheFile.isEmpty()) original.save(cacheFile, "PNG");
return original;
});
m_imageWatcher.setFuture(future);
}
void WallpaperImage::handleImageLoaded() {
m_image = m_imageWatcher.result();
setStatus(m_image.isNull() ? Error : Ready);
m_textureDirty = true;
update(); // Request redraw
update();
}
QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) {
@@ -148,7 +159,7 @@ QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *
if (m_textureDirty) {
if (m_texture) delete m_texture;
m_texture = window()->createTextureFromImage(m_image, QQuickWindow::TextureHasAlphaChannel);
m_texture = window()->createTextureFromImage(m_image, QQuickWindow::TextureHasAlphaChannel);
m_textureDirty = false;
}
@@ -157,40 +168,61 @@ QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *
node->setRect(boundingRect());
node->setFiltering(QSGTexture::Linear);
qreal cW = m_cropWidth / m_zoom;
qreal cH = m_cropHeight / m_zoom;
qreal cW = m_cropWidth / m_zoom;
qreal cH = m_cropHeight / m_zoom;
QRectF reqRect(
m_cropX * m_texture->textureSize().width(),
m_cropY * m_texture->textureSize().height(),
cW * m_texture->textureSize().width(),
cH * m_texture->textureSize().height()
);
QRectF reqRect(
m_cropX * m_texture->textureSize().width(),
m_cropY * m_texture->textureSize().height(),
cW * m_texture->textureSize().width(),
cH * m_texture->textureSize().height()
);
QRectF bounds = boundingRect();
if (bounds.isEmpty() || reqRect.isEmpty()) return node;
QRectF bounds = boundingRect();
if (bounds.isEmpty() || reqRect.isEmpty()) return node;
qreal targetRatio = bounds.width() / bounds.height();
qreal reqRatio = reqRect.width() / reqRect.height();
qreal targetRatio = bounds.width() / bounds.height();
qreal reqRatio = reqRect.width() / reqRect.height();
QRectF sourceRect = reqRect;
QRectF sourceRect = reqRect;
// Force 'PreserveAspectCrop' behavior on the requested region
if (reqRatio > targetRatio) {
// Requested region is too wide, center-crop the sides
qreal newWidth = reqRect.height() * targetRatio;
qreal xOffset = (reqRect.width() - newWidth) / 2.0;
sourceRect.setX(reqRect.x() + xOffset);
sourceRect.setWidth(newWidth);
} else if (reqRatio < targetRatio) {
// Requested region is too tall, center-crop the top/bottom
qreal newHeight = reqRect.width() / targetRatio;
qreal yOffset = (reqRect.height() - newHeight) / 2.0;
sourceRect.setY(reqRect.y() + yOffset);
sourceRect.setHeight(newHeight);
}
if (reqRatio > targetRatio) {
qreal newWidth = reqRect.height() * targetRatio;
qreal xOffset = (reqRect.width() - newWidth) / 2.0;
sourceRect.setX(reqRect.x() + xOffset);
sourceRect.setWidth(newWidth);
} else if (reqRatio < targetRatio) {
qreal newHeight = reqRect.width() / targetRatio;
qreal yOffset = (reqRect.height() - newHeight) / 2.0;
sourceRect.setY(reqRect.y() + yOffset);
sourceRect.setHeight(newHeight);
}
node->setSourceRect(sourceRect);
QRectF normalizedActual(
sourceRect.x() / m_texture->textureSize().width(),
sourceRect.y() / m_texture->textureSize().height(),
sourceRect.width() / m_texture->textureSize().width(),
sourceRect.height() / m_texture->textureSize().height()
);
bool changed = false;
auto updateIfChanged = [&](qreal &dst, qreal value) {
if (!qFuzzyCompare(dst, value)) {
dst = value;
changed = true;
}
};
updateIfChanged(m_actualCropX, normalizedActual.x());
updateIfChanged(m_actualCropY, normalizedActual.y());
updateIfChanged(m_actualCropWidth, normalizedActual.width());
updateIfChanged(m_actualCropHeight, normalizedActual.height());
if (changed)
emit actualCropChanged();
node->setSourceRect(sourceRect);
}
return node;
@@ -6,6 +6,7 @@
#include <QSGTexture>
#include <QFutureWatcher>
#include <QtQml/qqml.h>
#include <qtmetamacros.h>
namespace ZShell::internal {
@@ -15,6 +16,12 @@ QML_NAMED_ELEMENT(WallpaperImage)
Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
Q_PROPERTY(QSize screenResolution READ screenResolution WRITE setScreenResolution NOTIFY screenResolutionChanged)
Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged)
Q_PROPERTY(Status status READ status NOTIFY statusChanged)
Q_PROPERTY(qreal actualCropX READ actualCropX NOTIFY actualCropChanged)
Q_PROPERTY(qreal actualCropY READ actualCropY NOTIFY actualCropChanged)
Q_PROPERTY(qreal actualCropWidth READ actualCropWidth NOTIFY actualCropChanged)
Q_PROPERTY(qreal actualCropHeight READ actualCropHeight NOTIFY actualCropChanged)
Q_PROPERTY(qreal cropX READ cropX WRITE setCropX NOTIFY cropXChanged)
Q_PROPERTY(qreal cropY READ cropY WRITE setCropY NOTIFY cropYChanged)
@@ -25,6 +32,18 @@ public:
explicit WallpaperImage(QQuickItem *parent = nullptr);
~WallpaperImage() override;
enum Status {
Null,
Ready,
Loading,
Error
};
Q_ENUM(Status)
Status status() const {
return m_status;
}
QUrl source() const {
return m_source;
}
@@ -40,6 +59,22 @@ qreal zoom() const {
}
void setZoom(qreal zoom);
qreal actualCropX() const {
return m_actualCropX;
}
qreal actualCropY() const {
return m_actualCropY;
}
qreal actualCropWidth() const {
return m_actualCropWidth;
}
qreal actualCropHeight() const {
return m_actualCropHeight;
}
qreal cropX() const {
return m_cropX;
}
@@ -67,20 +102,29 @@ signals:
void sourceChanged();
void screenResolutionChanged();
void zoomChanged();
void actualCropChanged();
void cropXChanged();
void cropYChanged();
void cropWidthChanged();
void cropHeightChanged();
void statusChanged();
private:
void loadImage();
void handleImageLoaded();
QString getCacheFilePath() const;
void setStatus(const Status s);
Status m_status = Null;
QUrl m_source;
QSize m_screenResolution;
qreal m_zoom = 1.0;
qreal m_actualCropX = 0.0;
qreal m_actualCropY = 0.0;
qreal m_actualCropWidth = 1.0;
qreal m_actualCropHeight = 1.0;
qreal m_cropX = 0.0;
qreal m_cropY = 0.0;
qreal m_cropWidth = 1.0;
+20 -11
View File
@@ -1,19 +1,28 @@
qml_module(ZShell-services
URI ZShell.Services
SOURCES
service.hpp service.cpp
serviceref.hpp serviceref.cpp
beattracker.hpp beattracker.cpp
audiocollector.hpp audiocollector.cpp
audioprovider.hpp audioprovider.cpp
cavaprovider.hpp cavaprovider.cpp
SOURCES
service.hpp service.cpp
serviceref.hpp serviceref.cpp
beattracker.hpp beattracker.cpp
audiocollector.hpp audiocollector.cpp
audioprovider.hpp audioprovider.cpp
cavaprovider.hpp cavaprovider.cpp
desktopmodel.hpp desktopmodel.cpp
desktopstatemanager.hpp desktopstatemanager.cpp
hyprsunsetmanager.hpp hyprsunsetmanager.cpp
LIBRARIES
tickingservice.hpp tickingservice.cpp
sensorslib.hpp sensorslib.cpp
usagefmt.hpp usagefmt.cpp
cpu.hpp cpu.cpp
gpu.hpp gpu.cpp
memory.hpp memory.cpp
diskinfo.hpp diskinfo.cpp
storage.hpp storage.cpp
LIBRARIES
Qt6::Core
Qt6::Qml
PkgConfig::Pipewire
PkgConfig::Aubio
PkgConfig::Cava
PkgConfig::Pipewire
PkgConfig::Aubio
PkgConfig::Cava
Sensors::Sensors
)
+1 -1
View File
@@ -36,7 +36,7 @@ void CavaProcessor::process() {
QVector<double> values(m_bars);
for(int i = 0; i < m_bars; ++i) {
values[i] = std::clamp(m_out[i], 0.0, 1.0);
values[i] = m_out[i];
}
// Left to right pass
+117
View File
@@ -0,0 +1,117 @@
#include "cpu.hpp"
#include "sensorslib.hpp"
#include <cmath>
#include <qfile.h>
#include <qregularexpression.h>
namespace ZShell::services {
Cpu::Cpu(QObject* parent)
: TickingService(parent) {
readNameOnce();
}
QString Cpu::name() const {
return m_name;
}
qreal Cpu::percentage() const {
return m_percentage;
}
qreal Cpu::temperature() const {
return m_temperature;
}
void Cpu::tick() {
if (!m_nameLoaded) {
readNameOnce();
}
refreshPercentage();
refreshTemperature();
}
void Cpu::readNameOnce() {
QFile f(QStringLiteral("/proc/cpuinfo"));
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
return;
}
const QByteArray data = f.readAll();
f.close();
static const QRegularExpression re(QStringLiteral("model name\\s*:\\s*(.+)"));
const auto match = re.match(QString::fromLatin1(data));
if (!match.hasMatch()) {
return;
}
const QString cleaned = cleanName(match.captured(1));
m_nameLoaded = true;
if (cleaned == m_name) {
return;
}
m_name = cleaned;
Q_EMIT nameChanged();
}
void Cpu::refreshPercentage() {
QFile f(QStringLiteral("/proc/stat"));
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
return;
}
const QByteArray data = f.readAll();
f.close();
static const QRegularExpression re(
QStringLiteral("^cpu\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)"));
const auto match = re.match(QString::fromLatin1(data));
if (!match.hasMatch()) {
return;
}
quint64 total = 0;
quint64 idle = 0;
for (int i = 1; i <= 7; ++i) {
const quint64 v = match.captured(i).toULongLong();
total += v;
if (i == 4 || i == 5) {
idle += v;
}
}
const quint64 totalDiff = total > m_lastTotal ? total - m_lastTotal : 0;
const quint64 idleDiff = idle > m_lastIdle ? idle - m_lastIdle : 0;
const qreal newPerc = totalDiff > 0 ? 1.0 - static_cast<qreal>(idleDiff) / static_cast<qreal>(totalDiff) : 0.0;
m_lastTotal = total;
m_lastIdle = idle;
if (std::abs(newPerc - m_percentage) > 0.0001) {
m_percentage = newPerc;
Q_EMIT percentageChanged();
}
}
void Cpu::refreshTemperature() {
const auto t = sensorslib::cpuPackageTemp();
const qreal newTemp = t.value_or(0.0);
if (std::abs(newTemp - m_temperature) > 0.05) {
m_temperature = newTemp;
Q_EMIT temperatureChanged();
}
}
QString Cpu::cleanName(QString s) {
static const QRegularExpression noise(
QStringLiteral("\\(R\\)|\\(TM\\)|CPU|\\d+(?:th|nd|rd|st) Gen |Core |Processor"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression spaces(QStringLiteral("\\s+"));
s.replace(noise, QString());
s.replace(spaces, QStringLiteral(" "));
return s.trimmed();
}
} // namespace ZShell::services
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include "tickingservice.hpp"
#include <qqmlintegration.h>
namespace ZShell::services {
class Cpu : public TickingService {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged)
Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged)
public:
explicit Cpu(QObject* parent = nullptr);
[[nodiscard]] QString name() const;
[[nodiscard]] qreal percentage() const;
[[nodiscard]] qreal temperature() const;
signals:
void nameChanged();
void percentageChanged();
void temperatureChanged();
protected:
void tick() override;
private:
void readNameOnce();
void refreshPercentage();
void refreshTemperature();
[[nodiscard]] static QString cleanName(QString s);
QString m_name;
qreal m_percentage = 0.0;
qreal m_temperature = 0.0;
quint64 m_lastIdle = 0;
quint64 m_lastTotal = 0;
bool m_nameLoaded = false;
};
} // namespace ZShell::services
+68
View File
@@ -0,0 +1,68 @@
#include "diskinfo.hpp"
namespace ZShell::services {
namespace {
constexpr qreal kKib = 1024.0;
} // namespace
DiskInfo::DiskInfo(QString mount, quint64 usedBytes, quint64 totalBytes, bool hasRoot, QObject* parent)
: QObject(parent)
, m_mount(std::move(mount))
, m_usedBytes(usedBytes)
, m_totalBytes(totalBytes)
, m_hasRoot(hasRoot) {
}
QString DiskInfo::mount() const {
return m_mount;
}
qreal DiskInfo::used() const {
return static_cast<qreal>(m_usedBytes) / kKib;
}
qreal DiskInfo::total() const {
return static_cast<qreal>(m_totalBytes) / kKib;
}
qreal DiskInfo::free() const {
const quint64 freeBytes = m_totalBytes > m_usedBytes ? m_totalBytes - m_usedBytes : 0;
return static_cast<qreal>(freeBytes) / kKib;
}
qreal DiskInfo::perc() const {
return m_totalBytes > 0 ? static_cast<qreal>(m_usedBytes) / static_cast<qreal>(m_totalBytes) : 0.0;
}
bool DiskInfo::hasRoot() const {
return m_hasRoot;
}
void DiskInfo::update(quint64 usedBytes, quint64 totalBytes, bool hasRoot) {
const bool usedDiff = usedBytes != m_usedBytes;
const bool totalDiff = totalBytes != m_totalBytes;
const bool rootDiff = hasRoot != m_hasRoot;
m_usedBytes = usedBytes;
m_totalBytes = totalBytes;
m_hasRoot = hasRoot;
if (usedDiff) {
Q_EMIT usedChanged();
}
if (totalDiff) {
Q_EMIT totalChanged();
}
if (usedDiff || totalDiff) {
Q_EMIT freeChanged();
Q_EMIT percChanged();
}
if (rootDiff) {
Q_EMIT hasRootChanged();
}
}
} // namespace ZShell::services
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
namespace ZShell::services {
class DiskInfo : public QObject {
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("DiskInfo is created by DiskUsage")
Q_PROPERTY(QString mount READ mount CONSTANT)
Q_PROPERTY(qreal used READ used NOTIFY usedChanged)
Q_PROPERTY(qreal total READ total NOTIFY totalChanged)
Q_PROPERTY(qreal free READ free NOTIFY freeChanged)
Q_PROPERTY(qreal perc READ perc NOTIFY percChanged)
Q_PROPERTY(bool hasRoot READ hasRoot NOTIFY hasRootChanged)
public:
DiskInfo(QString mount, quint64 usedBytes, quint64 totalBytes, bool hasRoot, QObject* parent = nullptr);
[[nodiscard]] QString mount() const;
[[nodiscard]] qreal used() const;
[[nodiscard]] qreal total() const;
[[nodiscard]] qreal free() const;
[[nodiscard]] qreal perc() const;
[[nodiscard]] bool hasRoot() const;
void update(quint64 usedBytes, quint64 totalBytes, bool hasRoot);
signals:
void usedChanged();
void totalChanged();
void freeChanged();
void percChanged();
void hasRootChanged();
private:
QString m_mount;
quint64 m_usedBytes;
quint64 m_totalBytes;
bool m_hasRoot;
};
} // namespace ZShell::services
+332
View File
@@ -0,0 +1,332 @@
#include "gpu.hpp"
#include "sensorslib.hpp"
#include <cmath>
#include <qdir.h>
#include <qfile.h>
#include <qregularexpression.h>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QFileSystemWatcher>
#include <QTimer>
namespace ZShell::services {
namespace {
constexpr const char* kTypeDetectScript =
"if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then echo NVIDIA;"
" elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC;"
" else echo NONE; fi";
constexpr const char* kNameDetectScript = "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null"
" || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1"
" || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1";
} // namespace
Gpu::Gpu(QObject* parent)
: TickingService(parent) {
QString configPath = QDir::homePath() + QStringLiteral("/.config/zshell/config.json");
auto reloadConfig = [this, configPath]() {
QFile file(configPath);
if (file.open(QIODevice::ReadOnly)) {
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isNull()) {
QJsonObject services = doc.object().value("services").toObject();
setUserType(parseType(services.value("gpuType").toString()));
}
}
};
reloadConfig();
static QFileSystemWatcher* watcher = new QFileSystemWatcher();
if (!watcher->files().contains(configPath)) {
QObject::connect(watcher, &QFileSystemWatcher::fileChanged, this, [this, configPath]() {
QTimer::singleShot(100, this, [this, configPath]() {
QFile file(configPath);
if (file.exists()) {
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isNull()) {
QJsonObject services = doc.object().value("services").toObject();
setUserType(parseType(services.value("gpuType").toString()));
}
}
});
});
watcher->addPath(configPath);
}
// Detection must run before any ServiceRef appears: callers may gate the ref on
// `type !== Gpu.None`, which would otherwise deadlock the detection.
if (m_userType == Auto) {
detectTypeOnce();
}
detectNameOnce();
}
Gpu::Type Gpu::type() const {
return m_userType == Auto ? m_autoType : m_userType;
}
Gpu::Type Gpu::userType() const {
return m_userType;
}
Gpu::Type Gpu::autoType() const {
return m_autoType;
}
QString Gpu::name() const {
return m_name;
}
qreal Gpu::percentage() const {
return m_percentage;
}
qreal Gpu::temperature() const {
return m_temperature;
}
qreal Gpu::memoryUsed() const {
return m_memoryUsed;
}
qreal Gpu::memoryTotal() const {
return m_memoryTotal;
}
void Gpu::setUserType(Type value) {
if (value == m_userType) {
return;
}
const Type prevDerived = type();
m_userType = value;
Q_EMIT userTypeChanged();
if (type() != prevDerived) {
Q_EMIT typeChanged();
}
}
void Gpu::setAutoType(Type value) {
if (value == m_userType) {
return;
}
const Type prevDerived = type();
m_autoType = value;
Q_EMIT autoTypeChanged();
if (type() != prevDerived) {
Q_EMIT typeChanged();
}
}
void Gpu::setName(QString value) {
if (value == m_name) {
return;
}
m_name = std::move(value);
Q_EMIT nameChanged();
}
void Gpu::setMemoryUsed(qreal value) {
if (m_memoryUsed == value) return;
m_memoryUsed = value;
Q_EMIT memoryUsedChanged();
}
void Gpu::setMemoryTotal(qreal value) {
if (m_memoryTotal == value) return;
m_memoryTotal = value;
Q_EMIT memoryTotalChanged();
}
void Gpu::tick() {
const Type t = type();
if (t == Generic) {
readGenericUsage();
readGpuTemperature();
} else if (t == Nvidia) {
startNvidiaUsage();
} else {
if (std::abs(m_percentage) > 0.0001) {
m_percentage = 0.0;
Q_EMIT percentageChanged();
}
if (std::abs(m_temperature) > 0.05) {
m_temperature = 0.0;
Q_EMIT temperatureChanged();
}
}
}
void Gpu::detectTypeOnce() {
if (m_typeProc) {
return;
}
m_typeProc = new QProcess(this);
QObject::connect(m_typeProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) {
const QByteArray out = m_typeProc->readAllStandardOutput().trimmed();
if (!out.isEmpty()) {
setAutoType(parseType(QString::fromLatin1(out)));
}
m_typeProc->deleteLater();
m_typeProc = nullptr;
});
m_typeProc->start(QStringLiteral("sh"), { QStringLiteral("-c"), QString::fromLatin1(kTypeDetectScript) });
}
void Gpu::detectNameOnce() {
if (m_nameProc) {
return;
}
m_nameProc = new QProcess(this);
QObject::connect(m_nameProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) {
const QString output = QString::fromUtf8(m_nameProc->readAllStandardOutput()).trimmed();
if (!output.isEmpty()) {
const QString lower = output.toLower();
if (lower.contains(QStringLiteral("nvidia")) || lower.contains(QStringLiteral("geforce")) ||
lower.contains(QStringLiteral("rtx")) || lower.contains(QStringLiteral("gtx")) ||
lower.contains(QStringLiteral("rx"))) {
setName(cleanName(output));
} else {
static const QRegularExpression bracketRe(QStringLiteral("\\[([^\\]]+)\\][^\\[]*$"));
const auto bracket = bracketRe.match(output);
if (bracket.hasMatch()) {
setName(cleanName(bracket.captured(1)));
} else {
static const QRegularExpression colonRe(QStringLiteral(":\\s*(.+)"));
const auto colon = colonRe.match(output);
if (colon.hasMatch()) {
setName(cleanName(colon.captured(1)));
}
}
}
}
m_nameProc->deleteLater();
m_nameProc = nullptr;
});
m_nameProc->start(QStringLiteral("sh"), { QStringLiteral("-c"), QString::fromLatin1(kNameDetectScript) });
}
void Gpu::readGenericUsage() {
const QStringList paths =
QDir(QStringLiteral("/sys/class/drm"))
.entryList(QStringList() << QStringLiteral("card*"), QDir::Dirs | QDir::NoDotAndDotDot);
qreal sum = 0.0;
int count = 0;
for (const QString& card : paths) {
QFile f(QStringLiteral("/sys/class/drm/%1/device/gpu_busy_percent").arg(card));
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
continue;
}
bool ok = false;
const qreal v = f.readAll().trimmed().toDouble(&ok);
f.close();
if (ok) {
sum += v;
++count;
}
}
const qreal newPerc = count > 0 ? sum / count / 100.0 : 0.0;
if (std::abs(newPerc - m_percentage) > 0.0001) {
m_percentage = newPerc;
Q_EMIT percentageChanged();
}
}
void Gpu::startNvidiaUsage() {
if (m_nvidiaProc) {
return;
}
m_nvidiaProc = new QProcess(this);
QObject::connect(m_nvidiaProc, &QProcess::readyReadStandardOutput, this, [this]() {
while (m_nvidiaProc->canReadLine()) {
const QByteArray out = m_nvidiaProc->readLine();
const QString output = QString::fromUtf8(out).trimmed();
if (output.isEmpty())
continue;
const QList<QString> parts = output.split(',');
if (parts.size() < 4)
return;
bool ok1 = false;
bool ok2 = false;
bool ok3 = false;
bool ok4 = false;
const qreal usage = parts.at(0).trimmed().toDouble(&ok1) / 100.0;
const qreal temp = parts.at(1).trimmed().toDouble(&ok2);
const qreal memUsed = parts.at(2).trimmed().toDouble(&ok3);
const qreal memTotal = parts.at(3).trimmed().toDouble(&ok4);
if (ok1 && std::abs(usage - m_percentage) > 0.0001) {
m_percentage = usage;
Q_EMIT percentageChanged();
}
if (ok2 && std::abs(temp - m_temperature) > 0.05) {
m_temperature = temp;
Q_EMIT temperatureChanged();
}
if (ok3) {
setMemoryUsed(memUsed);
}
if (ok4) {
setMemoryTotal(memTotal);
}
}
});
QObject::connect(m_nvidiaProc, &QProcess::finished, this, [this]() {
m_nvidiaProc->deleteLater();
m_nvidiaProc = nullptr;
});
QObject::connect(m_nvidiaProc, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
m_nvidiaProc->deleteLater();
m_nvidiaProc = nullptr;
});
m_nvidiaProc->start(QStringLiteral("nvidia-smi"), { QStringLiteral("--query-gpu=utilization.gpu,temperature.gpu,memory.used,memory.total"),
QStringLiteral("--format=csv,noheader,nounits"),
QStringLiteral("-lms"),
QString::number(updateInterval()) });
}
void Gpu::readGpuTemperature() {
const auto t = sensorslib::gpuPciAverageTemp();
const qreal newTemp = t.value_or(0.0);
if (std::abs(newTemp - m_temperature) > 0.05) {
m_temperature = newTemp;
Q_EMIT temperatureChanged();
}
}
Gpu::Type Gpu::parseType(const QString& s) {
const QString u = s.trimmed().toUpper();
if (u.isEmpty()) {
return Auto;
}
if (u == QStringLiteral("NVIDIA")) {
return Nvidia;
}
if (u == QStringLiteral("GENERIC")) {
return Generic;
}
return None;
}
QString Gpu::cleanName(QString s) {
static const QRegularExpression noise(
QStringLiteral("\\(R\\)|\\(TM\\)|Graphics"), QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression spaces(QStringLiteral("\\s+"));
s.replace(noise, QString());
s.replace(spaces, QStringLiteral(" "));
return s.trimmed();
}
} // namespace ZShell::services
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include "tickingservice.hpp"
#include <qprocess.h>
#include <qqmlintegration.h>
namespace ZShell::services {
class Gpu : public TickingService {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
enum Type {
Auto, // user override is empty (config "") — defer to detected autoType
None, // no usable GPU
Nvidia, // queried via nvidia-smi
Generic, // queried via /sys/class/drm/card*/device/gpu_busy_percent
};
Q_ENUM(Type)
private:
Q_PROPERTY(Type type READ type NOTIFY typeChanged)
Q_PROPERTY(Type userType READ userType NOTIFY userTypeChanged)
Q_PROPERTY(Type autoType READ autoType NOTIFY autoTypeChanged)
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged)
Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged)
Q_PROPERTY(qreal memoryUsed READ memoryUsed NOTIFY memoryUsedChanged)
Q_PROPERTY(qreal memoryTotal READ memoryTotal NOTIFY memoryTotalChanged)
public:
explicit Gpu(QObject* parent = nullptr);
[[nodiscard]] Type type() const;
[[nodiscard]] Type userType() const;
[[nodiscard]] Type autoType() const;
[[nodiscard]] QString name() const;
[[nodiscard]] qreal percentage() const;
[[nodiscard]] qreal temperature() const;
[[nodiscard]] qreal memoryUsed() const;
[[nodiscard]] qreal memoryTotal() const;
signals:
void typeChanged();
void userTypeChanged();
void autoTypeChanged();
void nameChanged();
void percentageChanged();
void temperatureChanged();
void memoryUsedChanged();
void memoryTotalChanged();
protected:
void tick() override;
private:
void detectTypeOnce();
void detectNameOnce();
void readGenericUsage();
void startNvidiaUsage();
void readGpuTemperature();
void setUserType(Type value);
void setAutoType(Type value);
void setName(QString value);
void setMemoryUsed(qreal value);
void setMemoryTotal(qreal value);
[[nodiscard]] static Type parseType(const QString& s);
[[nodiscard]] static QString cleanName(QString s);
Type m_userType = Auto;
Type m_autoType = None;
QString m_name;
qreal m_percentage = 0.0;
qreal m_temperature = 0.0;
qreal m_memoryUsed = 0.0;
qreal m_memoryTotal = 0.0;
QProcess* m_typeProc = nullptr;
QProcess* m_nameProc = nullptr;
QProcess* m_nvidiaProc = nullptr;
};
} // namespace ZShell::services
+59
View File
@@ -0,0 +1,59 @@
#include "memory.hpp"
#include <qfile.h>
#include <qregularexpression.h>
namespace ZShell::services {
Memory::Memory(QObject* parent)
: TickingService(parent) {
}
qreal Memory::used() const {
return m_used;
}
qreal Memory::total() const {
return m_total;
}
qreal Memory::percentage() const {
return m_total > 0.0 ? m_used / m_total : 0.0;
}
void Memory::tick() {
QFile f(QStringLiteral("/proc/meminfo"));
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
return;
}
const QByteArray data = f.readAll();
f.close();
static const QRegularExpression reTotal(QStringLiteral("MemTotal: *(\\d+)"));
static const QRegularExpression reAvail(QStringLiteral("MemAvailable: *(\\d+)"));
const QString text = QString::fromLatin1(data);
const auto totalMatch = reTotal.match(text);
const auto availMatch = reAvail.match(text);
if (!totalMatch.hasMatch() || !availMatch.hasMatch()) {
return;
}
const quint64 totalKib = totalMatch.captured(1).toULongLong();
const quint64 availKib = availMatch.captured(1).toULongLong();
if (totalKib == 0) {
return;
}
const quint64 usedKib = totalKib > availKib ? totalKib - availKib : 0;
if (totalKib == m_lastTotal && usedKib == m_lastUsed) {
return;
}
m_lastTotal = totalKib;
m_lastUsed = usedKib;
m_total = static_cast<qreal>(totalKib);
m_used = static_cast<qreal>(usedKib);
Q_EMIT changed();
}
} // namespace ZShell::services
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include "tickingservice.hpp"
#include <qqmlintegration.h>
#include <qvariant.h>
namespace ZShell::services {
class Memory : public TickingService {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(qreal used READ used NOTIFY changed)
Q_PROPERTY(qreal total READ total NOTIFY changed)
Q_PROPERTY(qreal percentage READ percentage NOTIFY changed)
public:
explicit Memory(QObject* parent = nullptr);
[[nodiscard]] qreal used() const;
[[nodiscard]] qreal total() const;
[[nodiscard]] qreal percentage() const;
signals:
void changed();
protected:
void tick() override;
private:
qreal m_used = 0.0;
qreal m_total = 1.0;
quint64 m_lastUsed = 0;
quint64 m_lastTotal = 0;
};
} // namespace ZShell::services
+166
View File
@@ -0,0 +1,166 @@
#include "sensorslib.hpp"
#include <atomic>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <qloggingcategory.h>
#include <sensors/sensors.h>
Q_LOGGING_CATEGORY(lcSensorsLib, "ZShell.services.sensorslib", QtInfoMsg)
namespace ZShell::services::sensorslib {
namespace {
std::atomic<bool> g_initOk{ false };
std::once_flag g_initFlag;
void doInit() {
if (sensors_init(nullptr) != 0) {
qCWarning(lcSensorsLib, "sensors_init failed");
g_initOk.store(false, std::memory_order_release);
return;
}
g_initOk.store(true, std::memory_order_release);
std::atexit([] {
if (g_initOk.load(std::memory_order_acquire)) {
sensors_cleanup();
}
});
}
[[nodiscard]] std::optional<double> readTempInput(const sensors_chip_name* chip, const sensors_feature* feat) {
const sensors_subfeature* sf = sensors_get_subfeature(chip, feat, SENSORS_SUBFEATURE_TEMP_INPUT);
if (!sf) {
return std::nullopt;
}
double value = 0.0;
if (sensors_get_value(chip, sf->number, &value) != 0) {
return std::nullopt;
}
return value;
}
[[nodiscard]] QByteArray featureLabel(const sensors_chip_name* chip, const sensors_feature* feat) {
char* raw = sensors_get_label(chip, feat);
if (!raw) {
return {};
}
QByteArray out(raw);
std::free(raw);
return out;
}
bool labelEquals(const QByteArray& label, const char* literal) {
return label == QByteArrayView(literal);
}
bool labelStartsWith(const QByteArray& label, const char* prefix) {
const auto n = std::strlen(prefix);
return static_cast<size_t>(label.size()) >= n && std::memcmp(label.constData(), prefix, n) == 0;
}
} // namespace
void ensureInit() {
std::call_once(g_initFlag, doInit);
}
std::optional<double> cpuPackageTemp() {
ensureInit();
if (!g_initOk.load(std::memory_order_acquire)) {
return std::nullopt;
}
std::optional<double> primary; // Package id N / Tdie
std::optional<double> fallback; // Tctl
int chipNr = 0;
while (const sensors_chip_name* chip = sensors_get_detected_chips(nullptr, &chipNr)) {
int featNr = 0;
while (const sensors_feature* feat = sensors_get_features(chip, &featNr)) {
if (feat->type != SENSORS_FEATURE_TEMP) {
continue;
}
const QByteArray label = featureLabel(chip, feat);
if (label.isEmpty()) {
continue;
}
if (labelStartsWith(label, "Package id ") || labelEquals(label, "Tdie")) {
if (auto v = readTempInput(chip, feat)) {
primary = v;
}
} else if (labelEquals(label, "Tctl")) {
if (auto v = readTempInput(chip, feat)) {
fallback = v;
}
}
}
}
return primary.has_value() ? primary : fallback;
}
std::optional<double> gpuPciAverageTemp() {
ensureInit();
if (!g_initOk.load(std::memory_order_acquire)) {
return std::nullopt;
}
double sumPrimary = 0.0;
int countPrimary = 0;
double sumFallback = 0.0;
int countFallback = 0;
int chipNr = 0;
while (const sensors_chip_name* chip = sensors_get_detected_chips(nullptr, &chipNr)) {
if (chip->bus.type != SENSORS_BUS_TYPE_PCI) {
continue;
}
int featNr = 0;
while (const sensors_feature* feat = sensors_get_features(chip, &featNr)) {
if (feat->type != SENSORS_FEATURE_TEMP) {
continue;
}
const QByteArray label = featureLabel(chip, feat);
if (label.isEmpty()) {
continue;
}
const bool tempIndexed = labelStartsWith(label, "temp") && label.size() > 4 &&
std::isdigit(static_cast<unsigned char>(label[4]));
const bool isPrimary = tempIndexed || labelEquals(label, "GPU core") || labelEquals(label, "edge");
const bool isFallback = labelEquals(label, "junction") || labelEquals(label, "mem");
if (!isPrimary && !isFallback) {
continue;
}
const auto v = readTempInput(chip, feat);
if (!v) {
continue;
}
if (isPrimary) {
sumPrimary += *v;
++countPrimary;
} else {
sumFallback += *v;
++countFallback;
}
}
}
if (countPrimary > 0) {
return sumPrimary / countPrimary;
}
if (countFallback > 0) {
return sumFallback / countFallback;
}
return std::nullopt;
}
} // namespace ZShell::services::sensorslib

Some files were not shown because too many files have changed in this diff Show More