Initial commit
This commit is contained in:
commit
a8c2f88654
53 changed files with 9269 additions and 0 deletions
68
Components/Cava.qml
Normal file
68
Components/Cava.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
134
Components/CircularProgressBar.qml
Normal file
134
Components/CircularProgressBar.qml
Normal 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()
|
||||
}
|
||||
47
Components/CircularSpectrum.qml
Normal file
47
Components/CircularSpectrum.qml
Normal 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
86
Components/Corners.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
168
Components/PillIndicator.qml
Normal file
168
Components/PillIndicator.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue