Compare commits
36
Commits
4cb2d843a5
..
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6f7742bd8 | ||
|
|
a37f6075d1 | ||
|
|
80d5f13663 | ||
|
|
2f3f41cedd | ||
|
|
faad2dbd09 | ||
|
|
164b48ab6a | ||
|
|
e55d4aa36b | ||
|
|
91b4466773 | ||
|
|
7be81e584e | ||
|
|
8a87f86758 | ||
|
|
e619632077 | ||
|
|
81eb56e1d0 | ||
|
|
a1c148bf70 | ||
|
|
fc9be754fd | ||
|
|
ca1243f9c7 | ||
|
|
697058146e | ||
|
|
d516dd8be4 | ||
|
|
b17bc61d80 | ||
|
|
e37332fe6b | ||
|
|
c102d0e38f | ||
|
|
fbadcbb717 | ||
|
|
2cb4d9b739 | ||
|
|
a98a7944b9 | ||
|
|
5fb137699f | ||
|
|
866373be48 | ||
|
|
b034400df9 | ||
|
|
c0b62dfb6c | ||
|
|
ea82b8cf22 | ||
|
|
be8b7ae436 | ||
|
|
e3d3daf807 | ||
|
|
0057830fbc | ||
|
|
eafffdcc8f | ||
|
|
578a10c950 | ||
|
|
2034f3a8db | ||
|
|
7e38f3f428 | ||
|
|
2a50732bc2 |
@@ -12,11 +12,8 @@ Slider {
|
|||||||
readonly property bool isVertical: orientation === Qt.Vertical
|
readonly property bool isVertical: orientation === Qt.Vertical
|
||||||
property real multiplier: 100
|
property real multiplier: 100
|
||||||
property real oldValue
|
property real oldValue
|
||||||
|
|
||||||
// Wrapper components can inject their own track visuals here.
|
|
||||||
property Component trackContent
|
property Component trackContent
|
||||||
|
|
||||||
// Keep current behavior for existing usages.
|
|
||||||
orientation: Qt.Vertical
|
orientation: Qt.Vertical
|
||||||
|
|
||||||
background: CustomRect {
|
background: CustomRect {
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ CustomRect {
|
|||||||
property color disabledColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1)
|
property color disabledColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1)
|
||||||
property color disabledOnColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38)
|
property color disabledOnColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38)
|
||||||
property bool fillWidth
|
property bool fillWidth
|
||||||
|
property font font: ({
|
||||||
|
family: Appearance.font.family.sans,
|
||||||
|
pointSize: Appearance.font.size.larger,
|
||||||
|
bold: false
|
||||||
|
})
|
||||||
property real horizontalPadding: padding
|
property real horizontalPadding: padding
|
||||||
readonly property alias hovered: stateLayer.containsMouse
|
readonly property alias hovered: stateLayer.containsMouse
|
||||||
required implicitHeight
|
required implicitHeight
|
||||||
@@ -71,7 +76,7 @@ CustomRect {
|
|||||||
id: stateLayer
|
id: stateLayer
|
||||||
|
|
||||||
color: root.internalChecked ? root.activeOnColor : root.inactiveOnColor
|
color: root.internalChecked ? root.activeOnColor : root.inactiveOnColor
|
||||||
enabled: enabled
|
enabled: root.enabled
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (root.isToggle)
|
if (root.isToggle)
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ BusyIndicator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property int animState
|
property int animState
|
||||||
property color bgColour: DynamicColors.palette.m3secondaryContainer
|
property color bgColor: DynamicColors.palette.m3secondaryContainer
|
||||||
property color fgColour: DynamicColors.palette.m3primary
|
property color fgColor: DynamicColors.palette.m3primary
|
||||||
property real implicitSize: Appearance.font.size.normal * 3
|
property real implicitSize: Appearance.font.size.normal * 3
|
||||||
property real internalStrokeWidth: strokeWidth
|
property real internalStrokeWidth: strokeWidth
|
||||||
readonly property alias progress: manager.progress
|
readonly property alias progress: manager.progress
|
||||||
@@ -31,8 +31,8 @@ BusyIndicator {
|
|||||||
|
|
||||||
contentItem: CircularProgress {
|
contentItem: CircularProgress {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
bgColour: root.bgColour
|
bgColor: root.bgColor
|
||||||
fgColour: root.fgColour
|
fgColor: root.fgColor
|
||||||
padding: root.padding
|
padding: root.padding
|
||||||
rotation: manager.rotation
|
rotation: manager.rotation
|
||||||
startAngle: manager.startFraction * 360
|
startAngle: manager.startFraction * 360
|
||||||
@@ -73,7 +73,6 @@ BusyIndicator {
|
|||||||
|
|
||||||
CircularIndicatorManager {
|
CircularIndicatorManager {
|
||||||
id: manager
|
id: manager
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
|
|||||||
@@ -1,66 +1,116 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Shapes
|
import QtQuick.Shapes
|
||||||
|
import ZShell.Components
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
|
||||||
Shape {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property real arcRadius: (size - padding - strokeWidth) / 2
|
readonly property real arcRadius: (size - padding - strokeWidth * (1 + waveAmplitude * 2)) / 2
|
||||||
property color bgColour: DynamicColors.palette.m3secondaryContainer
|
property color bgColor: DynamicColors.palette.m3secondaryContainer
|
||||||
property color fgColour: DynamicColors.palette.m3primary
|
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)
|
readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI)
|
||||||
|
property alias hasEndIndicator: dot.active
|
||||||
|
property real implicitSize
|
||||||
property int padding: 0
|
property int padding: 0
|
||||||
readonly property real size: Math.min(width, height)
|
readonly property real size: Math.min(width, height)
|
||||||
property int spacing: Appearance.spacing.small
|
property int spacing: Appearance.spacing.small
|
||||||
property int startAngle: -90
|
property int startAngle: -90
|
||||||
property int strokeWidth: Appearance.padding.smaller
|
property int strokeWidth: Appearance.padding.small
|
||||||
readonly property real vValue: value || 1 / 360
|
property int sweepAngle: 360
|
||||||
|
readonly property real thickness: strokeWidth * (1 + waveAmplitude) * 2
|
||||||
property real value
|
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
|
implicitHeight: implicitSize
|
||||||
preferredRendererType: Shape.CurveRenderer
|
implicitWidth: implicitSize
|
||||||
|
|
||||||
ShapePath {
|
Shape {
|
||||||
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
|
asynchronous: true
|
||||||
fillColor: "transparent"
|
opacity: Math.min(1, remainingArc.sweepAngle)
|
||||||
strokeColor: root.bgColour
|
preferredRendererType: Shape.CurveRenderer
|
||||||
strokeWidth: root.strokeWidth
|
|
||||||
|
|
||||||
Behavior on strokeColor {
|
ShapePath {
|
||||||
CAnim {
|
capStyle: ShapePath.RoundCap
|
||||||
duration: Appearance.anim.durations.large
|
fillColor: "transparent"
|
||||||
|
strokeColor: root.bgColor
|
||||||
|
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
|
||||||
|
|
||||||
|
Behavior on strokeColor {
|
||||||
|
CAnim {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
PathAngleArc {
|
PathAngleArc {
|
||||||
centerX: root.size / 2
|
id: remainingArc
|
||||||
centerY: root.size / 2
|
|
||||||
radiusX: root.arcRadius
|
centerX: root.size / 2
|
||||||
radiusY: root.arcRadius
|
centerY: root.size / 2
|
||||||
startAngle: root.startAngle + 360 * root.vValue + root.gapAngle
|
radiusX: root.arcRadius
|
||||||
sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2)
|
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 {
|
WavyLine {
|
||||||
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
|
id: wave
|
||||||
fillColor: "transparent"
|
|
||||||
strokeColor: root.fgColour
|
|
||||||
strokeWidth: root.strokeWidth
|
|
||||||
|
|
||||||
Behavior on strokeColor {
|
amplitudeMultiplier: root.wavy ? 0.5 : 0
|
||||||
CAnim {
|
anchors.fill: parent
|
||||||
duration: Appearance.anim.durations.large
|
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 {
|
duration: 2000
|
||||||
centerX: root.size / 2
|
easing.type: Easing.Linear
|
||||||
centerY: root.size / 2
|
from: 0
|
||||||
radiusX: root.arcRadius
|
loops: Animation.Infinite
|
||||||
radiusY: root.arcRadius
|
paused: root.wavePaused || wave.amplitudeMultiplier === 0
|
||||||
startAngle: root.startAngle
|
running: true
|
||||||
sweepAngle: 360 * root.vValue
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncFromPenColor() {
|
function syncFromPenColor() {
|
||||||
if (!drawing)
|
if (!drawing.drawingState)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const c = drawing.penColor;
|
const c = drawing.drawingState.penColor;
|
||||||
|
|
||||||
if (c.hsvSaturation > 0) {
|
if (c.hsvSaturation > 0) {
|
||||||
currentHue = c.hsvHue;
|
currentHue = c.hsvHue;
|
||||||
@@ -85,12 +85,20 @@ Item {
|
|||||||
|
|
||||||
currentHue = relative / arcSweep;
|
currentHue = relative / arcSweep;
|
||||||
lastChromaticHue = currentHue;
|
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
|
implicitHeight: 180
|
||||||
implicitWidth: 220
|
implicitWidth: 220
|
||||||
|
|
||||||
|
Behavior on currentHue {
|
||||||
|
enabled: !root.dragActive
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
type: Anim.StandardLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: syncFromPenColor()
|
Component.onCompleted: syncFromPenColor()
|
||||||
onCurrentHueChanged: canvas.requestPaint()
|
onCurrentHueChanged: canvas.requestPaint()
|
||||||
onDrawingChanged: syncFromPenColor()
|
onDrawingChanged: syncFromPenColor()
|
||||||
@@ -103,7 +111,7 @@ Item {
|
|||||||
root.syncFromPenColor();
|
root.syncFromPenColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
target: root.drawing
|
target: root.drawing.drawingState
|
||||||
}
|
}
|
||||||
|
|
||||||
Canvas {
|
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 {
|
Item {
|
||||||
id: handle
|
id: handle
|
||||||
|
|
||||||
@@ -174,7 +197,7 @@ Item {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.centerIn: parent
|
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
|
height: width
|
||||||
radius: width / 2
|
radius: width / 2
|
||||||
width: parent.width - 12
|
width: parent.width - 12
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
Flickable {
|
Flickable {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
interactive: !Visibilities.getForActive().isDrawing
|
||||||
maximumFlickVelocity: 3000
|
maximumFlickVelocity: 3000
|
||||||
|
|
||||||
rebound: Transition {
|
rebound: Transition {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool doneFakeFlick
|
property bool doneFakeFlick
|
||||||
|
|
||||||
|
interactive: !Visibilities.getForActive().isDrawing
|
||||||
maximumFlickVelocity: 3000
|
maximumFlickVelocity: 3000
|
||||||
|
|
||||||
rebound: Transition {
|
rebound: Transition {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Templates
|
import QtQuick.Templates
|
||||||
|
import qs.Helpers
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
|
||||||
ScrollBar {
|
ScrollBar {
|
||||||
@@ -15,6 +16,7 @@ ScrollBar {
|
|||||||
property bool shouldBeActive
|
property bool shouldBeActive
|
||||||
readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0
|
readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0
|
||||||
|
|
||||||
|
enabled: !Visibilities.getForActive().isDrawing
|
||||||
implicitWidth: Appearance.padding.extraSmall * 2
|
implicitWidth: Appearance.padding.extraSmall * 2
|
||||||
|
|
||||||
contentItem: Item {
|
contentItem: Item {
|
||||||
@@ -59,11 +61,13 @@ ScrollBar {
|
|||||||
implicitHeight: root.height * root.effectiveSize
|
implicitHeight: root.height * root.effectiveSize
|
||||||
implicitWidth: fullMouse.pressed || fullMouse.containsMouse ? Appearance.padding.extraSmall * 2 : Appearance.padding.extraSmall
|
implicitWidth: fullMouse.pressed || fullMouse.containsMouse ? Appearance.padding.extraSmall * 2 : Appearance.padding.extraSmall
|
||||||
opacity: {
|
opacity: {
|
||||||
|
if (!root.enabled)
|
||||||
|
return 0;
|
||||||
if (root.size === 1)
|
if (root.size === 1)
|
||||||
return 0;
|
return 0;
|
||||||
if (fullMouse.pressed)
|
if (fullMouse.pressed)
|
||||||
return 1;
|
return 1;
|
||||||
if (mouse.containsMouse)
|
if (fullMouse.containsMouse)
|
||||||
return 0.8;
|
return 0.8;
|
||||||
if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive)
|
if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive)
|
||||||
return 0.6;
|
return 0.6;
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ Slider {
|
|||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
id: inset
|
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
|
property real dockT: attached ? 1 : 0
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
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 {
|
Binding {
|
||||||
id: posBinding
|
id: posBinding
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ Text {
|
|||||||
enabled: root.animate
|
enabled: root.animate
|
||||||
|
|
||||||
SequentialAnimation {
|
SequentialAnimation {
|
||||||
Anim {
|
TAnim {
|
||||||
property: root.animateProp
|
|
||||||
target: root
|
target: root
|
||||||
to: root.animateFrom
|
to: root.animateFrom
|
||||||
type: Anim.FastEffects
|
type: Anim.FastEffects
|
||||||
@@ -37,12 +36,18 @@ Text {
|
|||||||
PropertyAction {
|
PropertyAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
Anim {
|
TAnim {
|
||||||
property: root.animateProp
|
|
||||||
target: root
|
target: root
|
||||||
to: root.animateTo
|
to: root.animateTo
|
||||||
type: Anim.DefaultEffects
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ TextField {
|
|||||||
property bool disableBlink
|
property bool disableBlink
|
||||||
|
|
||||||
color: DynamicColors.palette.m3primary
|
color: DynamicColors.palette.m3primary
|
||||||
height: root.cursorHeight
|
|
||||||
implicitWidth: 2
|
implicitWidth: 2
|
||||||
radius: Appearance.rounding.normal
|
radius: Appearance.rounding.normal
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import qs.Config
|
|||||||
ButtonBase {
|
ButtonBase {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property alias font: label.font
|
|
||||||
property alias icon: label.text
|
property alias icon: label.text
|
||||||
readonly property alias label: label
|
readonly property alias label: label
|
||||||
|
|
||||||
@@ -36,6 +35,7 @@ ButtonBase {
|
|||||||
anchors.verticalCenterOffset: 1
|
anchors.verticalCenterOffset: 1
|
||||||
color: root.onColor
|
color: root.onColor
|
||||||
fill: !root.isToggle || root.internalChecked ? 1 : 0
|
fill: !root.isToggle || root.internalChecked ? 1 : 0
|
||||||
|
font.pointSize: root.font.pointSize
|
||||||
|
|
||||||
Behavior on fill {
|
Behavior on fill {
|
||||||
Anim {
|
Anim {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ MouseArea {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
|
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
|
||||||
|
enabled: parent.enabled && !Visibilities.getForActive().isDrawing
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
|
||||||
Behavior on stateOpacity {
|
Behavior on stateOpacity {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,9 +77,11 @@ JsonObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
component Padding: JsonObject {
|
component Padding: JsonObject {
|
||||||
|
property int extraLarge: 28 * scale
|
||||||
property int extraLargeIncreased: 32 * scale
|
property int extraLargeIncreased: 32 * scale
|
||||||
property int extraSmall: 4 * scale
|
property int extraSmall: 4 * scale
|
||||||
property int large: 16 * scale
|
property int large: 16 * scale
|
||||||
|
property int largeIncreased: 20 * scale
|
||||||
property int larger: 12 * scale
|
property int larger: 12 * scale
|
||||||
property int normal: 8 * scale
|
property int normal: 8 * scale
|
||||||
property real scale: 1
|
property real scale: 1
|
||||||
|
|||||||
+3
-1
@@ -188,7 +188,7 @@ Singleton {
|
|||||||
resourceProgessThickness: dashboard.sizes.resourceProgessThickness,
|
resourceProgessThickness: dashboard.sizes.resourceProgessThickness,
|
||||||
weatherWidth: dashboard.sizes.weatherWidth,
|
weatherWidth: dashboard.sizes.weatherWidth,
|
||||||
mediaCoverArtSize: dashboard.sizes.mediaCoverArtSize,
|
mediaCoverArtSize: dashboard.sizes.mediaCoverArtSize,
|
||||||
mediaVisualiserSize: dashboard.sizes.mediaVisualiserSize,
|
mediaVisualizerSize: dashboard.sizes.mediaVisualizerSize,
|
||||||
resourceSize: dashboard.sizes.resourceSize
|
resourceSize: dashboard.sizes.resourceSize
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -209,6 +209,7 @@ Singleton {
|
|||||||
return {
|
return {
|
||||||
logo: general.logo,
|
logo: general.logo,
|
||||||
wallpaperPath: general.wallpaperPath,
|
wallpaperPath: general.wallpaperPath,
|
||||||
|
showOverFullscreen: general.showOverFullscreen,
|
||||||
desktopIcons: general.desktopIcons,
|
desktopIcons: general.desktopIcons,
|
||||||
dateFormat: general.dateFormat,
|
dateFormat: general.dateFormat,
|
||||||
color: {
|
color: {
|
||||||
@@ -330,6 +331,7 @@ Singleton {
|
|||||||
weatherLocation: services.weatherLocation,
|
weatherLocation: services.weatherLocation,
|
||||||
updates: services.updates,
|
updates: services.updates,
|
||||||
useFahrenheit: services.useFahrenheit,
|
useFahrenheit: services.useFahrenheit,
|
||||||
|
minBrightness: services.minBrightness,
|
||||||
ddcutilService: services.ddcutilService,
|
ddcutilService: services.ddcutilService,
|
||||||
useTwelveHourClock: services.useTwelveHourClock,
|
useTwelveHourClock: services.useTwelveHourClock,
|
||||||
gpuType: services.gpuType,
|
gpuType: services.gpuType,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ JsonObject {
|
|||||||
readonly property int mediaCoverArtSize: 150
|
readonly property int mediaCoverArtSize: 150
|
||||||
readonly property int mediaProgressSweep: 180
|
readonly property int mediaProgressSweep: 180
|
||||||
readonly property int mediaProgressThickness: 8
|
readonly property int mediaProgressThickness: 8
|
||||||
readonly property int mediaVisualiserSize: 80
|
readonly property int mediaVisualizerSize: 200
|
||||||
readonly property int mediaWidth: 200
|
readonly property int mediaWidth: 200
|
||||||
readonly property int resourceProgessThickness: 10
|
readonly property int resourceProgessThickness: 10
|
||||||
readonly property int resourceSize: 200
|
readonly property int resourceSize: 200
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ JsonObject {
|
|||||||
property Idle idle: Idle {
|
property Idle idle: Idle {
|
||||||
}
|
}
|
||||||
property string logo: ""
|
property string logo: ""
|
||||||
|
property bool showOverFullscreen: true
|
||||||
property string wallpaperPath: Quickshell.env("HOME") + "/Pictures/Wallpapers"
|
property string wallpaperPath: Quickshell.env("HOME") + "/Pictures/Wallpapers"
|
||||||
|
|
||||||
component Apps: JsonObject {
|
component Apps: JsonObject {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ JsonObject {
|
|||||||
property string defaultPlayer: "Spotify"
|
property string defaultPlayer: "Spotify"
|
||||||
property string gpuType: ""
|
property string gpuType: ""
|
||||||
property real maxVolume: 1.0
|
property real maxVolume: 1.0
|
||||||
|
property real minBrightness: 0.01
|
||||||
property list<var> playerAliases: [
|
property list<var> playerAliases: [
|
||||||
{
|
{
|
||||||
"from": "com.github.th_ch.youtube_music",
|
"from": "com.github.th_ch.youtube_music",
|
||||||
|
|||||||
@@ -215,6 +215,10 @@ Singleton {
|
|||||||
notif.hasActionIcons = notif.notification.hasActionIcons;
|
notif.hasActionIcons = notif.notification.hasActionIcons;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onHintsChanged(): void {
|
||||||
|
notif.hints = notif.notification.hints;
|
||||||
|
}
|
||||||
|
|
||||||
function onImageChanged(): void {
|
function onImageChanged(): void {
|
||||||
notif.imageSource = notif.notification.image || "";
|
notif.imageSource = notif.notification.image || "";
|
||||||
notif.image = notif.imageSource;
|
notif.image = notif.imageSource;
|
||||||
@@ -237,6 +241,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
property real expireTimeout: 5
|
property real expireTimeout: 5
|
||||||
property bool hasActionIcons
|
property bool hasActionIcons
|
||||||
|
property var hints
|
||||||
property string image
|
property string image
|
||||||
property string imageSource
|
property string imageSource
|
||||||
property var locks: new Set()
|
property var locks: new Set()
|
||||||
@@ -333,6 +338,7 @@ Singleton {
|
|||||||
appName = notification.appName;
|
appName = notification.appName;
|
||||||
imageSource = notification.image || "";
|
imageSource = notification.image || "";
|
||||||
image = imageSource;
|
image = imageSource;
|
||||||
|
hints = notification.hints;
|
||||||
expireTimeout = notification.expireTimeout;
|
expireTimeout = notification.expireTimeout;
|
||||||
urgency = notification.urgency;
|
urgency = notification.urgency;
|
||||||
resident = notification.resident;
|
resident = notification.resident;
|
||||||
|
|||||||
+21
-27
@@ -1,36 +1,30 @@
|
|||||||
import QtQuick
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
Canvas {
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import ZShell.Internal
|
||||||
|
|
||||||
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property color penColor: "white"
|
readonly property alias content: contentLoader.item
|
||||||
property real penWidth: 4
|
readonly property PersistentProperties drawingState: PersistentProperties {
|
||||||
property var points: []
|
property color penColor: "white"
|
||||||
|
property int penWidth: 4
|
||||||
|
|
||||||
function clear(): void {
|
reloadableId: "drawingState"
|
||||||
var ctx = getContext('2d');
|
|
||||||
root.points = [];
|
|
||||||
ctx.reset();
|
|
||||||
root.requestPaint();
|
|
||||||
}
|
}
|
||||||
|
required property PersistentProperties visibilities
|
||||||
|
|
||||||
renderStrategy: Canvas.Cooperative
|
Loader {
|
||||||
|
id: contentLoader
|
||||||
|
|
||||||
onPaint: {
|
active: root.visibilities.isDrawing
|
||||||
if (points.length < 2)
|
anchors.fill: parent
|
||||||
return;
|
|
||||||
var ctx = root.getContext('2d');
|
sourceComponent: StrokeCanvas {
|
||||||
ctx.save();
|
penColor: root.drawingState.penColor
|
||||||
ctx.lineWidth = root.penWidth;
|
penWidth: root.drawingState.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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,9 +9,9 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property Item bar
|
required property Item bar
|
||||||
|
required property real borderThickness
|
||||||
property bool dashboardShortcutActive
|
property bool dashboardShortcutActive
|
||||||
required property Drawing drawing
|
required property Drawing drawing
|
||||||
required property DrawingInput input
|
|
||||||
property bool osdShortcutActive
|
property bool osdShortcutActive
|
||||||
required property Panels panels
|
required property Panels panels
|
||||||
required property BarPopouts.Wrapper popouts
|
required property BarPopouts.Wrapper popouts
|
||||||
@@ -48,7 +48,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function withinPanelWidth(panel: Item, x: real, y: real): bool {
|
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;
|
return x >= panelX && x <= panelX + panel.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ Item {
|
|||||||
|
|
||||||
cursorShape: (active && centroid.pressPosition.y < root.bar.implicitHeight) ? Qt.ClosedHandCursor : undefined
|
cursorShape: (active && centroid.pressPosition.y < root.bar.implicitHeight) ? Qt.ClosedHandCursor : undefined
|
||||||
dragThreshold: 0
|
dragThreshold: 0
|
||||||
|
enabled: !root.visibilities.isDrawing
|
||||||
grabPermissions: PointerHandler.CanTakeOverFromHandlersOfSameType | PointerHandler.ApprovesTakeOverByAnything
|
grabPermissions: PointerHandler.CanTakeOverFromHandlersOfSameType | PointerHandler.ApprovesTakeOverByAnything
|
||||||
maximumPointCount: 1
|
maximumPointCount: 1
|
||||||
minimumPointCount: 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)
|
if (centroid.pressPosition.y > root.screen.height - Config.barConfig.border && centroid.pressPosition.x < root.screen.width / 5 && dragY < -50)
|
||||||
root.visibilities.clipboard = true;
|
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) {
|
if (dragY < -10) {
|
||||||
root.visibilities.dock = true;
|
root.visibilities.dock = true;
|
||||||
root.singleGestureTriggered = 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 {
|
HoverHandler {
|
||||||
id: hoverHandler
|
id: hoverHandler
|
||||||
|
|
||||||
|
cursorShape: root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, point.position.x, point.position.y) ? Qt.BlankCursor : undefined
|
||||||
|
|
||||||
onHoveredChanged: {
|
onHoveredChanged: {
|
||||||
if (!hovered) {
|
if (!hovered) {
|
||||||
if (!root.osdShortcutActive) {
|
if (!root.osdShortcutActive) {
|
||||||
@@ -138,9 +182,15 @@ Item {
|
|||||||
const x = point.position.x;
|
const x = point.position.x;
|
||||||
const y = point.position.y;
|
const y = point.position.y;
|
||||||
|
|
||||||
if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) {
|
if (root.visibilities.isDrawing) {
|
||||||
root.input.z = 2;
|
if (root.inLeftPanel(root.panels.drawing, x, y) && !(drawingHandler.point.pressedButtons & Qt.LeftButton)) {
|
||||||
root.panels.drawing.expanded = false;
|
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)
|
if (!root.visibilities.bar && Config.barConfig.autoHide && y < root.bar.implicitHeight)
|
||||||
|
|||||||
+4
-2
@@ -20,12 +20,13 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property Item bar
|
required property Item bar
|
||||||
|
required property real borderThickness
|
||||||
readonly property alias clipboard: clipboard
|
readonly property alias clipboard: clipboard
|
||||||
readonly property alias dashboard: dashboard
|
readonly property alias dashboard: dashboard
|
||||||
readonly property alias dashboardWrapper: dashboardWrapper
|
readonly property alias dashboardWrapper: dashboardWrapper
|
||||||
readonly property alias dock: dock
|
readonly property alias dock: dock
|
||||||
readonly property alias drawing: drawing
|
readonly property alias drawing: drawing
|
||||||
required property Canvas drawingItem
|
required property var drawingItem
|
||||||
readonly property alias launcher: launcher
|
readonly property alias launcher: launcher
|
||||||
readonly property alias notifications: notifications
|
readonly property alias notifications: notifications
|
||||||
readonly property alias osd: osd
|
readonly property alias osd: osd
|
||||||
@@ -43,7 +44,7 @@ Item {
|
|||||||
required property PersistentProperties visibilities
|
required property PersistentProperties visibilities
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Config.barConfig.border
|
anchors.margins: borderThickness
|
||||||
anchors.topMargin: bar.implicitHeight
|
anchors.topMargin: bar.implicitHeight
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -99,6 +100,7 @@ Item {
|
|||||||
id: popouts
|
id: popouts
|
||||||
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
|
borderThickness: root.borderThickness
|
||||||
screen: root.screen
|
screen: root.screen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+99
-47
@@ -18,18 +18,56 @@ CustomWindow {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property alias bar: bar
|
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 interactionWrapper: interactions
|
||||||
readonly property alias menuRegion: menuPopoutRegion
|
readonly property alias menuRegion: menuPopoutRegion
|
||||||
|
readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)
|
||||||
property var root: Quickshell.shellDir
|
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.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"
|
color: "transparent"
|
||||||
contentItem.focus: true
|
contentItem.focus: true
|
||||||
mask: visibilities.isDrawing ? null : region
|
mask: visibilities.isDrawing ? null : (hasFullscreen ? emptyRegion : region)
|
||||||
name: "Bar"
|
name: "Bar"
|
||||||
|
|
||||||
|
Behavior on fsTransitionProg {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on surfaceColor {
|
||||||
|
CAnim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contentItem.Keys.onEscapePressed: {
|
contentItem.Keys.onEscapePressed: {
|
||||||
if (Config.barConfig.autoHide)
|
if (Config.barConfig.autoHide)
|
||||||
visibilities.bar = false;
|
visibilities.bar = false;
|
||||||
@@ -61,15 +99,24 @@ CustomWindow {
|
|||||||
intersection: Intersection.Subtract
|
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 {
|
Region {
|
||||||
id: region
|
id: region
|
||||||
|
|
||||||
height: root.height - bar.implicitHeight - Config.barConfig.border
|
height: root.height - bar.implicitHeight - root.borderThickness - root.dragMaskPadding * 2
|
||||||
intersection: Intersection.Xor
|
intersection: Intersection.Xor
|
||||||
regions: [...popoutRegions.instances, menuPopoutRegion]
|
regions: [...popoutRegions.instances, menuPopoutRegion]
|
||||||
width: root.width - Config.barConfig.border * 2
|
width: root.width - root.borderThickness * 2 - root.dragMaskPadding * 2
|
||||||
x: Config.barConfig.border
|
x: root.borderThickness + root.dragMaskPadding
|
||||||
y: bar.implicitHeight
|
y: bar.implicitHeight + root.dragMaskPadding
|
||||||
}
|
}
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
@@ -90,7 +137,7 @@ CustomWindow {
|
|||||||
height: modelData.height
|
height: modelData.height
|
||||||
intersection: Intersection.Subtract
|
intersection: Intersection.Subtract
|
||||||
width: modelData.width
|
width: modelData.width
|
||||||
x: modelData.x + Config.barConfig.border
|
x: modelData.x + root.borderThickness
|
||||||
y: modelData.y + bar.implicitHeight
|
y: modelData.y + bar.implicitHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,37 +195,34 @@ CustomWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
id: surface
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
opacity: Appearance.transparency.enabled ? DynamicColors.transparency.base : 1
|
opacity: root.surfaceColor.a
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
blurMax: 32
|
blurMax: 32
|
||||||
shadowColor: Qt.alpha(DynamicColors.palette.m3shadow, 1)
|
shadowColor: Qt.alpha(DynamicColors.palette.m3shadow, Math.max(0, root.shadowOpacity))
|
||||||
shadowEnabled: true
|
shadowEnabled: true
|
||||||
}
|
}
|
||||||
|
|
||||||
BlobGroup {
|
BlobGroup {
|
||||||
id: blobGroup
|
id: blobGroup
|
||||||
|
|
||||||
color: DynamicColors.palette.m3surface
|
color: root.surfaceColor
|
||||||
smoothing: Config.barConfig.smoothing
|
smoothing: Config.barConfig.smoothing
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
CAnim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BlobInvertedRect {
|
BlobInvertedRect {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: -50
|
anchors.margins: -50
|
||||||
borderBottom: Config.barConfig.border - anchors.margins
|
borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset
|
||||||
borderLeft: Config.barConfig.border - anchors.margins
|
borderLeft: root.borderThickness - anchors.margins - root.sdfBorderOffset
|
||||||
borderRight: Config.barConfig.border - anchors.margins
|
borderRight: root.borderThickness - anchors.margins - root.sdfBorderOffset
|
||||||
borderTop: bar.implicitHeight - anchors.margins
|
borderTop: bar.implicitHeight - anchors.margins - root.sdfBorderOffset
|
||||||
group: blobGroup
|
group: blobGroup
|
||||||
radius: Config.barConfig.rounding
|
radius: root.borderRounding
|
||||||
}
|
}
|
||||||
|
|
||||||
PanelBg {
|
PanelBg {
|
||||||
@@ -191,7 +235,7 @@ CustomWindow {
|
|||||||
implicitWidth: panels.dashboard.width
|
implicitWidth: panels.dashboard.width
|
||||||
panel: panels.dashboardWrapper
|
panel: panels.dashboardWrapper
|
||||||
radius: Appearance.rounding.normal
|
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
|
y: panels.dashboardWrapper.y + panels.dashboard.y + bar.implicitHeight - panels.dashboard.height * extraHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +269,7 @@ CustomWindow {
|
|||||||
implicitWidth: panels.osd.width
|
implicitWidth: panels.osd.width
|
||||||
panel: panels.osdWrapper
|
panel: panels.osdWrapper
|
||||||
radius: 20
|
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
|
y: panels.osdWrapper.y + panels.osd.y + bar.implicitHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +299,7 @@ CustomWindow {
|
|||||||
implicitWidth: panels.popouts.width
|
implicitWidth: panels.popouts.width
|
||||||
panel: panels.popoutsWrapper
|
panel: panels.popoutsWrapper
|
||||||
radius: panels.popouts.current?.panelRadius ?? Appearance.rounding.normal
|
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
|
y: panels.popoutsWrapper.y + panels.popouts.y + bar.implicitHeight - panels.popouts.height * extraHeight
|
||||||
|
|
||||||
Behavior on extraHeight {
|
Behavior on extraHeight {
|
||||||
@@ -271,8 +315,8 @@ CustomWindow {
|
|||||||
implicitHeight: panels.resources.height
|
implicitHeight: panels.resources.height
|
||||||
implicitWidth: panels.resources.width
|
implicitWidth: panels.resources.width
|
||||||
panel: panels.resourcesWrapper
|
panel: panels.resourcesWrapper
|
||||||
radius: Appearance.rounding.normal
|
radius: Appearance.rounding.large
|
||||||
x: panels.resourcesWrapper.x + panels.resources.x + Config.barConfig.border
|
x: panels.resourcesWrapper.x + panels.resources.x + root.borderThickness
|
||||||
y: panels.resourcesWrapper.y + panels.resources.y + bar.implicitHeight
|
y: panels.resourcesWrapper.y + panels.resources.y + bar.implicitHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +332,7 @@ CustomWindow {
|
|||||||
radius: Appearance.rounding.large
|
radius: Appearance.rounding.large
|
||||||
topLeftRadius: Appearance.rounding.large + Appearance.padding.smaller
|
topLeftRadius: Appearance.rounding.large + Appearance.padding.smaller
|
||||||
topRightRadius: 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
|
y: panels.settingsWrapper.y + panels.settings.y + bar.implicitHeight - panels.settings.height * extraHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,32 +361,37 @@ CustomWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Drawing {
|
||||||
id: drawingLoader
|
id: drawing
|
||||||
|
|
||||||
active: visibilities.isDrawing
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
layer.enabled: true
|
||||||
|
visibilities: visibilities
|
||||||
|
z: 2
|
||||||
|
|
||||||
sourceComponent: Drawing {
|
layer.effect: MultiEffect {
|
||||||
id: drawing
|
maskEnabled: true
|
||||||
|
maskInverted: true
|
||||||
|
maskSource: maskSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Item {
|
||||||
id: inputLoader
|
id: maskSource
|
||||||
|
|
||||||
active: visibilities.isDrawing
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
z: 2
|
layer.enabled: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
sourceComponent: DrawingInput {
|
CustomRect {
|
||||||
id: input
|
readonly property int extraWidth: radius
|
||||||
|
|
||||||
bar: bar
|
color: "white"
|
||||||
drawing: drawingLoader.item
|
implicitHeight: panels.drawing.height
|
||||||
panels: panels
|
implicitWidth: panels.drawing.width + extraWidth
|
||||||
popout: panels.drawing
|
radius: drawingBg.radius
|
||||||
visibilities: visibilities
|
x: -extraWidth + root.borderThickness + panels.drawing.x
|
||||||
|
y: panels.drawing.y + bar.implicitHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,9 +400,9 @@ CustomWindow {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
bar: bar
|
bar: bar
|
||||||
drawing: drawingLoader.item
|
borderThickness: root.borderLayoutThickness
|
||||||
|
drawing: drawing
|
||||||
enabled: true
|
enabled: true
|
||||||
input: inputLoader.item
|
|
||||||
panels: panels
|
panels: panels
|
||||||
popouts: panels.popouts
|
popouts: panels.popouts
|
||||||
screen: root.screen
|
screen: root.screen
|
||||||
@@ -363,7 +412,8 @@ CustomWindow {
|
|||||||
id: panels
|
id: panels
|
||||||
|
|
||||||
bar: bar
|
bar: bar
|
||||||
drawingItem: drawingLoader.item
|
borderThickness: root.borderThickness
|
||||||
|
drawingItem: drawing
|
||||||
screen: root.screen
|
screen: root.screen
|
||||||
visibilities: visibilities
|
visibilities: visibilities
|
||||||
|
|
||||||
@@ -407,6 +457,8 @@ CustomWindow {
|
|||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
enabled: !visibilities.isDrawing
|
||||||
|
fullscreen: root.hasFullscreen
|
||||||
popouts: panels.popouts
|
popouts: panels.popouts
|
||||||
popoutsWrapper: panels.popoutsWrapper
|
popoutsWrapper: panels.popoutsWrapper
|
||||||
screen: root.screen
|
screen: root.screen
|
||||||
@@ -423,7 +475,7 @@ CustomWindow {
|
|||||||
implicitHeight: panel.height
|
implicitHeight: panel.height
|
||||||
implicitWidth: panel.width
|
implicitWidth: panel.width
|
||||||
radius: Appearance.rounding.smallest
|
radius: Appearance.rounding.smallest
|
||||||
x: panel.x + Config.barConfig.border
|
x: panel.x + root.borderThickness
|
||||||
y: panel.y + bar.implicitHeight
|
y: panel.y + bar.implicitHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setBrightness(value: real): void {
|
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);
|
const rounded = Math.round(value * 100);
|
||||||
if (Math.round(brightness * 100) === rounded)
|
if (Math.round(brightness * 100) === rounded)
|
||||||
return;
|
return;
|
||||||
|
|||||||
+30
-114
@@ -20,10 +20,8 @@ Singleton {
|
|||||||
}))
|
}))
|
||||||
property string previewImageFile: "/tmp/qs-cliphist-preview.img"
|
property string previewImageFile: "/tmp/qs-cliphist-preview.img"
|
||||||
property string previewImageSource: ""
|
property string previewImageSource: ""
|
||||||
property bool previewIsCode: looksLikeCode(previewText)
|
|
||||||
property bool previewIsImage: false
|
property bool previewIsImage: false
|
||||||
readonly property string previewMarkup: previewIsCode ? generateHighlightedMarkup(previewText) : previewText
|
property list<var> previewText: []
|
||||||
property string previewText: ""
|
|
||||||
property int previewToken: 0
|
property int previewToken: 0
|
||||||
property real scoreThreshold: 0.2
|
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 === "" ? " " : 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 {
|
function paste(entry): void {
|
||||||
Quickshell.execDetached(["bash", "-c", `printf '${shellSingleQuoteEscape(entry)}' | ${root.cliphistBinary} decode | wl-copy && wl-paste`]);
|
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 {
|
function refresh(): void {
|
||||||
readProc.buffer = [];
|
readProc.buffer = [];
|
||||||
readProc.running = true;
|
readProc.running = true;
|
||||||
@@ -182,7 +98,7 @@ Singleton {
|
|||||||
const token = previewToken;
|
const token = previewToken;
|
||||||
|
|
||||||
if (!currentEntry) {
|
if (!currentEntry) {
|
||||||
previewText = "";
|
previewText = [];
|
||||||
previewImageSource = "";
|
previewImageSource = "";
|
||||||
previewIsImage = false;
|
previewIsImage = false;
|
||||||
return;
|
return;
|
||||||
@@ -269,7 +185,7 @@ Singleton {
|
|||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
if (previewTextProc.token !== root.previewToken)
|
if (previewTextProc.token !== root.previewToken)
|
||||||
return;
|
return;
|
||||||
root.previewText = this.text;
|
root.previewText = root.processPreviewLines(this.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,21 @@ Singleton {
|
|||||||
readonly property real uploadSpeed: _uploadSpeed
|
readonly property real uploadSpeed: _uploadSpeed
|
||||||
readonly property real uploadTotal: _uploadTotal
|
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 {
|
function formatBytes(bytes: real): var {
|
||||||
// Handle negative or invalid values
|
// Handle negative or invalid values
|
||||||
if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {
|
if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {
|
||||||
@@ -156,10 +171,15 @@ Singleton {
|
|||||||
Timer {
|
Timer {
|
||||||
interval: Config.dashboard.resourceUpdateInterval
|
interval: Config.dashboard.resourceUpdateInterval
|
||||||
repeat: true
|
repeat: true
|
||||||
running: root.refCount > 0
|
running: true
|
||||||
triggeredOnStart: true
|
triggeredOnStart: true
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
|
if (root.refCount <= 0) {
|
||||||
|
root.resetSampling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
netDevFile.reload();
|
netDevFile.reload();
|
||||||
const content = netDevFile.text();
|
const content = netDevFile.text();
|
||||||
if (!content)
|
if (!content)
|
||||||
|
|||||||
@@ -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
@@ -32,33 +32,26 @@ Searcher {
|
|||||||
showPreview = true;
|
showPreview = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCrop(screen: string, rect: rect, scaledRect: rect, zoom: real): void {
|
function setCrop(screen: string, rect: rect, zoom: real): void {
|
||||||
let updated = Object.assign({}, root.crops);
|
|
||||||
|
|
||||||
if (zoom <= 0)
|
if (zoom <= 0)
|
||||||
zoom = 1.0;
|
zoom = 1.0;
|
||||||
else if (zoom > 5.0)
|
else if (zoom > 5.0)
|
||||||
zoom = 5.0;
|
zoom = 5.0;
|
||||||
|
|
||||||
updated[screen] = {
|
root.crops[screen] = {
|
||||||
x: rect.x,
|
x: rect.x,
|
||||||
y: rect.y,
|
y: rect.y,
|
||||||
width: rect.width,
|
width: rect.width,
|
||||||
height: rect.height,
|
height: rect.height,
|
||||||
scaledX: scaledRect.x,
|
|
||||||
scaledY: scaledRect.y,
|
|
||||||
scaledWidth: scaledRect.width,
|
|
||||||
scaledHeight: scaledRect.height,
|
|
||||||
zoom: zoom
|
zoom: zoom
|
||||||
};
|
};
|
||||||
|
// root.crops = updated;
|
||||||
root.crops = updated;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWallpaper(path: string): void {
|
function setWallpaper(path: string): void {
|
||||||
actualCurrent = path;
|
actualCurrent = path;
|
||||||
WallpaperPath.currentWallpaperPath = 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}`]);
|
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)
|
if (Config.general.color.schemeGeneration)
|
||||||
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]);
|
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`
|
path: `${Paths.state}/wallpaper-crops.json`
|
||||||
watchChanges: true
|
watchChanges: true
|
||||||
|
|
||||||
onAdapterUpdated: writeAdapter()
|
onAdapterUpdated: cropWriteDelay.restart()
|
||||||
onFileChanged: reload()
|
onFileChanged: reload()
|
||||||
|
|
||||||
JsonAdapter {
|
JsonAdapter {
|
||||||
@@ -101,6 +94,16 @@ Searcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: cropWriteDelay
|
||||||
|
|
||||||
|
interval: 100
|
||||||
|
repeat: false
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onTriggered: monitorCrops.writeAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
FileSystemModel {
|
FileSystemModel {
|
||||||
id: wallpapers
|
id: wallpapers
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ RowLayout {
|
|||||||
required property Wrapper popouts
|
required property Wrapper popouts
|
||||||
required property ClipWrapper popoutsWrapper
|
required property ClipWrapper popoutsWrapper
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
|
required property bool fullscreen
|
||||||
readonly property int vPadding: 6
|
readonly property int vPadding: 6
|
||||||
required property PersistentProperties visibilities
|
required property PersistentProperties visibilities
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ RowLayout {
|
|||||||
|
|
||||||
delegate: WrappedLoader {
|
delegate: WrappedLoader {
|
||||||
sourceComponent: HyprsunsetWidget {
|
sourceComponent: HyprsunsetWidget {
|
||||||
|
visible: !root.fullscreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,6 +111,7 @@ RowLayout {
|
|||||||
delegate: WrappedLoader {
|
delegate: WrappedLoader {
|
||||||
sourceComponent: Workspaces {
|
sourceComponent: Workspaces {
|
||||||
screen: root.screen
|
screen: root.screen
|
||||||
|
visible: !root.fullscreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,6 +123,7 @@ RowLayout {
|
|||||||
sourceComponent: TrayWidget {
|
sourceComponent: TrayWidget {
|
||||||
loader: root
|
loader: root
|
||||||
popouts: root.popouts
|
popouts: root.popouts
|
||||||
|
visible: !root.fullscreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,6 +134,7 @@ RowLayout {
|
|||||||
delegate: WrappedLoader {
|
delegate: WrappedLoader {
|
||||||
sourceComponent: Resources {
|
sourceComponent: Resources {
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
|
visible: !root.fullscreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,6 +144,7 @@ RowLayout {
|
|||||||
|
|
||||||
delegate: WrappedLoader {
|
delegate: WrappedLoader {
|
||||||
sourceComponent: UpdatesWidget {
|
sourceComponent: UpdatesWidget {
|
||||||
|
visible: !root.fullscreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,6 +156,7 @@ RowLayout {
|
|||||||
sourceComponent: NotifBell {
|
sourceComponent: NotifBell {
|
||||||
popouts: root.popouts
|
popouts: root.popouts
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
|
visible: !root.fullscreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,6 +169,7 @@ RowLayout {
|
|||||||
loader: root
|
loader: root
|
||||||
popouts: root.popouts
|
popouts: root.popouts
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
|
visible: !root.fullscreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,6 +180,7 @@ RowLayout {
|
|||||||
delegate: WrappedLoader {
|
delegate: WrappedLoader {
|
||||||
sourceComponent: WindowTitle {
|
sourceComponent: WindowTitle {
|
||||||
bar: root
|
bar: root
|
||||||
|
visible: !root.fullscreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,6 +199,7 @@ RowLayout {
|
|||||||
|
|
||||||
delegate: WrappedLoader {
|
delegate: WrappedLoader {
|
||||||
sourceComponent: MediaWidget {
|
sourceComponent: MediaWidget {
|
||||||
|
visible: !root.fullscreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ Item {
|
|||||||
|
|
||||||
readonly property int contentHeight: Config.barConfig.height + padding * 2
|
readonly property int contentHeight: Config.barConfig.height + padding * 2
|
||||||
readonly property int exclusiveZone: Config.barConfig.autoHide ? Config.barConfig.border : contentHeight
|
readonly property int exclusiveZone: Config.barConfig.autoHide ? Config.barConfig.border : contentHeight
|
||||||
|
required property bool fullscreen
|
||||||
property bool isHovered
|
property bool isHovered
|
||||||
readonly property int padding: Math.max(Appearance.padding.smaller, Config.barConfig.border)
|
readonly property int padding: Math.max(Appearance.padding.smaller, Config.barConfig.border)
|
||||||
required property Wrapper popouts
|
required property Wrapper popouts
|
||||||
required property ClipWrapper popoutsWrapper
|
required property ClipWrapper popoutsWrapper
|
||||||
required property ShellScreen screen
|
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
|
readonly property int vPadding: 6
|
||||||
required property PersistentProperties visibilities
|
required property PersistentProperties visibilities
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ Item {
|
|||||||
content.item?.checkPopout(x);
|
content.item?.checkPopout(x);
|
||||||
}
|
}
|
||||||
|
|
||||||
implicitHeight: Config.barConfig.border
|
implicitHeight: fullscreen ? 0 : Config.barConfig.border
|
||||||
visible: height > Config.barConfig.border
|
visible: height > Config.barConfig.border
|
||||||
|
|
||||||
states: State {
|
states: State {
|
||||||
@@ -75,6 +76,7 @@ Item {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
|
||||||
sourceComponent: Bar {
|
sourceComponent: Bar {
|
||||||
|
fullscreen: root.fullscreen
|
||||||
height: root.contentHeight
|
height: root.contentHeight
|
||||||
popouts: root.popouts
|
popouts: root.popouts
|
||||||
popoutsWrapper: root.popoutsWrapper
|
popoutsWrapper: root.popoutsWrapper
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import qs.Config
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
required property real borderThickness
|
||||||
readonly property alias content: content
|
readonly property alias content: content
|
||||||
property real offsetScale: y > 0 || content.hasCurrent ? 0 : 1
|
property real offsetScale: y > 0 || content.hasCurrent ? 0 : 1
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
@@ -17,7 +18,7 @@ Item {
|
|||||||
implicitWidth: content.implicitWidth
|
implicitWidth: content.implicitWidth
|
||||||
visible: width > 0 && height > 0
|
visible: width > 0 && height > 0
|
||||||
x: {
|
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);
|
const diff = parent.width - Math.floor(off + content.nonAnimWidth);
|
||||||
if (diff < 0)
|
if (diff < 0)
|
||||||
return off + diff;
|
return off + diff;
|
||||||
|
|||||||
+172
-92
@@ -71,7 +71,7 @@ Item {
|
|||||||
anchors.leftMargin: Appearance.spacing.normal
|
anchors.leftMargin: Appearance.spacing.normal
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
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
|
radius: 25
|
||||||
|
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
@@ -79,25 +79,111 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
Column {
|
||||||
id: textPreview
|
id: textPreviewColumn
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.margins: Appearance.padding.large
|
anchors.leftMargin: 0
|
||||||
|
anchors.margins: Appearance.padding.normal
|
||||||
anchors.top: parent.top
|
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
|
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 {
|
Image {
|
||||||
id: imagePreview
|
id: imagePreview
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.centerIn: parent
|
||||||
anchors.margins: Appearance.padding.large
|
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
cache: false
|
cache: false
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
@@ -106,6 +192,7 @@ Item {
|
|||||||
smooth: true
|
smooth: true
|
||||||
source: ClipHistory.previewImageSource
|
source: ClipHistory.previewImageSource
|
||||||
visible: ClipHistory.previewIsImage && ClipHistory.previewImageSource !== ""
|
visible: ClipHistory.previewIsImage && ClipHistory.previewImageSource !== ""
|
||||||
|
width: Math.min(sourceSize.width, Config.clipboard.sizes.previewWidth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,111 +220,103 @@ Item {
|
|||||||
flickable: view
|
flickable: view
|
||||||
minimumSize: 0.1
|
minimumSize: 0.1
|
||||||
}
|
}
|
||||||
delegate: RowLayout {
|
delegate: CustomRect {
|
||||||
id: clipItem
|
id: clipItem
|
||||||
|
|
||||||
readonly property bool isImage: ClipHistory.entryIsImage(modelData)
|
readonly property bool isImage: ClipHistory.entryIsImage(modelData)
|
||||||
required property string modelData
|
required property string modelData
|
||||||
|
|
||||||
height: root.itemHeight
|
implicitHeight: root.itemHeight
|
||||||
spacing: Appearance.spacing.small
|
implicitWidth: view.width
|
||||||
width: view.width
|
radius: textLayer.pressed ? (Appearance.rounding.small / 2) : Appearance.rounding.small
|
||||||
|
|
||||||
CustomClippingRect {
|
Behavior on radius {
|
||||||
id: textRect
|
Anim {
|
||||||
|
type: Anim.FastEffects
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
RowLayout {
|
||||||
id: textWrapper
|
anchors.fill: parent
|
||||||
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
anchors.fill: parent
|
CustomClippingRect {
|
||||||
layer.enabled: true
|
id: textRect
|
||||||
|
|
||||||
layer.effect: OpacityMask {
|
Layout.fillHeight: true
|
||||||
maskSource: fadeMask
|
Layout.fillWidth: true
|
||||||
}
|
|
||||||
|
|
||||||
MaterialIcon {
|
Item {
|
||||||
id: icon
|
id: textWrapper
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.fill: parent
|
||||||
anchors.margins: Appearance.padding.normal
|
layer.enabled: true
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
text: clipItem.isImage ? "image" : "text_fields"
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
layer.effect: OpacityMask {
|
||||||
id: text
|
maskSource: fadeMask
|
||||||
|
|
||||||
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 {
|
MaterialIcon {
|
||||||
color: Qt.rgba(1, 1, 1, 0)
|
id: icon
|
||||||
position: 1.0
|
|
||||||
|
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 {
|
IconButton {
|
||||||
id: textLayer
|
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 {
|
StateLayer {
|
||||||
Layout.bottomMargin: Appearance.padding.normal
|
id: textLayer
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.preferredWidth: height
|
|
||||||
Layout.topMargin: Appearance.padding.normal
|
|
||||||
icon: "content_copy"
|
|
||||||
isToggle: false
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton {
|
onClicked: ClipHistory.copy(clipItem.modelData)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
highlight: CustomRect {
|
highlight: CustomRect {
|
||||||
@@ -266,6 +345,7 @@ Item {
|
|||||||
ClipHistory.currentEntry = currentItem.modelData;
|
ClipHistory.currentEntry = currentItem.modelData;
|
||||||
ClipHistory.refreshPreview();
|
ClipHistory.refreshPreview();
|
||||||
}
|
}
|
||||||
|
onVisibleChanged: currentIndex = 0
|
||||||
|
|
||||||
CustomClippingWrapperRect {
|
CustomClippingWrapperRect {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Item {
|
|||||||
property int contentHeight
|
property int contentHeight
|
||||||
property real offsetScale: shouldBeActive ? 0 : 1
|
property real offsetScale: shouldBeActive ? 0 : 1
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
readonly property bool shouldBeActive: visibilities.clipboard
|
readonly property bool shouldBeActive: visibilities.clipboard && Config.clipboard.enabled
|
||||||
required property PersistentProperties visibilities
|
required property PersistentProperties visibilities
|
||||||
|
|
||||||
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
|
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
|
||||||
@@ -32,7 +32,6 @@ Item {
|
|||||||
|
|
||||||
active: root.shouldBeActive || root.visible
|
active: root.shouldBeActive || root.visible
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
screen: root.screen
|
screen: root.screen
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import qs.Components
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
required property PersistentProperties dashState
|
||||||
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2
|
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2
|
||||||
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
|
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
|
||||||
required property PersistentProperties state
|
|
||||||
required property PersistentProperties visibilities
|
required property PersistentProperties visibilities
|
||||||
|
|
||||||
implicitHeight: nonAnimHeight
|
implicitHeight: nonAnimHeight
|
||||||
@@ -59,7 +59,7 @@ Item {
|
|||||||
index: 0
|
index: 0
|
||||||
|
|
||||||
sourceComponent: Dash {
|
sourceComponent: Dash {
|
||||||
state: root.state
|
dashState: root.dashState
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import qs.Modules.Dashboard.Dash
|
|||||||
GridLayout {
|
GridLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
required property PersistentProperties dashState
|
||||||
readonly property bool dashboardVisible: visibilities.dashboard
|
readonly property bool dashboardVisible: visibilities.dashboard
|
||||||
property int radius: Appearance.rounding.smallest
|
property int radius: Appearance.rounding.smallest
|
||||||
required property PersistentProperties state
|
|
||||||
required property PersistentProperties visibilities
|
required property PersistentProperties visibilities
|
||||||
|
|
||||||
columnSpacing: Appearance.spacing.smaller
|
columnSpacing: Appearance.spacing.smaller
|
||||||
@@ -60,7 +60,7 @@ GridLayout {
|
|||||||
User {
|
User {
|
||||||
id: user
|
id: user
|
||||||
|
|
||||||
state: root.state
|
dashState: root.dashState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ GridLayout {
|
|||||||
Calendar {
|
Calendar {
|
||||||
id: calendar
|
id: calendar
|
||||||
|
|
||||||
state: root.state
|
dashState: root.dashState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+290
-146
@@ -1,25 +1,50 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Helpers
|
|
||||||
import qs.Config
|
import qs.Config
|
||||||
import qs.Modules
|
|
||||||
|
|
||||||
CustomMouseArea {
|
CustomMouseArea {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property int currMonth: state.currentDate.getMonth()
|
property int activeGrid: 1
|
||||||
readonly property int currYear: state.currentDate.getFullYear()
|
required property PersistentProperties dashState
|
||||||
required property var state
|
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 {
|
function onWheel(event: WheelEvent): void {
|
||||||
if (event.angleDelta.y > 0)
|
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)
|
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
|
acceptedButtons: Qt.MiddleButton
|
||||||
@@ -27,102 +52,160 @@ CustomMouseArea {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
implicitHeight: inner.implicitHeight + inner.anchors.margins * 2
|
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 {
|
ColumnLayout {
|
||||||
id: inner
|
id: inner
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Appearance.padding.large
|
anchors.margins: Appearance.padding.large
|
||||||
spacing: Appearance.spacing.small
|
spacing: Appearance.spacing.extraSmall
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: monthNavigationRow
|
id: monthNavigationRow
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Appearance.spacing.small
|
spacing: Appearance.spacing.extraSmall
|
||||||
|
|
||||||
Item {
|
IconButton {
|
||||||
implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2
|
icon: "chevron_left"
|
||||||
implicitWidth: implicitHeight
|
padding: Appearance.padding.small
|
||||||
|
type: IconButton.Text
|
||||||
|
|
||||||
StateLayer {
|
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1)
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2
|
implicitHeight: monthYearDisplay1.implicitHeight + Appearance.padding.extraSmall * 2
|
||||||
implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2
|
implicitWidth: monthYearDisplay1.implicitWidth + Appearance.padding.large * 2
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
anchors.fill: monthYearDisplay
|
color: DynamicColors.palette.m3primary
|
||||||
anchors.leftMargin: -Appearance.padding.normal
|
|
||||||
anchors.margins: -Appearance.padding.small
|
|
||||||
anchors.rightMargin: -Appearance.padding.normal
|
|
||||||
enabled: {
|
enabled: {
|
||||||
const now = new Date();
|
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: {
|
Behavior on radius {
|
||||||
root.state.currentDate = new Date();
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClicked: root.dashState.currentDate = new Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
Item {
|
||||||
id: monthYearDisplay
|
id: titleClip
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.fill: parent
|
||||||
color: DynamicColors.palette.m3primary
|
clip: true
|
||||||
font.capitalization: Font.Capitalize
|
|
||||||
font.pointSize: Appearance.font.size.normal
|
CustomText {
|
||||||
font.weight: 500
|
id: monthYearDisplay1
|
||||||
text: grid.title
|
|
||||||
|
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 {
|
IconButton {
|
||||||
implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2
|
icon: "chevron_right"
|
||||||
implicitWidth: implicitHeight
|
padding: Appearance.padding.small
|
||||||
|
type: IconButton.Text
|
||||||
|
|
||||||
StateLayer {
|
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1)
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,109 +213,170 @@ CustomMouseArea {
|
|||||||
id: daysRow
|
id: daysRow
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
locale: grid.locale
|
locale: Qt.locale()
|
||||||
|
|
||||||
delegate: CustomText {
|
delegate: CustomText {
|
||||||
required property var model
|
required property var model
|
||||||
|
|
||||||
color: (model.day === 0) ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurfaceVariant
|
color: (model.day === 0) ? DynamicColors.palette.m3tertiary : DynamicColors.palette.m3onSurface
|
||||||
font.weight: 500
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
text: model.shortName
|
text: Qt.locale("en_US").standaloneDayName(model.day, Locale.ShortFormat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
id: gridClip
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: grid.implicitHeight
|
clip: true
|
||||||
|
implicitHeight: grid1.implicitHeight
|
||||||
|
|
||||||
MonthGrid {
|
Component {
|
||||||
id: grid
|
id: gridComp
|
||||||
|
|
||||||
anchors.fill: parent
|
Item {
|
||||||
locale: Qt.locale("en_SE")
|
id: internalGridContainer
|
||||||
month: root.currMonth
|
|
||||||
spacing: 3
|
|
||||||
year: root.currYear
|
|
||||||
|
|
||||||
delegate: Item {
|
property int month
|
||||||
id: dayItem
|
property alias title: internalGrid.title
|
||||||
|
property int year
|
||||||
|
|
||||||
required property var model
|
implicitHeight: internalGrid.implicitHeight
|
||||||
|
|
||||||
implicitHeight: text.implicitHeight + Appearance.padding.small * 2
|
MonthGrid {
|
||||||
implicitWidth: implicitHeight
|
id: internalGrid
|
||||||
|
|
||||||
CustomText {
|
anchors.fill: parent
|
||||||
id: text
|
locale: daysRow.locale
|
||||||
|
month: internalGridContainer.month
|
||||||
|
spacing: 3
|
||||||
|
title: `${Qt.locale("en_US").standaloneMonthName(month, Locale.LongFormat)} ${year}`
|
||||||
|
year: internalGridContainer.year
|
||||||
|
|
||||||
anchors.centerIn: parent
|
delegate: Item {
|
||||||
color: {
|
id: dayItem
|
||||||
const dayOfWeek = dayItem.model.date.getUTCDay();
|
|
||||||
if (dayOfWeek === 6)
|
|
||||||
return DynamicColors.palette.m3secondary;
|
|
||||||
|
|
||||||
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 {
|
Loader {
|
||||||
id: todayIndicator
|
id: grid1
|
||||||
|
|
||||||
property Item today
|
anchors.fill: parent
|
||||||
readonly property Item todayItem: grid.contentItem.children.find(c => c.model.today) ?? null
|
sourceComponent: gridComp
|
||||||
|
visible: root.activeGrid === 1 || monthChangeAnim.running
|
||||||
|
|
||||||
clip: true
|
transform: Translate {
|
||||||
color: DynamicColors.palette.m3primary
|
id: grid1Translate
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onTodayItemChanged: {
|
Binding {
|
||||||
if (todayItem)
|
property: "month"
|
||||||
today = todayItem;
|
target: grid1.item
|
||||||
|
value: root.month1
|
||||||
}
|
}
|
||||||
|
|
||||||
Coloriser {
|
Binding {
|
||||||
colorizationColor: DynamicColors.palette.m3onPrimary
|
property: "year"
|
||||||
implicitHeight: grid.height
|
target: grid1.item
|
||||||
implicitWidth: grid.width
|
value: root.year1
|
||||||
source: grid
|
}
|
||||||
sourceColor: DynamicColors.palette.m3onSurface
|
}
|
||||||
x: -todayIndicator.x
|
|
||||||
y: -todayIndicator.y
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Quickshell
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Shapes
|
import QtQuick.Shapes
|
||||||
|
import QtQuick.Effects
|
||||||
import ZShell.Services
|
import ZShell.Services
|
||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
import qs.Components
|
import qs.Components
|
||||||
@@ -45,49 +46,85 @@ Item {
|
|||||||
Shape {
|
Shape {
|
||||||
id: visualizer
|
id: visualizer
|
||||||
|
|
||||||
readonly property real barW: Math.max(0, (width - gap * (bars - 1)) / bars)
|
|
||||||
readonly property int bars: Config.services.visualizerBars
|
readonly property int bars: Config.services.visualizerBars
|
||||||
property color color: DynamicColors.palette.m3primary
|
property color color: DynamicColors.palette.m3tertiary
|
||||||
readonly property real gap: Appearance.spacing.small
|
property color fillColor: Qt.alpha(color, 0.25)
|
||||||
|
|
||||||
anchors.fill: layout
|
anchors.fill: layout
|
||||||
|
anchors.leftMargin: -(shape.strokeWidth / 2)
|
||||||
|
layer.enabled: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
data: visualizerBars.instances
|
|
||||||
preferredRendererType: Shape.CurveRenderer
|
preferredRendererType: Shape.CurveRenderer
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
id: visualizerBars
|
|
||||||
|
|
||||||
model: Array.from({
|
|
||||||
length: Config.services.visualizerBars
|
|
||||||
}, (_, i) => i)
|
|
||||||
|
|
||||||
ShapePath {
|
ShapePath {
|
||||||
id: visualizerBar
|
id: shape
|
||||||
|
|
||||||
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
|
|
||||||
strokeColor: visualizer.color
|
strokeColor: visualizer.color
|
||||||
strokeWidth: visualizer.barW
|
fillColor: visualizer.fillColor
|
||||||
|
strokeWidth: 2
|
||||||
|
|
||||||
Behavior on strokeColor {
|
PathSvg {
|
||||||
CAnim {
|
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 {
|
function y(i) {
|
||||||
relativeX: 0
|
return h - values[i] * Config.dashboard.sizes.mediaVisualizerSize;
|
||||||
relativeY: -visualizerBar.magnitude
|
}
|
||||||
|
|
||||||
|
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 {
|
Shape {
|
||||||
preferredRendererType: Shape.CurveRenderer
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
|
||||||
|
|||||||
@@ -1,92 +1,89 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ZShell.Services
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
|
||||||
Row {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
padding: Appearance.padding.large
|
implicitWidth: layout.implicitWidth + layout.anchors.margins * 4
|
||||||
spacing: Appearance.spacing.large
|
|
||||||
|
|
||||||
Ref {
|
ServiceRef {
|
||||||
service: SystemUsage
|
service: Storage
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
ServiceRef {
|
||||||
color: DynamicColors.palette.m3primary
|
service: Memory
|
||||||
icon: "memory"
|
|
||||||
value: SystemUsage.cpuPerc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
ServiceRef {
|
||||||
color: DynamicColors.palette.m3secondary
|
service: Cpu
|
||||||
icon: "memory_alt"
|
|
||||||
value: SystemUsage.memPerc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
ServiceRef {
|
||||||
color: DynamicColors.palette.m3tertiary
|
service: Gpu
|
||||||
icon: "gamepad"
|
|
||||||
value: SystemUsage.gpuPerc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
ColumnLayout {
|
||||||
color: DynamicColors.palette.m3primary
|
id: layout
|
||||||
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
|
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.margins: Appearance.padding.large
|
anchors.margins: Appearance.padding.large
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
implicitWidth: icon.implicitWidth
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
Behavior on value {
|
Resource {
|
||||||
Anim {
|
fgColor: DynamicColors.palette.m3primary
|
||||||
duration: Appearance.anim.durations.large
|
icon: "memory"
|
||||||
}
|
value: Cpu.percentage
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomRect {
|
Resource {
|
||||||
anchors.bottom: icon.top
|
fgColor: DynamicColors.palette.m3secondary
|
||||||
anchors.bottomMargin: Appearance.spacing.small
|
icon: "memory_alt"
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
value: Memory.percentage
|
||||||
anchors.top: parent.top
|
}
|
||||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
|
||||||
implicitWidth: Config.dashboard.sizes.resourceProgessThickness
|
|
||||||
radius: Appearance.rounding.full
|
|
||||||
|
|
||||||
CustomRect {
|
Resource {
|
||||||
anchors.bottom: parent.bottom
|
fgColor: DynamicColors.palette.m3tertiary
|
||||||
anchors.left: parent.left
|
icon: "gamepad"
|
||||||
anchors.right: parent.right
|
value: Gpu.percentage
|
||||||
color: res.color
|
}
|
||||||
implicitHeight: res.value * parent.height
|
|
||||||
radius: Appearance.rounding.full
|
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 {
|
MaterialIcon {
|
||||||
id: icon
|
anchors.centerIn: parent
|
||||||
|
color: res.fgColor
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
color: res.color
|
|
||||||
text: res.icon
|
text: res.icon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import QtQuick
|
|||||||
Row {
|
Row {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property PersistentProperties state
|
required property PersistentProperties dashState
|
||||||
|
|
||||||
padding: 20
|
padding: 20
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ Item {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
state: root.dashState
|
dashState: root.dashState
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+102
-46
@@ -1,33 +1,35 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import ZShell.Components
|
||||||
import qs.Config
|
import qs.Config
|
||||||
import qs.Components
|
import qs.Components
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property var colors: ["#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#a855f7", "#ec4899", "#ffffff", "#000000"]
|
readonly property var colors1: ["#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4"]
|
||||||
required property Canvas drawing
|
readonly property var colors2: ["#3b82f6", "#a855f7", "#ec4899", "#ffffff", "#000000"]
|
||||||
|
required property var drawing
|
||||||
required property var visibilities
|
required property var visibilities
|
||||||
|
required property Wrapper wrapper
|
||||||
|
|
||||||
function syncFromPenColor() {
|
function syncFromPenColor() {
|
||||||
if (!drawing)
|
if (!drawing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!saturationSlider.pressed)
|
if (!saturationSlider.pressed)
|
||||||
saturationSlider.value = drawing.penColor.hsvSaturation;
|
saturationSlider.value = drawing.drawingState.penColor.hsvSaturation;
|
||||||
|
|
||||||
if (!brightnessSlider.pressed)
|
if (!brightnessSlider.pressed)
|
||||||
brightnessSlider.value = drawing.penColor.hsvValue;
|
brightnessSlider.value = drawing.drawingState.penColor.hsvValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePenColorFromHsv() {
|
function updatePenColorFromHsv() {
|
||||||
if (!drawing)
|
if (!drawing)
|
||||||
return;
|
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
|
implicitHeight: column.height + Appearance.padding.larger * 2
|
||||||
@@ -40,7 +42,7 @@ Item {
|
|||||||
root.syncFromPenColor();
|
root.syncFromPenColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
target: root.drawing
|
target: root.drawing.drawingState
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -58,13 +60,14 @@ Item {
|
|||||||
GradientSlider {
|
GradientSlider {
|
||||||
id: saturationSlider
|
id: saturationSlider
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
brightness: brightnessSlider.value
|
brightness: brightnessSlider.value
|
||||||
channel: "saturation"
|
channel: "saturation"
|
||||||
from: 0
|
from: 0
|
||||||
hue: huePicker.currentHue
|
hue: huePicker.currentHue
|
||||||
icon: "\ue40a"
|
icon: "\ue40a"
|
||||||
implicitHeight: 30
|
implicitHeight: 30
|
||||||
implicitWidth: palette.width
|
|
||||||
orientation: Qt.Horizontal
|
orientation: Qt.Horizontal
|
||||||
to: 1
|
to: 1
|
||||||
|
|
||||||
@@ -74,12 +77,13 @@ Item {
|
|||||||
GradientSlider {
|
GradientSlider {
|
||||||
id: brightnessSlider
|
id: brightnessSlider
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
channel: "brightness"
|
channel: "brightness"
|
||||||
from: 0
|
from: 0
|
||||||
hue: huePicker.currentHue
|
hue: huePicker.currentHue
|
||||||
icon: "\ue1ac"
|
icon: "\ue1ac"
|
||||||
implicitHeight: 30
|
implicitHeight: 30
|
||||||
implicitWidth: palette.width
|
|
||||||
orientation: Qt.Horizontal
|
orientation: Qt.Horizontal
|
||||||
saturation: saturationSlider.value
|
saturation: saturationSlider.value
|
||||||
to: 1
|
to: 1
|
||||||
@@ -87,65 +91,117 @@ Item {
|
|||||||
onMoved: root.updatePenColorFromHsv()
|
onMoved: root.updatePenColorFromHsv()
|
||||||
}
|
}
|
||||||
|
|
||||||
GridLayout {
|
ButtonRow {
|
||||||
id: palette
|
id: row1
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
columns: 5
|
spacing: Appearance.spacing.normal
|
||||||
rowSpacing: 8
|
|
||||||
rows: 2
|
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.colors
|
model: root.colors1
|
||||||
|
|
||||||
delegate: Item {
|
delegate: ColorButton {
|
||||||
id: colorCircle
|
row: row1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
required property color modelData
|
ButtonRow {
|
||||||
readonly property bool selected: Qt.colorEqual(root.drawing.penColor, modelData)
|
id: row2
|
||||||
|
|
||||||
Layout.fillWidth: true
|
anchors.left: parent.left
|
||||||
height: 28
|
anchors.right: parent.right
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
CustomRect {
|
Repeater {
|
||||||
anchors.centerIn: parent
|
model: root.colors2
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
delegate: ColorButton {
|
||||||
anchors.centerIn: parent
|
row: row2
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FilledSlider {
|
FilledSlider {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
from: 1
|
from: 1
|
||||||
icon: "border_color"
|
icon: "border_color"
|
||||||
implicitHeight: 30
|
implicitHeight: 30
|
||||||
implicitWidth: palette.width
|
|
||||||
multiplier: 1
|
multiplier: 1
|
||||||
orientation: Qt.Horizontal
|
orientation: Qt.Horizontal
|
||||||
to: 45
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,36 @@ import qs.Daemons
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property Canvas drawing
|
required property var drawing
|
||||||
property bool expanded: true
|
property bool expanded: true
|
||||||
property real offsetScale: shouldBeActive ? 0 : 1
|
property real offsetScale: shouldBeActive ? 0 : 1
|
||||||
|
property bool pinned: false
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
readonly property bool shouldBeActive: visibilities.isDrawing
|
readonly property bool shouldBeActive: visibilities.isDrawing
|
||||||
required property var visibilities
|
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
|
anchors.leftMargin: (-implicitWidth - 5) * offsetScale
|
||||||
implicitHeight: content.implicitHeight
|
implicitHeight: content.implicitHeight
|
||||||
implicitWidth: root.expanded ? content.implicitWidth : icon.implicitWidth
|
implicitWidth: root.expanded ? content.implicitWidth : icon.implicitWidth
|
||||||
@@ -44,7 +67,7 @@ Item {
|
|||||||
Loader {
|
Loader {
|
||||||
id: icon
|
id: icon
|
||||||
|
|
||||||
active: root.shouldBeActive || root.visible
|
active: (root.shouldBeActive || root.visible) && opacity > 0
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
@@ -63,7 +86,7 @@ Item {
|
|||||||
Loader {
|
Loader {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
active: root.shouldBeActive || root.visible
|
active: (root.shouldBeActive || root.visible) && opacity > 0
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
@@ -76,6 +99,7 @@ Item {
|
|||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
drawing: root.drawing
|
drawing: root.drawing
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
|
wrapper: root
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ CustomRect {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
animate: true
|
animate: true
|
||||||
color: root.tempEnabled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
color: root.tempEnabled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||||
|
fill: root.tempEnabled ? 1 : 0
|
||||||
text: root.tempEnabled ? "lightbulb" : "light_off"
|
text: root.tempEnabled ? "lightbulb" : "light_off"
|
||||||
|
|
||||||
|
Behavior on fill {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-11
@@ -1,6 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import qs.Modules
|
import ZShell.Services
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Config
|
import qs.Config
|
||||||
@@ -16,30 +16,34 @@ GridLayout {
|
|||||||
rowSpacing: Appearance.spacing.large
|
rowSpacing: Appearance.spacing.large
|
||||||
rows: 1
|
rows: 1
|
||||||
|
|
||||||
Ref {
|
ServiceRef {
|
||||||
service: SystemUsage
|
service: Memory
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceRef {
|
||||||
|
service: Cpu
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
Resource {
|
||||||
Layout.bottomMargin: Appearance.padding.large
|
Layout.bottomMargin: Appearance.padding.large
|
||||||
Layout.topMargin: Appearance.padding.large
|
Layout.topMargin: Appearance.padding.large
|
||||||
colour: DynamicColors.palette.m3primary
|
fgColor: DynamicColors.palette.m3primary
|
||||||
icon: "memory"
|
icon: "memory"
|
||||||
value: SystemUsage.cpuPerc
|
value: Cpu.percentage
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
Resource {
|
||||||
Layout.bottomMargin: Appearance.padding.large
|
Layout.bottomMargin: Appearance.padding.large
|
||||||
Layout.topMargin: Appearance.padding.large
|
Layout.topMargin: Appearance.padding.large
|
||||||
colour: DynamicColors.palette.m3secondary
|
fgColor: DynamicColors.palette.m3secondary
|
||||||
icon: "memory_alt"
|
icon: "memory_alt"
|
||||||
value: SystemUsage.memPerc
|
value: Memory.percentage
|
||||||
}
|
}
|
||||||
|
|
||||||
component Resource: CustomRect {
|
component Resource: CustomRect {
|
||||||
id: res
|
id: res
|
||||||
|
|
||||||
required property color colour
|
required property color fgColor
|
||||||
required property string icon
|
required property string icon
|
||||||
required property real value
|
required property real value
|
||||||
|
|
||||||
@@ -58,8 +62,8 @@ GridLayout {
|
|||||||
id: circ
|
id: circ
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
bgColour: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3)
|
bgColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3)
|
||||||
fgColour: res.colour
|
fgColor: res.fgColor
|
||||||
padding: Appearance.padding.large * 3
|
padding: Appearance.padding.large * 3
|
||||||
strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
|
strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
|
||||||
value: res.value
|
value: res.value
|
||||||
@@ -69,7 +73,7 @@ GridLayout {
|
|||||||
id: icon
|
id: icon
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: res.colour
|
color: res.fgColor
|
||||||
font.pointSize: (circ.arcRadius * 0.7) || 1
|
font.pointSize: (circ.arcRadius * 0.7) || 1
|
||||||
font.weight: 600
|
font.weight: 600
|
||||||
text: res.icon
|
text: res.icon
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ CustomRect {
|
|||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: root.visibilities.sidebar ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
color: root.visibilities.sidebar ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||||
|
fill: root.visibilities.sidebar ? 1 : 0
|
||||||
font.family: "Material Symbols Rounded"
|
font.family: "Material Symbols Rounded"
|
||||||
font.pointSize: Appearance.font.size.larger
|
font.pointSize: Appearance.font.size.larger
|
||||||
text: HasNotifications.hasNotifications ? "\uf4fe" : "\ue7f4"
|
text: HasNotifications.hasNotifications ? "\uf4fe" : "\ue7f4"
|
||||||
@@ -29,6 +30,10 @@ CustomRect {
|
|||||||
CAnim {
|
CAnim {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Behavior on fill {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Config
|
import qs.Config
|
||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
@@ -20,28 +22,26 @@ Item {
|
|||||||
if (count === 0)
|
if (count === 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
let height = (count - 1) * 8;
|
let height = (count - 1) * Appearance.spacing.small;
|
||||||
for (let i = 0; i < count; i++)
|
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)
|
||||||
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x || visibilities.dashboard)
|
return 0;
|
||||||
return 0;
|
|
||||||
|
|
||||||
if (visibilities.osd) {
|
if (visibilities.osd) {
|
||||||
const h = panels.osd.y - 8 * 2 - padding * 2;
|
const h = panels.osd.y - Appearance.spacing.small * 2 - padding * 2;
|
||||||
if (height > h)
|
if (height > h)
|
||||||
height = h;
|
height = h;
|
||||||
}
|
|
||||||
|
|
||||||
if (visibilities.session) {
|
|
||||||
const h = panels.session.y - 8 * 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
|
implicitWidth: Config.notifs.sizes.width + padding * 2
|
||||||
|
|
||||||
@@ -60,83 +60,11 @@ Item {
|
|||||||
id: list
|
id: list
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cacheBuffer: QsWindow.window?.screen.height ?? 0
|
cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0
|
||||||
orientation: Qt.Vertical
|
orientation: Qt.Vertical
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
delegate: Item {
|
delegate: NotifWrapper {
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
displaced: Transition {
|
displaced: Transition {
|
||||||
Anim {
|
Anim {
|
||||||
@@ -158,4 +86,79 @@ Item {
|
|||||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
easing.bezierCurve: Appearance.anim.curves.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import qs.Components
|
import QtQuick
|
||||||
import qs.Config
|
import QtQuick.Shapes
|
||||||
import qs.Modules
|
import Quickshell
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import ZShell.Components
|
||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import Quickshell
|
import qs.Config
|
||||||
import Quickshell.Widgets
|
import qs.Components
|
||||||
import Quickshell.Services.Notifications
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
id: root
|
id: root
|
||||||
@@ -18,17 +17,16 @@ CustomRect {
|
|||||||
readonly property bool hasAppIcon: modelData.appIcon.length > 0
|
readonly property bool hasAppIcon: modelData.appIcon.length > 0
|
||||||
readonly property bool hasImage: modelData.image.length > 0
|
readonly property bool hasImage: modelData.image.length > 0
|
||||||
required property NotifServer.Notif modelData
|
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
|
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondaryContainer : DynamicColors.tPalette.m3surfaceContainer
|
||||||
implicitHeight: inner.implicitHeight
|
implicitHeight: inner.implicitHeight
|
||||||
implicitWidth: Config.notifs.sizes.width
|
radius: Appearance.rounding.small
|
||||||
radius: 6
|
x: implicitWidth
|
||||||
x: Config.notifs.sizes.width
|
|
||||||
|
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
Anim {
|
Anim {
|
||||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +52,7 @@ CustomRect {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
const actions = root.modelData.actions;
|
const actions = root.modelData.actions;
|
||||||
if (actions?.length === 1)
|
if (actions.length === 1)
|
||||||
actions[0].invoke();
|
actions[0].invoke();
|
||||||
}
|
}
|
||||||
onEntered: root.modelData.timer.stop()
|
onEntered: root.modelData.timer.stop()
|
||||||
@@ -79,7 +77,7 @@ CustomRect {
|
|||||||
if (!containsMouse)
|
if (!containsMouse)
|
||||||
root.modelData.timer.start();
|
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;
|
root.x = 0;
|
||||||
else
|
else
|
||||||
root.modelData.popup = false;
|
root.modelData.popup = false;
|
||||||
@@ -89,15 +87,13 @@ CustomRect {
|
|||||||
id: inner
|
id: inner
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.margins: 8
|
anchors.margins: Appearance.padding.normal
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
implicitHeight: root.nonAnimHeight
|
implicitHeight: root.nonAnimHeight
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
Anim {
|
Anim {
|
||||||
duration: MaterialEasing.expressiveEffectsTime
|
|
||||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +108,8 @@ CustomRect {
|
|||||||
visible: root.hasImage || root.hasAppIcon
|
visible: root.hasImage || root.hasAppIcon
|
||||||
width: Config.notifs.sizes.image
|
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
|
implicitHeight: Config.notifs.sizes.image
|
||||||
implicitWidth: Config.notifs.sizes.image
|
implicitWidth: Config.notifs.sizes.image
|
||||||
radius: Appearance.rounding.full
|
radius: Appearance.rounding.full
|
||||||
@@ -122,8 +119,11 @@ CustomRect {
|
|||||||
asynchronous: true
|
asynchronous: true
|
||||||
cache: false
|
cache: false
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
mipmap: true
|
|
||||||
source: Qt.resolvedUrl(root.modelData.image)
|
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)
|
height: Math.round(parent.width * 0.6)
|
||||||
width: Math.round(parent.width * 0.6)
|
width: Math.round(parent.width * 0.6)
|
||||||
|
|
||||||
sourceComponent: CustomIcon {
|
sourceComponent: ColoredIcon {
|
||||||
anchors.fill: parent
|
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")
|
layer.enabled: root.modelData.appIcon.endsWith("symbolic")
|
||||||
source: Quickshell.iconPath(root.modelData.appIcon)
|
source: Quickshell.iconPath(root.modelData.appIcon)
|
||||||
}
|
}
|
||||||
@@ -163,34 +164,67 @@ CustomRect {
|
|||||||
Loader {
|
Loader {
|
||||||
active: !root.hasAppIcon
|
active: !root.hasAppIcon
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
anchors.horizontalCenterOffset: -18 * 0.02
|
anchors.verticalCenterOffset: 1
|
||||||
anchors.verticalCenterOffset: 18 * 0.02
|
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
|
||||||
sourceComponent: MaterialIcon {
|
sourceComponent: MaterialIcon {
|
||||||
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer
|
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)
|
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 {
|
CustomText {
|
||||||
id: appName
|
id: appName
|
||||||
|
|
||||||
anchors.left: image.right
|
anchors.left: image.right
|
||||||
anchors.leftMargin: 10
|
anchors.leftMargin: Appearance.spacing.small
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
animate: true
|
animate: true
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
font.pointSize: 10
|
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
opacity: root.expanded ? 1 : 0
|
opacity: root.expanded ? 1 : 0
|
||||||
text: appNameMetrics.elidedText
|
text: appNameMetrics.elidedText
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,9 +233,8 @@ CustomRect {
|
|||||||
id: appNameMetrics
|
id: appNameMetrics
|
||||||
|
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3
|
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3
|
||||||
font.family: appName.font.family
|
font: appName.font
|
||||||
font.pointSize: appName.font.pointSize
|
|
||||||
text: root.modelData.appName
|
text: root.modelData.appName
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,9 +242,10 @@ CustomRect {
|
|||||||
id: summary
|
id: summary
|
||||||
|
|
||||||
anchors.left: image.right
|
anchors.left: image.right
|
||||||
anchors.leftMargin: 10
|
anchors.leftMargin: Appearance.spacing.small
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
animate: true
|
animate: true
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
height: implicitHeight
|
height: implicitHeight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
text: summaryMetrics.elidedText
|
text: summaryMetrics.elidedText
|
||||||
@@ -225,6 +259,9 @@ CustomRect {
|
|||||||
when: root.expanded
|
when: root.expanded
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
|
body.anchors.topMargin: Appearance.spacing.extraSmall
|
||||||
|
bodyPreview.anchors.topMargin: Appearance.spacing.extraSmall
|
||||||
|
summary.anchors.topMargin: Appearance.spacing.extraSmall
|
||||||
summary.maximumLineCount: undefined
|
summary.maximumLineCount: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,10 +276,11 @@ CustomRect {
|
|||||||
target: summary
|
target: summary
|
||||||
}
|
}
|
||||||
|
|
||||||
AnchorAnimation {
|
Anim {
|
||||||
duration: MaterialEasing.expressiveEffectsTime
|
property: "topMargin"
|
||||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
}
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
|
AnchorAnim {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,9 +289,8 @@ CustomRect {
|
|||||||
id: summaryMetrics
|
id: summaryMetrics
|
||||||
|
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3
|
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3
|
||||||
font.family: summary.font.family
|
font: summary.font
|
||||||
font.pointSize: summary.font.pointSize
|
|
||||||
text: root.modelData.summary
|
text: root.modelData.summary
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,10 +298,9 @@ CustomRect {
|
|||||||
id: timeSep
|
id: timeSep
|
||||||
|
|
||||||
anchors.left: summary.right
|
anchors.left: summary.right
|
||||||
anchors.leftMargin: 7
|
anchors.leftMargin: Appearance.spacing.small
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
font.pointSize: 10
|
|
||||||
text: "•"
|
text: "•"
|
||||||
|
|
||||||
states: State {
|
states: State {
|
||||||
@@ -277,10 +313,7 @@ CustomRect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
transitions: Transition {
|
transitions: Transition {
|
||||||
AnchorAnimation {
|
AnchorAnim {
|
||||||
duration: MaterialEasing.expressiveEffectsTime
|
|
||||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,11 +322,11 @@ CustomRect {
|
|||||||
id: time
|
id: time
|
||||||
|
|
||||||
anchors.left: timeSep.right
|
anchors.left: timeSep.right
|
||||||
anchors.leftMargin: 7
|
anchors.leftMargin: Appearance.spacing.small
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
animate: true
|
animate: true
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
font.pointSize: 10
|
font.pointSize: Appearance.font.size.small
|
||||||
horizontalAlignment: Text.AlignLeft
|
horizontalAlignment: Text.AlignLeft
|
||||||
text: root.modelData.timeStr
|
text: root.modelData.timeStr
|
||||||
}
|
}
|
||||||
@@ -303,25 +336,33 @@ CustomRect {
|
|||||||
|
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
implicitHeight: expandIcon.height
|
anchors.topMargin: -Appearance.padding.extraSmall
|
||||||
implicitWidth: expandIcon.height
|
implicitHeight: expandIcon.implicitHeight
|
||||||
|
implicitWidth: expandIcon.implicitHeight
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
|
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
|
||||||
radius: Appearance.rounding.full
|
radius: Appearance.rounding.full
|
||||||
|
|
||||||
onClicked: {
|
onClicked: root.expanded = !root.expanded
|
||||||
root.expanded = !root.expanded;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
id: expandIcon
|
id: expandIcon
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
animate: true
|
anchors.verticalCenterOffset: root.expanded ? -1 : 1
|
||||||
font.pointSize: 13
|
rotation: root.expanded ? 180 : 0
|
||||||
text: root.expanded ? "expand_less" : "expand_more"
|
text: "expand_more"
|
||||||
|
|
||||||
|
Behavior on anchors.verticalCenterOffset {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on rotation {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,17 +371,18 @@ CustomRect {
|
|||||||
|
|
||||||
anchors.left: summary.left
|
anchors.left: summary.left
|
||||||
anchors.right: expandBtn.left
|
anchors.right: expandBtn.left
|
||||||
anchors.rightMargin: 7
|
anchors.rightMargin: Appearance.spacing.small
|
||||||
anchors.top: summary.bottom
|
anchors.top: summary.bottom
|
||||||
animate: true
|
animate: true
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
font.pointSize: 10
|
font.pointSize: Appearance.font.size.small
|
||||||
opacity: root.expanded ? 0 : 1
|
opacity: root.expanded ? 0 : 1
|
||||||
text: bodyPreviewMetrics.elidedText
|
text: bodyPreviewMetrics.elidedText
|
||||||
textFormat: Text.MarkdownText
|
textFormat: Text.MarkdownText
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -350,8 +392,7 @@ CustomRect {
|
|||||||
|
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
elideWidth: bodyPreview.width
|
elideWidth: bodyPreview.width
|
||||||
font.family: bodyPreview.font.family
|
font: bodyPreview.font
|
||||||
font.pointSize: bodyPreview.font.pointSize
|
|
||||||
text: root.modelData.body
|
text: root.modelData.body
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,11 +401,10 @@ CustomRect {
|
|||||||
|
|
||||||
anchors.left: summary.left
|
anchors.left: summary.left
|
||||||
anchors.right: expandBtn.left
|
anchors.right: expandBtn.left
|
||||||
anchors.rightMargin: 7
|
anchors.rightMargin: Appearance.spacing.small
|
||||||
anchors.top: summary.bottom
|
anchors.top: summary.bottom
|
||||||
animate: true
|
animate: true
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
font.pointSize: 10
|
|
||||||
height: text ? implicitHeight : 0
|
height: text ? implicitHeight : 0
|
||||||
opacity: root.expanded ? 1 : 0
|
opacity: root.expanded ? 1 : 0
|
||||||
text: root.modelData.body
|
text: root.modelData.body
|
||||||
@@ -373,6 +413,7 @@ CustomRect {
|
|||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,83 +426,89 @@ CustomRect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
ButtonRow {
|
||||||
id: actions
|
id: actions
|
||||||
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
anchors.top: body.bottom
|
anchors.top: body.bottom
|
||||||
anchors.topMargin: 7
|
anchors.topMargin: Appearance.spacing.small
|
||||||
opacity: root.expanded ? 1 : 0
|
opacity: root.expanded ? 1 : 0
|
||||||
spacing: 10
|
spacing: Appearance.spacing.extraSmall
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Action {
|
IconButton {
|
||||||
modelData: QtObject {
|
enabled: root.expanded
|
||||||
readonly property string text: qsTr("Close")
|
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 {
|
onClicked: root.modelData.close()
|
||||||
root.modelData.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.modelData.actions
|
model: root.modelData.actions
|
||||||
|
|
||||||
delegate: Component {
|
TextButton {
|
||||||
Action {
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Item {
|
|||||||
id: layout
|
id: layout
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 8
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
@@ -29,7 +29,7 @@ Item {
|
|||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 8 - layout.spacing
|
Layout.topMargin: Appearance.padding.normal - layout.spacing
|
||||||
color: DynamicColors.tPalette.m3outlineVariant
|
color: DynamicColors.tPalette.m3outlineVariant
|
||||||
implicitHeight: 1
|
implicitHeight: 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Item {
|
|||||||
required property var visibilities
|
required property var visibilities
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 8
|
anchors.margins: Appearance.padding.normal
|
||||||
|
|
||||||
Component.onCompleted: NotifServer.list.forEach(n => n.popup = false)
|
Component.onCompleted: NotifServer.list.forEach(n => n.popup = false)
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ Item {
|
|||||||
Loader {
|
Loader {
|
||||||
active: opacity > 0
|
active: opacity > 0
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.margins: 8
|
anchors.margins: Appearance.padding.normal
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
opacity: root.notifCount > 0 ? 1 : 0
|
opacity: root.notifCount > 0 ? 1 : 0
|
||||||
scale: root.notifCount > 0 ? 1 : 0.5
|
scale: root.notifCount > 0 ? 1 : 0.5
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import Quickshell.Bluetooth
|
|||||||
import Quickshell.Networking as QSNetwork
|
import Quickshell.Networking as QSNetwork
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import ZShell.Components
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Config
|
import qs.Config
|
||||||
import qs.Modules
|
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
|
|
||||||
@@ -19,74 +19,69 @@ CustomRect {
|
|||||||
implicitHeight: layout.implicitHeight + 18 * 2
|
implicitHeight: layout.implicitHeight + 18 * 2
|
||||||
radius: Appearance.rounding.smallest
|
radius: Appearance.rounding.smallest
|
||||||
|
|
||||||
ColumnLayout {
|
ButtonRow {
|
||||||
id: layout
|
id: layout
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 18
|
anchors.margins: 18
|
||||||
spacing: 10
|
spacing: Appearance.spacing.extraSmall
|
||||||
|
|
||||||
RowLayout {
|
Toggle {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
checked: Network.wifiEnabled
|
||||||
spacing: 7
|
icon: Network.wifiEnabled ? "wifi" : "wifi_off"
|
||||||
|
visible: QSNetwork.Networking.devices.values.some(n => n.type === QSNetwork.DeviceType.Wifi)
|
||||||
|
|
||||||
Toggle {
|
onClicked: Network.toggleWifi()
|
||||||
checked: Network.wifiEnabled
|
}
|
||||||
icon: Network.wifiEnabled ? "wifi" : "wifi_off"
|
|
||||||
visible: QSNetwork.Networking.devices.values.some(n => n.type === QSNetwork.DeviceType.Wifi)
|
|
||||||
|
|
||||||
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 {
|
Toggle {
|
||||||
id: toggle
|
checked: !Audio.muted
|
||||||
|
icon: Audio.muted ? "volume_off" : "volume_up"
|
||||||
|
|
||||||
checked: !NotifServer.dnd
|
onClicked: {
|
||||||
icon: NotifServer.dnd ? "notifications_off" : "notifications"
|
const audio = Audio.sink?.audio;
|
||||||
|
if (audio)
|
||||||
onClicked: NotifServer.dnd = !NotifServer.dnd
|
audio.muted = !audio.muted;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Toggle {
|
Toggle {
|
||||||
checked: !Audio.sourceMuted
|
checked: Bluetooth.defaultAdapter?.enabled ?? false
|
||||||
icon: Audio.sourceMuted ? "mic_off" : "mic"
|
icon: Bluetooth.defaultAdapter?.enabled ? "bluetooth" : "bluetooth_disabled"
|
||||||
|
visible: Bluetooth.defaultAdapter ?? false
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
const audio = Audio.source?.audio;
|
const adapter = Bluetooth.defaultAdapter;
|
||||||
if (audio)
|
if (adapter)
|
||||||
audio.muted = !audio.muted;
|
adapter.enabled = !adapter.enabled;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Toggle {
|
Toggle {
|
||||||
checked: !Audio.muted
|
checked: GameMode.enabled
|
||||||
icon: Audio.muted ? "volume_off" : "volume_up"
|
icon: GameMode.enabled ? "videogame_asset" : "videogame_asset_off"
|
||||||
|
|
||||||
onClicked: {
|
onClicked: GameMode.enabled = !GameMode.enabled
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,19 +94,9 @@ CustomRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
component Toggle: IconButton {
|
component Toggle: IconButton {
|
||||||
Layout.fillWidth: true
|
fillWidth: true
|
||||||
Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? 18 : internalChecked ? 7 : 0)
|
isRound: true
|
||||||
inactiveColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
|
|
||||||
isToggle: true
|
isToggle: true
|
||||||
radius: stateLayer.pressed ? 6 / 2 : internalChecked ? 6 : 8
|
shapeMorph: true
|
||||||
radiusAnim.duration: MaterialEasing.expressiveEffectsTime
|
|
||||||
radiusAnim.easing.bezierCurve: MaterialEasing.expressiveEffects
|
|
||||||
|
|
||||||
Behavior on Layout.preferredWidth {
|
|
||||||
Anim {
|
|
||||||
duration: MaterialEasing.expressiveEffectsTime
|
|
||||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Item {
|
|||||||
required property var visibilities
|
required property var visibilities
|
||||||
|
|
||||||
implicitHeight: content.implicitHeight
|
implicitHeight: content.implicitHeight
|
||||||
implicitWidth: Math.max(sidebarPanel.width * (1 - sidebarPanel.offsetScale), content.implicitWidth)
|
implicitWidth: content.implicitWidth
|
||||||
visible: height > 0
|
visible: height > 0
|
||||||
|
|
||||||
Content {
|
Content {
|
||||||
|
|||||||
@@ -97,7 +97,9 @@ Item {
|
|||||||
|
|
||||||
FilledSlider {
|
FilledSlider {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
from: Config.services.minBrightness
|
||||||
icon: `brightness_${(Math.round(value * 6) + 1)}`
|
icon: `brightness_${(Math.round(value * 6) + 1)}`
|
||||||
|
to: 1.0
|
||||||
value: root.brightness
|
value: root.brightness
|
||||||
|
|
||||||
onPressedChanged: {
|
onPressedChanged: {
|
||||||
|
|||||||
+21
-11
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import ZShell.Services
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Modules
|
import qs.Modules
|
||||||
import qs.Config
|
import qs.Config
|
||||||
@@ -16,7 +17,7 @@ CustomRect {
|
|||||||
clip: true
|
clip: true
|
||||||
color: visibilities.resources ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3surfaceContainer
|
color: visibilities.resources ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3surfaceContainer
|
||||||
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
|
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
|
radius: Appearance.rounding.full
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
@@ -29,11 +30,20 @@ CustomRect {
|
|||||||
id: rowLayout
|
id: rowLayout
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
anchors.horizontalCenterOffset: -2
|
||||||
implicitHeight: root.implicitHeight
|
implicitHeight: root.implicitHeight
|
||||||
spacing: Appearance.spacing.smaller
|
spacing: Appearance.spacing.smaller
|
||||||
|
|
||||||
Ref {
|
ServiceRef {
|
||||||
service: SystemUsage
|
service: Gpu
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceRef {
|
||||||
|
service: Cpu
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceRef {
|
||||||
|
service: Memory
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
Resource {
|
||||||
@@ -41,8 +51,8 @@ CustomRect {
|
|||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
icon: "memory"
|
icon: "memory"
|
||||||
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||||
mainColor: root.visibilities.resources ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3primary
|
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3primary
|
||||||
percentage: SystemUsage.cpuPerc
|
percentage: Cpu.percentage
|
||||||
warningThreshold: 95
|
warningThreshold: 95
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +60,8 @@ CustomRect {
|
|||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
icon: "memory_alt"
|
icon: "memory_alt"
|
||||||
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||||
mainColor: root.visibilities.resources ? DynamicColors.palette.m3secondaryContainer : DynamicColors.palette.m3secondary
|
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3secondary
|
||||||
percentage: SystemUsage.memPerc
|
percentage: Memory.percentage
|
||||||
warningThreshold: 80
|
warningThreshold: 80
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,16 +69,16 @@ CustomRect {
|
|||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
icon: "gamepad"
|
icon: "gamepad"
|
||||||
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||||
mainColor: root.visibilities.resources ? DynamicColors.palette.m3tertiaryContainer : DynamicColors.palette.m3tertiary
|
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3tertiary
|
||||||
percentage: SystemUsage.gpuPerc
|
percentage: Gpu.percentage
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
Resource {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
icon: "developer_board"
|
icon: "developer_board"
|
||||||
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||||
mainColor: root.visibilities.resources ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3primary
|
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3primary
|
||||||
percentage: SystemUsage.gpuMemUsed
|
percentage: Gpu.memoryUsed / Gpu.memoryTotal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)}%`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) + "%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -1,42 +1,27 @@
|
|||||||
import Quickshell
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell.Services.UPower
|
import ZShell.Services
|
||||||
import ZShell.Internal
|
import qs.Modules.Resources.Cards
|
||||||
import qs.Components
|
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
|
import qs.Components
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2
|
required property Wrapper wrapper
|
||||||
readonly property real nonAnimHeight: content.implicitHeight + Appearance.padding.normal * 2
|
|
||||||
readonly property real nonAnimWidth: Math.max(minWidth, content.implicitWidth) + Appearance.padding.normal * 2
|
|
||||||
required property real padding
|
|
||||||
required property PersistentProperties visibilities
|
|
||||||
|
|
||||||
function displayTemp(temp: real): string {
|
implicitHeight: content.implicitHeight + Appearance.padding.normal * 2
|
||||||
return `${Math.ceil(temp)}°C`;
|
implicitWidth: content.implicitWidth
|
||||||
}
|
|
||||||
|
|
||||||
implicitHeight: nonAnimHeight
|
|
||||||
implicitWidth: nonAnimWidth
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: root.padding
|
anchors.margins: Appearance.padding.normal
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: root.padding
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Appearance.spacing.normal
|
spacing: Appearance.spacing.larger
|
||||||
|
|
||||||
Ref {
|
|
||||||
service: SystemUsage
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: mainColumn
|
id: mainColumn
|
||||||
@@ -45,748 +30,94 @@ Item {
|
|||||||
spacing: Appearance.spacing.normal
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: Appearance.spacing.normal
|
spacing: Appearance.spacing.normal
|
||||||
visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE")
|
visible: cpuCard.active || gpuCard.active
|
||||||
|
|
||||||
HeroCard {
|
WrappedLoader {
|
||||||
Layout.fillWidth: true
|
id: cpuCard
|
||||||
Layout.minimumWidth: 400
|
|
||||||
Layout.preferredHeight: 150
|
active: Config.dashboard.performance.showCpu
|
||||||
accentColor: DynamicColors.palette.m3primary
|
|
||||||
icon: "memory"
|
sourceComponent: HeroCard {
|
||||||
mainLabel: qsTr("Usage")
|
accent: DynamicColors.palette.m3primary
|
||||||
mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%`
|
icon: "memory"
|
||||||
secondaryLabel: qsTr("Temp")
|
label: qsTr("CPU")
|
||||||
secondaryValue: root.displayTemp(SystemUsage.cpuTemp)
|
subLabel: Cpu.name
|
||||||
temperature: SystemUsage.cpuTemp
|
temperature: Cpu.temperature
|
||||||
title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU")
|
usage: Cpu.percentage
|
||||||
usage: SystemUsage.cpuPerc
|
|
||||||
visible: Config.dashboard.performance.showCpu
|
ServiceRef {
|
||||||
|
service: Cpu
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HeroCard {
|
WrappedLoader {
|
||||||
Layout.fillWidth: true
|
id: gpuCard
|
||||||
Layout.minimumWidth: 400
|
|
||||||
Layout.preferredHeight: 150
|
active: Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None
|
||||||
accentColor: DynamicColors.palette.m3secondary
|
|
||||||
icon: "desktop_windows"
|
sourceComponent: HeroCard {
|
||||||
mainLabel: qsTr("Usage")
|
accent: DynamicColors.palette.m3secondary
|
||||||
mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%`
|
icon: "desktop_windows"
|
||||||
secondaryLabel: qsTr("Temp")
|
label: qsTr("GPU")
|
||||||
secondaryValue: root.displayTemp(SystemUsage.gpuTemp)
|
subLabel: Gpu.name
|
||||||
temperature: SystemUsage.gpuTemp
|
temperature: Gpu.temperature
|
||||||
title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU")
|
usage: Gpu.percentage
|
||||||
usage: SystemUsage.gpuPerc
|
|
||||||
visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE"
|
ServiceRef {
|
||||||
|
service: Gpu
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: Appearance.spacing.normal
|
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 {
|
WrappedLoader {
|
||||||
Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork
|
id: storageCard
|
||||||
Layout.minimumWidth: 250
|
|
||||||
Layout.preferredHeight: 220
|
active: Config.dashboard.performance.showStorage
|
||||||
accentColor: DynamicColors.palette.m3tertiary
|
|
||||||
icon: "memory_alt"
|
sourceComponent: StorageCard {
|
||||||
percentage: SystemUsage.memPerc
|
|
||||||
subtitle: {
|
|
||||||
const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed);
|
|
||||||
const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal);
|
|
||||||
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
|
|
||||||
}
|
}
|
||||||
title: qsTr("Memory")
|
|
||||||
visible: Config.dashboard.performance.showMemory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StorageGaugeCard {
|
WrappedLoader {
|
||||||
Layout.fillWidth: !Config.dashboard.performance.showNetwork
|
id: memoryCard
|
||||||
Layout.minimumWidth: 250
|
|
||||||
Layout.preferredHeight: 220
|
active: Config.dashboard.performance.showMemory
|
||||||
visible: Config.dashboard.performance.showStorage
|
|
||||||
|
sourceComponent: MemoryCard {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkCard {
|
WrappedLoader {
|
||||||
Layout.fillWidth: true
|
id: networkCard
|
||||||
Layout.minimumWidth: 200
|
|
||||||
Layout.preferredHeight: 220
|
active: Config.dashboard.performance.showNetwork
|
||||||
visible: Config.dashboard.performance.showNetwork
|
|
||||||
|
sourceComponent: NetworkCard {
|
||||||
|
wrapper: root.wrapper
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BatteryTank {
|
WrappedLoader {
|
||||||
Layout.preferredHeight: mainColumn.implicitHeight
|
Layout.fillWidth: false
|
||||||
Layout.preferredWidth: 120
|
active: Battery.isLaptop && Config.dashboard.performance.showBattery
|
||||||
visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery
|
|
||||||
|
sourceComponent: BatteryTank {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
component BatteryTank: CustomClippingRect {
|
component WrappedLoader: Loader {
|
||||||
id: batteryTank
|
Layout.fillHeight: true
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Appearance.spacing.small
|
visible: active
|
||||||
|
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ Item {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
padding: Appearance.padding.normal
|
wrapper: root
|
||||||
visibilities: root.visibilities
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ Item {
|
|||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
radius: Appearance.rounding.normal
|
radius: Appearance.rounding.normal
|
||||||
|
|
||||||
Flickable {
|
CustomFlickable {
|
||||||
id: clayout
|
id: clayout
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -92,6 +92,18 @@ SettingsPage {
|
|||||||
Separator {
|
Separator {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingSpinBox {
|
||||||
|
max: 1.0
|
||||||
|
min: 0
|
||||||
|
name: "Minimum brightness"
|
||||||
|
object: Config.services
|
||||||
|
setting: "minBrightness"
|
||||||
|
step: 0.01
|
||||||
|
}
|
||||||
|
|
||||||
|
Separator {
|
||||||
|
}
|
||||||
|
|
||||||
SettingSpinBox {
|
SettingSpinBox {
|
||||||
max: 5
|
max: 5
|
||||||
min: 0
|
min: 0
|
||||||
@@ -115,7 +127,7 @@ SettingsPage {
|
|||||||
|
|
||||||
SettingSpinBox {
|
SettingSpinBox {
|
||||||
min: 1
|
min: 1
|
||||||
name: "Visualizer bars"
|
name: "Visualizer resolution"
|
||||||
object: Config.services
|
object: Config.services
|
||||||
setting: "visualizerBars"
|
setting: "visualizerBars"
|
||||||
step: 1
|
step: 1
|
||||||
|
|||||||
@@ -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 {
|
SettingsSection {
|
||||||
sectionId: "Toasts"
|
sectionId: "Toasts"
|
||||||
|
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomRect {
|
CustomClippingRect {
|
||||||
id: categoryContent
|
id: categoryContent
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
@@ -197,7 +197,6 @@ Item {
|
|||||||
id: stack
|
id: stack
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Appearance.padding.extraSmall
|
|
||||||
initialItem: general
|
initialItem: general
|
||||||
|
|
||||||
popEnter: Transition {
|
popEnter: Transition {
|
||||||
|
|||||||
@@ -23,18 +23,15 @@ Item {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomClippingWrapperRect {
|
|
||||||
anchors.fill: parent
|
|
||||||
child: flickable
|
|
||||||
radius: Appearance.rounding.normal - Appearance.padding.extraSmall
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomFlickable {
|
CustomFlickable {
|
||||||
id: flickable
|
id: flickable
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
// for future:
|
||||||
|
// anchors.leftMargin: Appearance.padding.extraLarge
|
||||||
|
// anchors.rightMargin: Appearance.padding.extraLarge
|
||||||
clip: true
|
clip: true
|
||||||
contentHeight: clayout.implicitHeight
|
contentHeight: clayout.implicitHeight + clayout.anchors.margins * 2
|
||||||
|
|
||||||
CustomScrollBar.vertical: CustomScrollBar {
|
CustomScrollBar.vertical: CustomScrollBar {
|
||||||
flickable: flickable
|
flickable: flickable
|
||||||
@@ -64,7 +61,9 @@ Item {
|
|||||||
id: clayout
|
id: clayout
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Appearance.padding.extraSmall
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
spacing: Appearance.spacing.small
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
// move: Transition {
|
// move: Transition {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ CustomRect {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
implicitHeight: layout.height + contentPadding * 2
|
implicitHeight: layout.height + contentPadding * 2
|
||||||
radius: Appearance.rounding.normal - Appearance.padding.smaller
|
radius: Appearance.rounding.normal - Appearance.padding.extraSmall
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
Anim {
|
Anim {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ Item {
|
|||||||
const finalRect = Qt.rect(cropXPercent, cropYPercent, cropWidthPercent, cropHeightPercent);
|
const finalRect = Qt.rect(cropXPercent, cropYPercent, cropWidthPercent, cropHeightPercent);
|
||||||
|
|
||||||
// We just pass the percentages directly to the backend
|
// 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 {
|
function zoomClipRect(zoom: real): void {
|
||||||
@@ -165,7 +165,7 @@ Item {
|
|||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
// retainWhileLoading: true
|
retainWhileLoading: true
|
||||||
source: Wallpapers.current
|
source: Wallpapers.current
|
||||||
sourceSize.height: parent.height
|
sourceSize.height: parent.height
|
||||||
sourceSize.width: parent.width
|
sourceSize.width: parent.width
|
||||||
@@ -200,7 +200,7 @@ Item {
|
|||||||
Loader {
|
Loader {
|
||||||
id: cropRectLoader
|
id: cropRectLoader
|
||||||
|
|
||||||
active: scaledImg.paintedWidth > 0 && scaledImg.status == Image.Ready
|
active: scaledImg.paintedWidth > 0
|
||||||
|
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
CustomRect {
|
CustomRect {
|
||||||
|
|||||||
@@ -11,82 +11,98 @@ import ZShell.Internal
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
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
|
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
|
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
|
anchors.fill: parent
|
||||||
|
|
||||||
Component.onCompleted: root.refreshData()
|
Component.onCompleted: {
|
||||||
|
Hyprland.refreshMonitors();
|
||||||
|
|
||||||
Connections {
|
if (source)
|
||||||
function onHeightChanged() {
|
Qt.callLater(() => {
|
||||||
root.refreshData();
|
current = imgComp.createObject(this, {
|
||||||
}
|
source
|
||||||
|
});
|
||||||
function onWidthChanged() {
|
completed = true;
|
||||||
root.refreshData();
|
});
|
||||||
}
|
}
|
||||||
|
onSourceChanged: {
|
||||||
target: root.screen
|
if (!source)
|
||||||
|
current = null;
|
||||||
|
else
|
||||||
|
current = imgComp.createObject(this, {
|
||||||
|
source: source
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
WallpaperImage {
|
Component {
|
||||||
id: img
|
id: imgComp
|
||||||
|
|
||||||
anchors.fill: parent
|
WallpaperImage {
|
||||||
source: root.source
|
id: img
|
||||||
|
|
||||||
Behavior on cropHeight {
|
anchors.fill: parent
|
||||||
Anim {
|
cropHeight: root.cropHeight
|
||||||
}
|
cropWidth: root.cropWidth
|
||||||
}
|
cropX: root.cropX
|
||||||
Behavior on cropWidth {
|
cropY: root.cropY
|
||||||
Anim {
|
opacity: 0
|
||||||
}
|
screenResolution: root.screenResolution
|
||||||
}
|
source: root.source
|
||||||
Behavior on cropX {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on cropY {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on zoom {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
Behavior on cropHeight {
|
||||||
function onAdapterUpdated(): void {
|
Anim {
|
||||||
root.refreshData();
|
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 {
|
onStatusChanged: {
|
||||||
root.refreshData();
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_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(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
||||||
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
||||||
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
||||||
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
|
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
|
||||||
pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0)
|
pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0)
|
||||||
|
|
||||||
if(NOT Cava_FOUND)
|
if(NOT Cava_FOUND)
|
||||||
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
|
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
|
||||||
endif()
|
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")
|
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
|
||||||
qt_standard_project_setup(REQUIRES 6.9)
|
qt_standard_project_setup(REQUIRES 6.9)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ qml_module(ZShell-components
|
|||||||
SOURCES
|
SOURCES
|
||||||
lazylistview.hpp lazylistview.cpp
|
lazylistview.hpp lazylistview.cpp
|
||||||
wavyline.hpp wavyline.cpp
|
wavyline.hpp wavyline.cpp
|
||||||
|
buttonrow.hpp buttonrow.cpp
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1,20 +1,25 @@
|
|||||||
qml_module(ZShell-internal
|
qml_module(ZShell-internal
|
||||||
URI ZShell.Internal
|
URI ZShell.Internal
|
||||||
SOURCES
|
SOURCES
|
||||||
hyprextras.hpp hyprextras.cpp
|
hyprextras.hpp hyprextras.cpp
|
||||||
hyprdevices.hpp hyprdevices.cpp
|
hyprdevices.hpp hyprdevices.cpp
|
||||||
cachingimagemanager.hpp cachingimagemanager.cpp
|
cachingimagemanager.hpp cachingimagemanager.cpp
|
||||||
circularindicatormanager.hpp circularindicatormanager.cpp
|
circularindicatormanager.hpp circularindicatormanager.cpp
|
||||||
circularbuffer.hpp circularbuffer.cpp
|
circularbuffer.hpp circularbuffer.cpp
|
||||||
sparklineitem.hpp sparklineitem.cpp
|
sparklineitem.hpp sparklineitem.cpp
|
||||||
arcgauge.hpp arcgauge.cpp
|
arcgauge.hpp arcgauge.cpp
|
||||||
wallpaperimage.hpp wallpaperimage.cpp
|
wallpaperimage.hpp wallpaperimage.cpp
|
||||||
lidwatcher.hpp lidwatcher.cpp
|
lidwatcher.hpp lidwatcher.cpp
|
||||||
LIBRARIES
|
visualizerbars.hpp visualizerbars.cpp
|
||||||
Qt::Gui
|
linearindicatormanager.hpp linearindicatormanager.cpp
|
||||||
Qt::Quick
|
strokecanvasitem.hpp strokecanvasitem.cpp
|
||||||
Qt::Concurrent
|
strokecanvasrenderer.hpp strokecanvasrenderer.cpp
|
||||||
Qt::Core
|
LIBRARIES
|
||||||
|
Qt::Gui
|
||||||
|
Qt::Quick
|
||||||
|
Qt::Concurrent
|
||||||
|
Qt::Core
|
||||||
Qt::Network
|
Qt::Network
|
||||||
Qt::DBus
|
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
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "sparklineitem.hpp"
|
#include "sparklineitem.hpp"
|
||||||
|
|
||||||
|
#include <qmath.h>
|
||||||
#include <qpainter.h>
|
#include <qpainter.h>
|
||||||
#include <qpainterpath.h>
|
#include <qpainterpath.h>
|
||||||
#include <qpen.h>
|
#include <qpen.h>
|
||||||
@@ -33,19 +34,45 @@ void SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QC
|
|||||||
const qreal w = width();
|
const qreal w = width();
|
||||||
const qreal h = height();
|
const qreal h = height();
|
||||||
const int len = buffer->count();
|
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 stepX = w / static_cast<qreal>(m_historyLength - 1);
|
||||||
const qreal startX = w - (len - 1) * stepX - stepX * m_slideProgress + stepX;
|
const qreal startX = w - (len - 1) * stepX - stepX * m_slideProgress + stepX;
|
||||||
|
|
||||||
// Build line path
|
const qreal strokePad = qCeil(m_lineWidth / 2);
|
||||||
QPainterPath linePath;
|
const qreal curvePad = 3.0;
|
||||||
linePath.moveTo(startX, h - (buffer->at(0) / m_maxValue) * h);
|
const qreal topPad = strokePad + curvePad;
|
||||||
for (int i = 1; i < len; ++i) {
|
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 x = startX + i * stepX;
|
||||||
const qreal y = h - (buffer->at(i) / m_maxValue) * h;
|
const qreal value = qBound<qreal>(0.0, buffer->at(i), m_maxValue);
|
||||||
linePath.lineTo(x, y);
|
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);
|
QPen pen(color, m_lineWidth);
|
||||||
pen.setCapStyle(Qt::RoundCap);
|
pen.setCapStyle(Qt::RoundCap);
|
||||||
pen.setJoinStyle(Qt::RoundJoin);
|
pen.setJoinStyle(Qt::RoundJoin);
|
||||||
@@ -53,10 +80,9 @@ void SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QC
|
|||||||
painter->setBrush(Qt::NoBrush);
|
painter->setBrush(Qt::NoBrush);
|
||||||
painter->drawPath(linePath);
|
painter->drawPath(linePath);
|
||||||
|
|
||||||
// Fill under the line
|
|
||||||
QPainterPath fillPath = linePath;
|
QPainterPath fillPath = linePath;
|
||||||
fillPath.lineTo(startX + (len - 1) * stepX, h);
|
fillPath.lineTo(startX + (len - 1) * stepX, fillBottom);
|
||||||
fillPath.lineTo(startX, h);
|
fillPath.lineTo(startX, fillBottom);
|
||||||
fillPath.closeSubpath();
|
fillPath.closeSubpath();
|
||||||
|
|
||||||
QColor fillColor = color;
|
QColor fillColor = color;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
@@ -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
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <QtConcurrent>
|
#include <QtConcurrent>
|
||||||
#include <QSGImageNode>
|
#include <QSGImageNode>
|
||||||
#include <QQuickWindow>
|
#include <QQuickWindow>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
namespace ZShell::internal {
|
namespace ZShell::internal {
|
||||||
|
|
||||||
@@ -21,10 +22,20 @@ WallpaperImage::~WallpaperImage() {
|
|||||||
if (m_texture) delete m_texture;
|
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) {
|
void WallpaperImage::setSource(const QUrl &source) {
|
||||||
if (m_source == source) return;
|
if (m_source == source) return;
|
||||||
m_source = source;
|
m_source = source;
|
||||||
emit sourceChanged();
|
emit sourceChanged();
|
||||||
|
|
||||||
|
setStatus(Loading);
|
||||||
loadImage();
|
loadImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,28 +68,27 @@ void WallpaperImage::setCropY(qreal y) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WallpaperImage::setCropWidth(qreal w) {
|
void WallpaperImage::setCropWidth(qreal w) {
|
||||||
if (w <= 0.0) w = 1.0;
|
if (w <= 0.0) w = 1.0;
|
||||||
if (qFuzzyCompare(m_cropWidth, w)) return;
|
if (qFuzzyCompare(m_cropWidth, w)) return;
|
||||||
m_cropWidth = w;
|
m_cropWidth = w;
|
||||||
emit cropWidthChanged();
|
emit cropWidthChanged();
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallpaperImage::setCropHeight(qreal h) {
|
void WallpaperImage::setCropHeight(qreal h) {
|
||||||
if (h <= 0.0) h = 1.0;
|
if (h <= 0.0) h = 1.0;
|
||||||
if (qFuzzyCompare(m_cropHeight, h)) return;
|
if (qFuzzyCompare(m_cropHeight, h)) return;
|
||||||
m_cropHeight = h;
|
m_cropHeight = h;
|
||||||
emit cropHeightChanged();
|
emit cropHeightChanged();
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString WallpaperImage::getCacheFilePath() const {
|
QString WallpaperImage::getCacheFilePath() const {
|
||||||
if (m_source.isEmpty() || m_screenResolution.isEmpty()) return QString();
|
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);
|
QDir().mkpath(cachePath);
|
||||||
|
|
||||||
// Hash the source URL + resolution
|
|
||||||
QString id = m_source.toString() + "_" + QString::number(m_screenResolution.width()) + "x" + QString::number(m_screenResolution.height());
|
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();
|
QByteArray hash = QCryptographicHash::hash(id.toUtf8(), QCryptographicHash::Md5).toHex();
|
||||||
|
|
||||||
@@ -86,52 +96,53 @@ QString WallpaperImage::getCacheFilePath() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WallpaperImage::loadImage() {
|
void WallpaperImage::loadImage() {
|
||||||
if (m_source.isEmpty()) return;
|
if (m_source.isEmpty()) {
|
||||||
|
setStatus(Null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
QString cacheFile = getCacheFilePath();
|
QString cacheFile = getCacheFilePath();
|
||||||
QString sourceFile = m_source.isLocalFile() ? m_source.toLocalFile() : m_source.toString();
|
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;
|
|
||||||
|
|
||||||
// Run off the main thread to avoid blocking the UI
|
if (sourceFile.startsWith("qrc:/")) {
|
||||||
QFuture<QImage> future = QtConcurrent::run([sourceFile, cacheFile, targetRes]() -> QImage {
|
sourceFile = sourceFile.mid(3);
|
||||||
if (!targetRes.isEmpty() && !cacheFile.isEmpty() && QFileInfo::exists(cacheFile)) {
|
}
|
||||||
QImage cached(cacheFile);
|
|
||||||
if (!cached.isNull()) return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
QImage original(sourceFile);
|
QSize targetRes = m_screenResolution;
|
||||||
if (original.isNull()) return QImage();
|
|
||||||
|
|
||||||
if (targetRes.isEmpty()) {
|
QFuture<QImage> future = QtConcurrent::run([sourceFile, cacheFile, targetRes]() -> QImage {
|
||||||
// Screen resolution not set yet by QML, return the unscaled original for now to prevent a black screen
|
if (!targetRes.isEmpty() && !cacheFile.isEmpty() && QFileInfo::exists(cacheFile)) {
|
||||||
return original;
|
QImage cached(cacheFile);
|
||||||
}
|
if (!cached.isNull()) return cached;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if original is strictly larger than screen resolution
|
QImage original(sourceFile);
|
||||||
if (original.width() > targetRes.width() || original.height() > targetRes.height()) {
|
if (original.isNull()) return QImage();
|
||||||
QImage scaled = original.scaled(targetRes, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
|
||||||
if (!cacheFile.isEmpty()) scaled.save(cacheFile, "PNG");
|
|
||||||
return scaled;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise just cache and return the original
|
if (targetRes.isEmpty()) {
|
||||||
if (!cacheFile.isEmpty()) original.save(cacheFile, "PNG");
|
return original;
|
||||||
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() {
|
void WallpaperImage::handleImageLoaded() {
|
||||||
m_image = m_imageWatcher.result();
|
m_image = m_imageWatcher.result();
|
||||||
|
|
||||||
|
setStatus(m_image.isNull() ? Error : Ready);
|
||||||
|
|
||||||
m_textureDirty = true;
|
m_textureDirty = true;
|
||||||
update(); // Request redraw
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) {
|
QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) {
|
||||||
@@ -148,7 +159,7 @@ QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *
|
|||||||
|
|
||||||
if (m_textureDirty) {
|
if (m_textureDirty) {
|
||||||
if (m_texture) delete m_texture;
|
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;
|
m_textureDirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,40 +168,61 @@ QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *
|
|||||||
node->setRect(boundingRect());
|
node->setRect(boundingRect());
|
||||||
node->setFiltering(QSGTexture::Linear);
|
node->setFiltering(QSGTexture::Linear);
|
||||||
|
|
||||||
qreal cW = m_cropWidth / m_zoom;
|
qreal cW = m_cropWidth / m_zoom;
|
||||||
qreal cH = m_cropHeight / m_zoom;
|
qreal cH = m_cropHeight / m_zoom;
|
||||||
|
|
||||||
QRectF reqRect(
|
QRectF reqRect(
|
||||||
m_cropX * m_texture->textureSize().width(),
|
m_cropX * m_texture->textureSize().width(),
|
||||||
m_cropY * m_texture->textureSize().height(),
|
m_cropY * m_texture->textureSize().height(),
|
||||||
cW * m_texture->textureSize().width(),
|
cW * m_texture->textureSize().width(),
|
||||||
cH * m_texture->textureSize().height()
|
cH * m_texture->textureSize().height()
|
||||||
);
|
);
|
||||||
|
|
||||||
QRectF bounds = boundingRect();
|
QRectF bounds = boundingRect();
|
||||||
if (bounds.isEmpty() || reqRect.isEmpty()) return node;
|
if (bounds.isEmpty() || reqRect.isEmpty()) return node;
|
||||||
|
|
||||||
qreal targetRatio = bounds.width() / bounds.height();
|
qreal targetRatio = bounds.width() / bounds.height();
|
||||||
qreal reqRatio = reqRect.width() / reqRect.height();
|
qreal reqRatio = reqRect.width() / reqRect.height();
|
||||||
|
|
||||||
QRectF sourceRect = reqRect;
|
QRectF sourceRect = reqRect;
|
||||||
|
|
||||||
// Force 'PreserveAspectCrop' behavior on the requested region
|
if (reqRatio > targetRatio) {
|
||||||
if (reqRatio > targetRatio) {
|
qreal newWidth = reqRect.height() * targetRatio;
|
||||||
// Requested region is too wide, center-crop the sides
|
qreal xOffset = (reqRect.width() - newWidth) / 2.0;
|
||||||
qreal newWidth = reqRect.height() * targetRatio;
|
sourceRect.setX(reqRect.x() + xOffset);
|
||||||
qreal xOffset = (reqRect.width() - newWidth) / 2.0;
|
sourceRect.setWidth(newWidth);
|
||||||
sourceRect.setX(reqRect.x() + xOffset);
|
} else if (reqRatio < targetRatio) {
|
||||||
sourceRect.setWidth(newWidth);
|
qreal newHeight = reqRect.width() / targetRatio;
|
||||||
} else if (reqRatio < targetRatio) {
|
qreal yOffset = (reqRect.height() - newHeight) / 2.0;
|
||||||
// Requested region is too tall, center-crop the top/bottom
|
sourceRect.setY(reqRect.y() + yOffset);
|
||||||
qreal newHeight = reqRect.width() / targetRatio;
|
sourceRect.setHeight(newHeight);
|
||||||
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;
|
return node;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QSGTexture>
|
#include <QSGTexture>
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
#include <QtQml/qqml.h>
|
#include <QtQml/qqml.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
namespace ZShell::internal {
|
namespace ZShell::internal {
|
||||||
|
|
||||||
@@ -15,6 +16,12 @@ QML_NAMED_ELEMENT(WallpaperImage)
|
|||||||
Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
|
Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
|
||||||
Q_PROPERTY(QSize screenResolution READ screenResolution WRITE setScreenResolution NOTIFY screenResolutionChanged)
|
Q_PROPERTY(QSize screenResolution READ screenResolution WRITE setScreenResolution NOTIFY screenResolutionChanged)
|
||||||
Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged)
|
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 cropX READ cropX WRITE setCropX NOTIFY cropXChanged)
|
||||||
Q_PROPERTY(qreal cropY READ cropY WRITE setCropY NOTIFY cropYChanged)
|
Q_PROPERTY(qreal cropY READ cropY WRITE setCropY NOTIFY cropYChanged)
|
||||||
@@ -25,6 +32,18 @@ public:
|
|||||||
explicit WallpaperImage(QQuickItem *parent = nullptr);
|
explicit WallpaperImage(QQuickItem *parent = nullptr);
|
||||||
~WallpaperImage() override;
|
~WallpaperImage() override;
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
Null,
|
||||||
|
Ready,
|
||||||
|
Loading,
|
||||||
|
Error
|
||||||
|
};
|
||||||
|
Q_ENUM(Status)
|
||||||
|
|
||||||
|
Status status() const {
|
||||||
|
return m_status;
|
||||||
|
}
|
||||||
|
|
||||||
QUrl source() const {
|
QUrl source() const {
|
||||||
return m_source;
|
return m_source;
|
||||||
}
|
}
|
||||||
@@ -40,6 +59,22 @@ qreal zoom() const {
|
|||||||
}
|
}
|
||||||
void setZoom(qreal zoom);
|
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 {
|
qreal cropX() const {
|
||||||
return m_cropX;
|
return m_cropX;
|
||||||
}
|
}
|
||||||
@@ -67,20 +102,29 @@ signals:
|
|||||||
void sourceChanged();
|
void sourceChanged();
|
||||||
void screenResolutionChanged();
|
void screenResolutionChanged();
|
||||||
void zoomChanged();
|
void zoomChanged();
|
||||||
|
void actualCropChanged();
|
||||||
void cropXChanged();
|
void cropXChanged();
|
||||||
void cropYChanged();
|
void cropYChanged();
|
||||||
void cropWidthChanged();
|
void cropWidthChanged();
|
||||||
void cropHeightChanged();
|
void cropHeightChanged();
|
||||||
|
void statusChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void loadImage();
|
void loadImage();
|
||||||
void handleImageLoaded();
|
void handleImageLoaded();
|
||||||
QString getCacheFilePath() const;
|
QString getCacheFilePath() const;
|
||||||
|
void setStatus(const Status s);
|
||||||
|
|
||||||
|
Status m_status = Null;
|
||||||
QUrl m_source;
|
QUrl m_source;
|
||||||
QSize m_screenResolution;
|
QSize m_screenResolution;
|
||||||
qreal m_zoom = 1.0;
|
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_cropX = 0.0;
|
||||||
qreal m_cropY = 0.0;
|
qreal m_cropY = 0.0;
|
||||||
qreal m_cropWidth = 1.0;
|
qreal m_cropWidth = 1.0;
|
||||||
|
|||||||
@@ -1,19 +1,28 @@
|
|||||||
qml_module(ZShell-services
|
qml_module(ZShell-services
|
||||||
URI ZShell.Services
|
URI ZShell.Services
|
||||||
SOURCES
|
SOURCES
|
||||||
service.hpp service.cpp
|
service.hpp service.cpp
|
||||||
serviceref.hpp serviceref.cpp
|
serviceref.hpp serviceref.cpp
|
||||||
beattracker.hpp beattracker.cpp
|
beattracker.hpp beattracker.cpp
|
||||||
audiocollector.hpp audiocollector.cpp
|
audiocollector.hpp audiocollector.cpp
|
||||||
audioprovider.hpp audioprovider.cpp
|
audioprovider.hpp audioprovider.cpp
|
||||||
cavaprovider.hpp cavaprovider.cpp
|
cavaprovider.hpp cavaprovider.cpp
|
||||||
desktopmodel.hpp desktopmodel.cpp
|
desktopmodel.hpp desktopmodel.cpp
|
||||||
desktopstatemanager.hpp desktopstatemanager.cpp
|
desktopstatemanager.hpp desktopstatemanager.cpp
|
||||||
hyprsunsetmanager.hpp hyprsunsetmanager.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::Core
|
||||||
Qt6::Qml
|
Qt6::Qml
|
||||||
PkgConfig::Pipewire
|
PkgConfig::Pipewire
|
||||||
PkgConfig::Aubio
|
PkgConfig::Aubio
|
||||||
PkgConfig::Cava
|
PkgConfig::Cava
|
||||||
|
Sensors::Sensors
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ void CavaProcessor::process() {
|
|||||||
QVector<double> values(m_bars);
|
QVector<double> values(m_bars);
|
||||||
|
|
||||||
for(int i = 0; i < m_bars; ++i) {
|
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
|
// Left to right pass
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
Reference in New Issue
Block a user