feat: audio osd

This commit is contained in:
2 * r + 2 * t 2025-05-04 20:45:34 +10:00
parent 83020b8b1f
commit fb471f1023
13 changed files with 427 additions and 7 deletions

14
config/OsdConfig.qml Normal file
View file

@ -0,0 +1,14 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
readonly property int hideDelay: 1500
readonly property Sizes sizes: Sizes {}
component Sizes: QtObject {
readonly property int sliderWidth: 30
readonly property int sliderHeight: 150
}
}

View file

@ -11,8 +11,6 @@ Shape {
readonly property int roundingY: Math.min(rounding, realWrapperHeight / 2)
readonly property real wrapperHeight: realWrapperHeight - 1 // Pixel issues :sob:
anchors.bottom: parent.bottom
preferredRendererType: Shape.CurveRenderer
opacity: Appearance.transparency.enabled ? Appearance.transparency.base : 1

View file

@ -33,7 +33,6 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: search.top
anchors.bottomMargin: root.spacing
anchors.margins: root.padding
ContentList {
id: list

View file

@ -34,6 +34,7 @@ Scope {
id: bg
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
wrapperWidth: wrapper.width
realWrapperHeight: Math.min(wrapper.height, content.height)
@ -43,6 +44,7 @@ Scope {
id: wrapper
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
implicitWidth: content.width + bg.rounding * 2

View file

@ -8,8 +8,6 @@ Item {
required property real contentHeight
property bool shouldBeVisible
anchors.bottom: parent.bottom
height: 0
clip: true

View file

@ -0,0 +1,61 @@
import "root:/config"
import QtQuick
import QtQuick.Shapes
Shape {
id: root
required property real realWrapperWidth
required property real wrapperHeight
readonly property int rounding: Appearance.rounding.large
readonly property int roundingX: Math.min(rounding, realWrapperWidth / 2)
readonly property real wrapperWidth: realWrapperWidth - 1 // Pixel issues :sob:
preferredRendererType: Shape.CurveRenderer
opacity: Appearance.transparency.enabled ? Appearance.transparency.base : 1
ShapePath {
strokeWidth: -1
fillColor: Appearance.colours.m3surfaceContainer
startX: root.wrapperWidth
PathArc {
relativeX: -root.roundingX
relativeY: root.rounding
radiusX: root.roundingX
radiusY: root.rounding
}
PathLine {
x: root.roundingX
relativeY: 0
}
PathArc {
relativeX: -root.roundingX
relativeY: root.rounding
radiusX: root.roundingX
radiusY: root.rounding
direction: PathArc.Counterclockwise
}
PathLine {
y: root.wrapperHeight - root.rounding * 2
}
PathArc {
relativeX: root.roundingX
relativeY: root.rounding
radiusX: root.roundingX
radiusY: root.rounding
direction: PathArc.Counterclockwise
}
PathLine {
x: root.wrapperWidth - root.roundingX
relativeY: 0
}
PathArc {
relativeX: root.roundingX
relativeY: root.rounding
radiusX: root.roundingX
radiusY: root.rounding
}
}
}

42
modules/osd/Content.qml Normal file
View file

@ -0,0 +1,42 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
Column {
id: root
required property ShellScreen screen
padding: Appearance.padding.large
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
spacing: Appearance.spacing.normal
VerticalSlider {
icon: {
if (Audio.muted)
return "no_sound";
if (value >= 0.5)
return "volume_up";
if (value > 0)
return "volume_down";
return "volume_mute";
}
value: Audio.volume
onMoved: Audio.setVolume(value)
implicitWidth: OsdConfig.sizes.sliderWidth
implicitHeight: OsdConfig.sizes.sliderHeight
}
VerticalSlider {
icon: "brightness_6"
implicitWidth: OsdConfig.sizes.sliderWidth
implicitHeight: OsdConfig.sizes.sliderHeight
}
}

89
modules/osd/Osd.qml Normal file
View file

@ -0,0 +1,89 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
Scope {
id: root
property bool osdVisible
function show(): void {
root.osdVisible = true;
timer.restart();
}
Connections {
target: Audio
function onMutedChanged(): void {
root.show();
}
function onVolumeChanged(): void {
root.show();
}
}
Timer {
id: timer
interval: OsdConfig.hideDelay
onTriggered: root.osdVisible = false
}
Variants {
model: Quickshell.screens
LazyLoader {
loading: true
required property ShellScreen modelData
StyledWindow {
id: win
screen: parent.modelData
name: "osd"
visible: wrapper.shouldBeVisible
mask: Region {
item: wrapper
}
anchors.left: true
anchors.right: true
height: wrapper.height
Background {
id: bg
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
wrapperHeight: wrapper.height
realWrapperWidth: Math.min(wrapper.width, content.width)
}
Wrapper {
id: wrapper
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
implicitHeight: content.height + bg.rounding * 2
osdVisible: root.osdVisible
contentWidth: content.width
Content {
id: content
screen: parent.modelData
}
}
}
}
}
}

62
modules/osd/Wrapper.qml Normal file
View file

@ -0,0 +1,62 @@
import "root:/config"
import QtQuick
Item {
id: root
required property bool osdVisible
required property real contentWidth
property bool shouldBeVisible
width: 0
clip: true
states: State {
name: "visible"
when: root.osdVisible
PropertyChanges {
root.width: contentWidth
root.shouldBeVisible: true
}
}
transitions: [
Transition {
from: ""
to: "visible"
SequentialAnimation {
PropertyAction {
target: root
property: "shouldBeVisible"
}
NumberAnimation {
target: root
property: "width"
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
}
}
},
Transition {
from: "visible"
to: ""
SequentialAnimation {
NumberAnimation {
target: root
property: "width"
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasizedAccel
}
PropertyAction {
target: root
property: "shouldBeVisible"
}
}
}
]
}

View file

@ -1,8 +1,6 @@
pragma Singleton
import "root:/utils/scripts/fuzzysort.js" as Fuzzy
import "root:/widgets"
import "root:/config"
import Quickshell
import Quickshell.Io

25
services/Audio.qml Normal file
View file

@ -0,0 +1,25 @@
pragma Singleton
import Quickshell
import Quickshell.Services.Pipewire
Singleton {
id: root
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
readonly property bool muted: sink?.audio?.muted ?? false
readonly property real volume: sink?.audio?.volume ?? 0
function setVolume(volume: real): void {
if (sink?.ready && sink?.audio) {
sink.audio.muted = false;
sink.audio.volume = volume;
}
}
PwObjectTracker {
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
}
}

View file

@ -1,8 +1,10 @@
import "modules/bar"
import "modules/launcher"
import "modules/osd"
import Quickshell
ShellRoot {
Bar {}
Launcher {}
Osd {}
}

130
widgets/VerticalSlider.qml Normal file
View file

@ -0,0 +1,130 @@
import "root:/widgets"
import "root:/config"
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
Slider {
id: root
required property string icon
orientation: Qt.Vertical
background: StyledRect {
color: Appearance.alpha(Appearance.colours.m3surfaceContainerHigh, true)
radius: Appearance.rounding.full
StyledRect {
anchors.left: parent.left
anchors.right: parent.right
y: root.handle.y
implicitHeight: parent.height - y
color: Appearance.alpha(Appearance.colours.m3secondary, true)
radius: Appearance.rounding.full
}
}
handle: Item {
id: handle
property bool moving
y: root.visualPosition * (root.availableHeight - height)
implicitWidth: root.width
implicitHeight: root.width
RectangularShadow {
anchors.fill: parent
radius: rect.radius
color: Appearance.colours.m3shadow
blur: 5
spread: 0
}
StyledRect {
id: rect
anchors.fill: parent
color: Appearance.alpha(Appearance.colours.m3inverseSurface, true)
radius: Appearance.rounding.full
MaterialIcon {
id: icon
animate: true
text: root.icon
color: Appearance.colours.m3onPrimary
anchors.centerIn: parent
states: State {
name: "value"
when: handle.moving
PropertyChanges {
icon.animate: false
icon.text: Math.round(root.value * 100)
icon.font.pointSize: Appearance.font.size.small
icon.font.family: Appearance.font.family.sans
}
}
transitions: Transition {
from: "*"
to: "*"
NumberAnimation {
target: icon
property: "scale"
from: 1
to: 0
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
PropertyAction {
target: icon
properties: "animate,text,font.pointSize,font.family"
}
NumberAnimation {
target: icon
property: "scale"
from: 0
to: 1
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
}
}
}
onPressedChanged: handle.moving = pressed
onValueChanged: {
handle.moving = true;
stateChangeDelay.restart();
}
Timer {
id: stateChangeDelay
interval: 500
onTriggered: {
if (!root.pressed)
handle.moving = false;
}
}
Behavior on value {
NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}