volume tabs
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ RowLayout {
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: TrayWidget {
|
||||
bar: root.bar
|
||||
popouts: root.popouts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user