Files
dotfiles/quickshell/popouts/AudioPopout.qml
T
2026-08-11 17:35:26 +02:00

283 lines
8.5 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
import "root:/config"
import "root:/components"
import "root:/services"
// Audio is the expressive vertical slice for rail popouts: output owns the
// composition, input is a clear secondary band, and application streams recede
// into a compact mixer below both device controls.
Popout {
id: root
contentWidth: 400
contentHeight: surface.implicitContentHeight + Theme.padM * 2
initialFocusItem: outputMuteButton
PopoutSurface {
id: surface
anchors.fill: parent
tone: Theme.primary
readonly property real implicitContentHeight: header.height + column.implicitHeight + Theme.padM * 2
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "Audio"
icon: Audio.icon
accent: Theme.primary
subtitle: Audio.sinkName
}
Column {
id: column
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.padM
// ------------------------------------------------------- output
AudioOutputHero {
width: parent.width
}
Column {
width: parent.width
spacing: Theme.padS
P5Button {
id: outputMuteButton
width: parent.width
text: Audio.muted ? "Unmute output" : "Mute output"
icon: Audio.icon
onClicked: Audio.toggleMute()
}
P5Button {
width: parent.width
text: "Open audio mixer"
icon: "󰕾"
onClicked: Actions.openAudioSettings()
}
}
// -------------------------------------------------------- input
SectionRule {
width: parent.width
text: "Input"
}
AudioInputBand {
width: parent.width
}
SectionRule {
width: parent.width
text: "Outputs"
visible: Audio.sinks.length > 1
}
Repeater {
model: Audio.sinks.length > 1 ? Audio.sinks : []
PickRow {
required property var modelData
width: column.width
label: modelData.nickname || modelData.description || modelData.name
icon: "󰓃"
selected: Audio.sink === modelData
onClicked: Audio.setSink(modelData)
}
}
SectionRule {
width: parent.width
text: "Applications"
visible: Audio.streams.length > 0
}
Repeater {
model: Audio.streams
LevelRow {
required property var modelData
width: column.width
icon: "󰝚"
label: Audio.streamName(modelData)
readout: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : ""
value: modelData.audio ? modelData.audio.volume : 0
muted: modelData.audio ? modelData.audio.muted : false
accent: Theme.glow
onMoved: v => { if (modelData.audio) modelData.audio.volume = v; }
onToggled: { if (modelData.audio) modelData.audio.muted = !modelData.audio.muted; }
}
}
}
}
// ------------------------------------------------------------- pieces
component LevelRow: Item {
id: levelRow
property string icon: ""
property string label: ""
property string readout: ""
property real value: 0
property bool muted: false
property color accent: Theme.primary
signal moved(real value)
signal toggled()
implicitHeight: 42
Row {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.padS
Icon {
id: levelIcon
anchors.verticalCenter: parent.verticalCenter
text: levelRow.icon
color: levelRow.muted ? Theme.accent : levelRow.accent
font.pixelSize: Theme.fsLarge
TapHandler { onTapped: levelRow.toggled() }
HoverHandler { cursorShape: Qt.PointingHandCursor }
}
Text {
anchors.verticalCenter: parent.verticalCenter
width: levelRow.width - levelIcon.width - readoutText.width - Theme.padS * 3
text: levelRow.label
elide: Text.ElideRight
color: Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
font.italic: true
renderType: Text.NativeRendering
}
Text {
id: readoutText
anchors.verticalCenter: parent.verticalCenter
text: levelRow.readout
color: levelRow.muted ? Theme.accent : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
renderType: Text.NativeRendering
}
}
P5Slider {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
value: levelRow.value
accent: levelRow.accent
enabled: !levelRow.muted
onMoved: v => levelRow.moved(v)
}
}
component SectionRule: Item {
property string text: ""
implicitHeight: visible ? 18 : 0
Text {
id: sectionLabel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: parent.text.toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Rectangle {
anchors.left: sectionLabel.right
anchors.leftMargin: Theme.padS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Theme.alpha(Theme.outline, 0.6)
}
}
component PickRow: Item {
id: pickRow
property string label: ""
property string icon: ""
property bool selected: false
signal clicked()
implicitHeight: 26
Rectangle {
anchors.fill: parent
color: pickHover.hovered
? Theme.alpha(Theme.primary, 0.14)
: (pickRow.selected ? Theme.alpha(Theme.primary, 0.07) : "transparent")
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: pickRow.selected ? 18 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: pickRow.icon
color: pickRow.selected ? Theme.primary : Theme.muted
font.pixelSize: Theme.fsBody
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: pickRow.label
color: pickRow.selected ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
renderType: Text.NativeRendering
}
}
HoverHandler {
id: pickHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: pickRow.clicked()
}
}
}