Initial commit

This commit is contained in:
ly-sec 2025-07-11 14:14:28 +02:00
commit a8c2f88654
53 changed files with 9269 additions and 0 deletions

68
Components/Cava.qml Normal file
View file

@ -0,0 +1,68 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Components
Scope {
id: root
property int count: 32
property int noiseReduction: 60
property string channels: "mono" // or stereo
property string monoOption: "average" // or left or right
property var config: ({
general: { bars: count },
smoothing: { noise_reduction: noiseReduction },
output: {
method: "raw",
bit_format: 8,
channels: channels,
mono_option: monoOption,
}
})
property var values: Array(count).fill(0) // 0 <= value <= 1
onConfigChanged: {
process.running = false
process.running = true
}
Process {
property int index: 0
id: process
stdinEnabled: true
command: ["cava", "-p", "/dev/stdin"]
onExited: { stdinEnabled = true; index = 0 }
onStarted: {
const iniParts = []
for (const k in config) {
if (typeof config[k] !== "object") {
write(k + "=" + config[k] + "\n")
continue
}
write("[" + k + "]\n")
const obj = config[k]
for (const k2 in obj) {
write(k2 + "=" + obj[k2] + "\n")
}
}
stdinEnabled = false
}
stdout: SplitParser {
property var newValues: Array(count).fill(0)
splitMarker: ""
onRead: data => {
if (process.index + data.length > config.general.bars) {
process.index = 0
}
for (let i = 0; i < data.length; i += 1) {
newValues[i + process.index] = Math.min(data.charCodeAt(i), 128) / 128
}
process.index += data.length
if (newValues.length !== values.length) {
console.log("length!", values.length, newValues.length)
}
values = newValues
}
}
}
}

View file

@ -0,0 +1,134 @@
import QtQuick
import qs.Settings
Rectangle {
id: circularProgressBar
color: "transparent"
// Properties
property real progress: 0.0 // 0.0 to 1.0
property int size: 80
property color backgroundColor: Theme.surfaceVariant
property color progressColor: Theme.accentPrimary
property int strokeWidth: 6
property bool showText: true
property string text: Math.round(progress * 100) + "%"
property int textSize: 10
property color textColor: Theme.textPrimary
// Notch properties
property bool hasNotch: false
property real notchSize: 0.25 // Size of the notch as a fraction of the circle
property string notchIcon: ""
property int notchIconSize: 12
property color notchIconColor: Theme.accentPrimary
width: size
height: size
Canvas {
id: canvas
anchors.fill: parent
onPaint: {
var ctx = getContext("2d")
var centerX = width / 2
var centerY = height / 2
var radius = Math.min(width, height) / 2 - strokeWidth / 2
var startAngle = -Math.PI / 2 // Start from top
var notchAngle = notchSize * 2 * Math.PI
var notchStartAngle = -notchAngle / 2
var notchEndAngle = notchAngle / 2
// Clear canvas
ctx.reset()
// Background circle
ctx.strokeStyle = backgroundColor
ctx.lineWidth = strokeWidth
ctx.lineCap = "round"
ctx.beginPath()
if (hasNotch) {
// Draw background circle with notch on the right side
// Draw the arc excluding the notch area (notch is at 0 radians, right side)
ctx.arc(centerX, centerY, radius, notchEndAngle, 2 * Math.PI + notchStartAngle)
} else {
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI)
}
ctx.stroke()
// Progress arc
if (progress > 0) {
ctx.strokeStyle = progressColor
ctx.lineWidth = strokeWidth
ctx.lineCap = "round"
ctx.beginPath()
if (hasNotch) {
// Calculate progress with notch consideration
var availableAngle = 2 * Math.PI - notchAngle
var progressAngle = availableAngle * progress
// Start from where the notch cutout begins (top-right) and go clockwise
var adjustedStartAngle = notchEndAngle
var adjustedEndAngle = adjustedStartAngle + progressAngle
// Ensure we don't exceed the available space
if (adjustedEndAngle > 2 * Math.PI + notchStartAngle) {
adjustedEndAngle = 2 * Math.PI + notchStartAngle
}
if (adjustedEndAngle > adjustedStartAngle) {
ctx.arc(centerX, centerY, radius, adjustedStartAngle, adjustedEndAngle)
}
} else {
ctx.arc(centerX, centerY, radius, startAngle, startAngle + (2 * Math.PI * progress))
}
ctx.stroke()
}
}
}
// Center text - always show the percentage
Text {
id: centerText
anchors.centerIn: parent
text: circularProgressBar.text
font.pixelSize: textSize
font.bold: true
color: textColor
visible: showText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
// Notch icon - positioned further to the right
Text {
id: notchIconText
anchors.right: parent.right
anchors.rightMargin: -4
anchors.verticalCenter: parent.verticalCenter
text: notchIcon
font.family: "Material Symbols Outlined"
font.pixelSize: notchIconSize
color: notchIconColor
visible: hasNotch && notchIcon !== ""
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
// Animate progress changes
Behavior on progress {
NumberAnimation { duration: 300; easing.type: Easing.OutCubic }
}
// Redraw canvas when properties change
onProgressChanged: canvas.requestPaint()
onSizeChanged: canvas.requestPaint()
onBackgroundColorChanged: canvas.requestPaint()
onProgressColorChanged: canvas.requestPaint()
onStrokeWidthChanged: canvas.requestPaint()
onHasNotchChanged: canvas.requestPaint()
onNotchSizeChanged: canvas.requestPaint()
}

View file

@ -0,0 +1,47 @@
import QtQuick
import qs.Components
Item {
id: root
property int innerRadius: 34
property int outerRadius: 48
property int barCount: 40
property color fillColor: "#fff"
property color strokeColor: "#fff"
property int strokeWidth: 0
width: outerRadius * 2
height: outerRadius * 2
// Cava input
Cava {
id: cava
count: root.barCount
}
Repeater {
model: root.barCount
Rectangle {
property real value: cava.values[index]
property real angle: (index / root.barCount) * 360
width: Math.max(2, (root.innerRadius * 2 * Math.PI) / root.barCount - 4)
height: value * (root.outerRadius - root.innerRadius)
radius: width / 2
color: root.fillColor
border.color: root.strokeColor
border.width: root.strokeWidth
antialiasing: true
x: root.width / 2 + (root.innerRadius) * Math.cos(Math.PI/2 + 2 * Math.PI * index / root.barCount) - width / 2
y: root.height / 2 - (root.innerRadius) * Math.sin(Math.PI/2 + 2 * Math.PI * index / root.barCount) - height
transform: Rotation {
origin.x: width / 2
origin.y: height
angle: -angle
}
Behavior on height { SmoothedAnimation { duration: 120 } }
}
}
}

86
Components/Corners.qml Normal file
View file

@ -0,0 +1,86 @@
import QtQuick
import QtQuick.Shapes
import qs.Settings
Shape {
id: root
property string position: "topleft" // Corner position: topleft/topright/bottomleft/bottomright
property real size: 1.0 // Scale multiplier for entire corner
property int concaveWidth: 100 * size
property int concaveHeight: 60 * size
property int offsetX: -20
property int offsetY: -20
property color fillColor: Theme.accentPrimary
property int arcRadius: 20 * size
property var modelData: null
// Position flags derived from position string
property bool _isTop: position.includes("top")
property bool _isLeft: position.includes("left")
property bool _isRight: position.includes("right")
property bool _isBottom: position.includes("bottom")
// Shift the path vertically if offsetY is negative to pull shape up
property real pathOffsetY: Math.min(offsetY, 0)
// Base coordinates for left corner shape, shifted by pathOffsetY vertically
property real _baseStartX: 30 * size
property real _baseStartY: (_isTop ? 20 * size : 0) + pathOffsetY
property real _baseLineX: 30 * size
property real _baseLineY: (_isTop ? 0 : 20 * size) + pathOffsetY
property real _baseArcX: 50 * size
property real _baseArcY: (_isTop ? 20 * size : 0) + pathOffsetY
// Mirror coordinates for right corners
property real _startX: _isRight ? (concaveWidth - _baseStartX) : _baseStartX
property real _startY: _baseStartY
property real _lineX: _isRight ? (concaveWidth - _baseLineX) : _baseLineX
property real _lineY: _baseLineY
property real _arcX: _isRight ? (concaveWidth - _baseArcX) : _baseArcX
property real _arcY: _baseArcY
// Arc direction varies by corner to maintain proper concave shape
property int _arcDirection: {
if (_isTop && _isLeft) return PathArc.Counterclockwise
if (_isTop && _isRight) return PathArc.Clockwise
if (_isBottom && _isLeft) return PathArc.Clockwise
if (_isBottom && _isRight) return PathArc.Counterclockwise
return PathArc.Counterclockwise
}
width: concaveWidth
height: concaveHeight
// Position relative to parent based on corner type
x: _isLeft ? offsetX : (parent ? parent.width - width + offsetX : 0)
y: _isTop ? offsetY : (parent ? parent.height - height + offsetY : 0)
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.samples: 4
ShapePath {
strokeWidth: 0
fillColor: root.fillColor
strokeColor: root.fillColor
startX: root._startX
startY: root._startY
PathLine {
x: root._lineX
y: root._lineY
}
PathArc {
x: root._arcX
y: root._arcY
radiusX: root.arcRadius
radiusY: root.arcRadius
useLargeArc: false
direction: root._arcDirection
}
}
}

View file

@ -0,0 +1,168 @@
import QtQuick
import QtQuick.Controls
import qs.Settings
Item {
id: revealPill
// External properties
property string icon: ""
property string text: ""
property color pillColor: Theme.surfaceVariant
property color textColor: Theme.textPrimary
property color iconCircleColor: Theme.accentPrimary
property color iconTextColor: Theme.backgroundPrimary
property int pillHeight: 22
property int iconSize: 22
property int pillPaddingHorizontal: 14
// Internal state
property bool showPill: false
property bool shouldAnimateHide: false
// Exposed width logic
readonly property int pillOverlap: iconSize / 2
readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
signal shown()
signal hidden()
width: iconSize + (showPill ? maxPillWidth - pillOverlap : 0)
height: pillHeight
Rectangle {
id: pill
width: showPill ? maxPillWidth : 1 // Never 0 width
height: pillHeight
x: (iconCircle.x + iconCircle.width / 2) - width
opacity: showPill ? 1 : 0
color: pillColor
topLeftRadius: pillHeight / 2
bottomLeftRadius: pillHeight / 2
anchors.verticalCenter: parent.verticalCenter
Text {
id: textItem
anchors.centerIn: parent
text: revealPill.text
font.pixelSize: 14
font.weight: Font.Bold
color: textColor
visible: showPill // Hide text when pill is collapsed
}
Behavior on width {
enabled: showAnim.running || hideAnim.running
NumberAnimation { duration: 250; easing.type: Easing.OutCubic }
}
Behavior on opacity {
enabled: showAnim.running || hideAnim.running
NumberAnimation { duration: 250; easing.type: Easing.OutCubic }
}
}
// Icon circle
Rectangle {
id: iconCircle
width: iconSize
height: iconSize
radius: width / 2
color: showPill ? iconCircleColor : "transparent"
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
Behavior on color {
ColorAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
Text {
anchors.centerIn: parent
font.family: showPill ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 14
text: revealPill.icon
color: showPill ? iconTextColor : textColor
}
}
// Show animation
ParallelAnimation {
id: showAnim
running: false
NumberAnimation {
target: pill
property: "width"
from: 1 // Start from 1 instead of 0
to: maxPillWidth
duration: 250
easing.type: Easing.OutCubic
}
NumberAnimation {
target: pill
property: "opacity"
from: 0
to: 1
duration: 250
easing.type: Easing.OutCubic
}
onStarted: {
showPill = true
}
onStopped: {
delayedHideAnim.start()
shown()
}
}
// Delayed auto-hide
SequentialAnimation {
id: delayedHideAnim
running: false
PauseAnimation { duration: 2500 }
ScriptAction { script: if (shouldAnimateHide) hideAnim.start() }
}
// Hide animation
ParallelAnimation {
id: hideAnim
running: false
NumberAnimation {
target: pill
property: "width"
from: maxPillWidth
to: 1 // End at 1 instead of 0
duration: 250
easing.type: Easing.InCubic
}
NumberAnimation {
target: pill
property: "opacity"
from: 1
to: 0
duration: 250
easing.type: Easing.InCubic
}
onStopped: {
showPill = false
shouldAnimateHide = false
hidden()
}
}
// Exposed functions
function show() {
if (!showPill) {
shouldAnimateHide = true
showAnim.start()
} else {
// Reset hide timer if already shown
hideAnim.stop()
delayedHideAnim.restart()
}
}
function hide() {
if (showPill) {
hideAnim.start()
}
}
}