volume tabs

This commit is contained in:
Zacharias-Brohn
2026-01-26 18:52:04 +01:00
parent 2d5dd9cc14
commit 5d4e55a9c1
8 changed files with 391 additions and 129 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ Scope {
height: bar.screen.height - backgroundRect.implicitHeight
intersection: Intersection.Xor
regions: popoutRegions.instances
regions: panels.popouts.hasCurrent ? None : popoutRegions.instances
}
Variants {
+113
View File
@@ -74,6 +74,13 @@ Singleton {
Pipewire.preferredDefaultAudioSource = newSource;
}
function setAppAudioVolume(appStream: PwNode, newVolume: real): void {
if ( appStream?.ready && appStream?.audio ) {
appStream.audio.muted = false;
appStream.audio.volume = Math.max(0, Math.min(100, newVolume));
}
}
onSinkChanged: {
if (!sink?.ready)
return;
@@ -100,4 +107,110 @@ Singleton {
PwObjectTracker {
objects: [...root.sinks, ...root.sources]
}
PwNodeLinkTracker {
id: sinkLinkTracker
node: root.sink
}
PwObjectTracker {
objects: root.appStreams
}
readonly property var appStreams: {
var defaultSink = root.sink;
var defaultSinkId = defaultSink.id;
var connectedStreamIds = {};
var connectedStreams = [];
if ( !sinkLinkTracker.linkGroups ) {
return [];
}
var linkGroupsCount = 0;
if (sinkLinkTracker.linkGroups.length !== undefined) {
linkGroupsCount = sinkLinkTracker.linkGroups.length;
} else if (sinkLinkTracker.linkGroups.count !== undefined) {
linkGroupsCount = sinkLinkTracker.linkGroups.count;
} else {
return [];
}
if ( linkGroupsCount === 0 ) {
return [];
}
var intermediateNodeIds = {};
var nodesToCheck = [];
for (var i = 0; i < linkGroupsCount; i++) {
var linkGroup;
if (sinkLinkTracker.linkGroups.get) {
linkGroup = sinkLinkTracker.linkGroups.get(i);
} else {
linkGroup = sinkLinkTracker.linkGroups[i];
}
if (!linkGroup || !linkGroup.source) {
continue;
}
var sourceNode = linkGroup.source;
if (sourceNode.isStream && sourceNode.audio) {
if (!connectedStreamIds[sourceNode.id]) {
connectedStreamIds[sourceNode.id] = true;
connectedStreams.push(sourceNode);
}
} else {
intermediateNodeIds[sourceNode.id] = true;
nodesToCheck.push(sourceNode);
}
}
if (nodesToCheck.length > 0 || connectedStreams.length === 0) {
try {
var allNodes = [];
if (Pipewire.nodes) {
if (Pipewire.nodes.count !== undefined) {
var nodeCount = Pipewire.nodes.count;
for (var n = 0; n < nodeCount; n++) {
var node;
if (Pipewire.nodes.get) {
node = Pipewire.nodes.get(n);
} else {
node = Pipewire.nodes[n];
}
if (node)
allNodes.push(node);
}
} else if (Pipewire.nodes.values) {
allNodes = Pipewire.nodes.values;
}
}
for (var j = 0; j < allNodes.length; j++) {
var node = allNodes[j];
if (!node || !node.isStream || !node.audio) {
continue;
}
var streamId = node.id;
if (connectedStreamIds[streamId]) {
continue;
}
if (Object.keys(intermediateNodeIds).length > 0) {
connectedStreamIds[streamId] = true;
connectedStreams.push(node);
} else if (connectedStreams.length === 0) {
connectedStreamIds[streamId] = true;
connectedStreams.push(node);
}
}
} catch (e)
{}
}
return connectedStreams;
}
}
+11
View File
@@ -0,0 +1,11 @@
pragma Singleton
import Quickshell
import Quickshell.Networking
Singleton {
id: root
property list<NetworkDevice> devices: Networking.devices
property NetworkDevice activeDevice: devices.find(d => d.connected)
}
+199 -100
View File
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
@@ -17,21 +18,211 @@ Item {
required property var wrapper
ButtonGroup {
id: sinks
}
ButtonGroup {
id: sources
}
ColumnLayout {
id: layout
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: stack.currentItem ? stack.currentItem.childrenRect.height : 0
spacing: 12
RowLayout {
id: tabBar
spacing: 6
Layout.fillWidth: true
property int tabHeight: 36
CustomClippingRect {
radius: 6
Layout.fillWidth: true
Layout.preferredHeight: tabBar.tabHeight
color: stack.currentIndex === 0 ? DynamicColors.tPalette.m3primaryContainer : "transparent"
StateLayer {
function onClicked(): void {
stack.currentIndex = 0;
}
CustomText {
text: qsTr("Volumes")
anchors.centerIn: parent
}
}
}
CustomClippingRect {
radius: 6
Layout.fillWidth: true
Layout.preferredHeight: tabBar.tabHeight
color: stack.currentIndex === 1 ? DynamicColors.tPalette.m3primaryContainer : "transparent"
StateLayer {
function onClicked(): void {
stack.currentIndex = 1;
}
CustomText {
text: qsTr("Devices")
anchors.centerIn: parent
}
}
}
}
StackLayout {
id: stack
Layout.fillWidth: true
currentIndex: 0
VolumesTab {}
DevicesTab {}
}
}
component VolumesTab: ColumnLayout {
spacing: 12
CustomText {
text: qsTr("Output Volume (%1)")
.arg(Audio.muted
? qsTr("Muted")
: `${Math.round(Audio.volume * 100)}%`)
font.weight: 500
}
CustomMouseArea {
Layout.fillWidth: true
implicitHeight: 10
CustomSlider {
anchors.fill: parent
value: Audio.volume
onMoved: Audio.setVolume(value)
Behavior on value { Anim {} }
}
}
CustomText {
Layout.topMargin: 10
text: qsTr("Input Volume (%1)")
.arg(Audio.sourceMuted
? qsTr("Muted")
: `${Math.round(Audio.sourceVolume * 100)}%`)
font.weight: 500
}
CustomMouseArea {
Layout.fillWidth: true
implicitHeight: 10
CustomSlider {
anchors.fill: parent
value: Audio.sourceVolume
onMoved: Audio.setSourceVolume(value)
Behavior on value { Anim {} }
}
}
Repeater {
model: Audio.appStreams
Item {
id: appBox
Layout.topMargin: 10
Layout.fillWidth: true
Layout.preferredHeight: 42
visible: !isCaptureStream
required property PwNode modelData
PwObjectTracker {
objects: appBox.modelData ? [appBox.modelData] : []
}
readonly property bool isCaptureStream: {
if (!modelData || !modelData.properties)
return false;
const props = modelData.properties;
// Exclude capture streams - check for stream.capture.sink property
if (props["stream.capture.sink"] !== undefined) {
return true;
}
const mediaClass = props["media.class"] || "";
// Exclude Stream/Input (capture) but allow Stream/Output (playback)
if (mediaClass.includes("Capture") || mediaClass === "Stream/Input" || mediaClass === "Stream/Input/Audio") {
return true;
}
const mediaRole = props["media.role"] || "";
if (mediaRole === "Capture") {
return true;
}
return false;
}
RowLayout {
id: layoutVolume
anchors.fill: parent
spacing: 15
IconImage {
property string iconPath: Quickshell.iconPath(DesktopEntries.byId(appBox.modelData.name).icon)
source: iconPath !== "" ? iconPath : Quickshell.iconPath("application-x-executable")
Layout.alignment: Qt.AlignVCenter
implicitSize: 42
}
ColumnLayout {
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillHeight: true
TextMetrics {
id: metrics
text: appBox.modelData.properties["media.name"]
elide: Text.ElideRight
elideWidth: root.width - 50
}
CustomText {
text: metrics.elidedText
elide: Text.ElideRight
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
}
CustomMouseArea {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.bottomMargin: 5
implicitHeight: 10
CustomSlider {
anchors.fill: parent
value: appBox.modelData.audio.volume
onMoved: {
Audio.setAppAudioVolume(appBox.modelData, value)
console.log(layoutVolume.implicitHeight)
}
}
}
}
}
}
}
}
component DevicesTab: ColumnLayout {
spacing: 12
ButtonGroup { id: sinks }
ButtonGroup { id: sources }
CustomText {
text: qsTr("Output device")
font.weight: 500
@@ -41,8 +232,6 @@ Item {
model: Audio.sinks
CustomRadioButton {
id: control
required property PwNode modelData
ButtonGroup.group: sinks
@@ -70,95 +259,5 @@ Item {
text: modelData.description
}
}
CustomText {
Layout.topMargin: 10
Layout.bottomMargin: -7 / 2
text: qsTr("Output Volume (%1)").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`)
font.weight: 500
}
CustomMouseArea {
Layout.fillWidth: true
implicitHeight: 10
CustomSlider {
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: parent.implicitHeight
value: Audio.volume
onMoved: Audio.setVolume(value)
Behavior on value {
Anim {}
}
}
}
CustomText {
Layout.topMargin: 10
Layout.bottomMargin: -7 / 2
text: qsTr("Input Volume (%1)").arg(Audio.sourceMuted ? qsTr("Muted") : `${Math.round(Audio.sourceVolume * 100)}%`)
font.weight: 500
}
CustomMouseArea {
Layout.fillWidth: true
implicitHeight: 10
CustomSlider {
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: parent.implicitHeight
value: Audio.sourceVolume
onMoved: Audio.setSourceVolume(value)
Behavior on value {
Anim {}
}
}
}
CustomRect {
Layout.topMargin: 12
visible: true
implicitWidth: expandBtn.implicitWidth + 10 * 2
implicitHeight: expandBtn.implicitHeight + 5
radius: 4
color: DynamicColors.palette.m3primaryContainer
StateLayer {
color: DynamicColors.palette.m3onPrimaryContainer
function onClicked(): void {
Quickshell.execDetached(["app2unit", "--", "hyprpwcenter"]);
root.wrapper.hasCurrent = false;
}
}
RowLayout {
id: expandBtn
anchors.centerIn: parent
spacing: 7
CustomText {
Layout.leftMargin: 7
text: qsTr("Open settings")
color: DynamicColors.palette.m3onPrimaryContainer
}
MaterialIcon {
Layout.topMargin: 2
text: "chevron_right"
color: DynamicColors.palette.m3onPrimaryContainer
font.pointSize: 18
}
}
}
}
}
+1
View File
@@ -89,6 +89,7 @@ RowLayout {
delegate: WrappedLoader {
sourceComponent: TrayWidget {
bar: root.bar
popouts: root.popouts
}
}
}
+27 -1
View File
@@ -1,9 +1,35 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Networking
import QtQuick
import QtQuick.Layouts
import qs.Modules
import qs.Helpers
ColumnLayout {
Item {
id: root
required property var wrapper
ColumnLayout {
id: layout
spacing: 8
Repeater {
model: Network.devices
CustomRadioButton {
id: network
visible: modelData.name !== "lo"
required property NetworkDevice modelData
checked: Network.activeDevice?.name === modelData.name
onClicked:
text: modelData.description
}
}
}
}
+9 -1
View File
@@ -12,13 +12,21 @@ Item {
required property SystemTrayItem item
required property PanelWindow bar
required property int ind
required property Wrapper popouts
property bool hasLoaded: false
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if ( mouse.button === Qt.LeftButton ) {
root.item.activate();
} else if ( mouse.button === Qt.RightButton ) {
root.popouts.currentName = `traymenu${ root.ind }`;
root.popouts.currentCenter = Qt.binding( () => root.mapToItem( root.bar, root.implicitWidth / 2, 0 ).x );
root.popouts.hasCurrent = true;
}
}
}
+4
View File
@@ -12,6 +12,7 @@ Row {
anchors.bottom: parent.bottom
required property PanelWindow bar
required property Wrapper popouts
readonly property alias items: repeater
spacing: 0
@@ -22,6 +23,9 @@ Row {
TrayItem {
id: trayItem
required property SystemTrayItem modelData
required property int index
ind: index
popouts: root.popouts
implicitHeight: 34
implicitWidth: 28
item: modelData