From 5fd3c4a53ef55bcebbec60f730913fe92ae4f046 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 15:37:26 -0400 Subject: [PATCH] Working on volume --- Modules/Audio/AudioDeviceSelector.qml | 369 ++++++++++++++++++++++++++ Modules/Bar/Bar.qml | 7 + Modules/Bar/Volume.qml | 130 +++++++++ Services/Scaling.qml | 2 +- Services/Style.qml | 1 + Widgets/NPill.qml | 13 +- 6 files changed, 515 insertions(+), 7 deletions(-) create mode 100644 Modules/Audio/AudioDeviceSelector.qml create mode 100644 Modules/Bar/Volume.qml diff --git a/Modules/Audio/AudioDeviceSelector.qml b/Modules/Audio/AudioDeviceSelector.qml new file mode 100644 index 0000000..7cd5de6 --- /dev/null +++ b/Modules/Audio/AudioDeviceSelector.qml @@ -0,0 +1,369 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Services.Pipewire +import qs.Services +import qs.Widgets + +NPanel { + id: ioSelector + + // property int tabIndex: 0 + // property Item anchorItem: null + + // signal panelClosed() + + // function sinkNodes() { + // let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { + // return n.isSink && n.audio && n.isStream === false; + // }) : []; + // if (Pipewire.defaultAudioSink) + // nodes = nodes.slice().sort(function(a, b) { + // if (a.id === Pipewire.defaultAudioSink.id) + // return -1; + + // if (b.id === Pipewire.defaultAudioSink.id) + // return 1; + + // return 0; + // }); + + // return nodes; + // } + + // function sourceNodes() { + // let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { + // return !n.isSink && n.audio && n.isStream === false; + // }) : []; + // if (Pipewire.defaultAudioSource) + // nodes = nodes.slice().sort(function(a, b) { + // if (a.id === Pipewire.defaultAudioSource.id) + // return -1; + + // if (b.id === Pipewire.defaultAudioSource.id) + // return 1; + + // return 0; + // }); + + // return nodes; + // } + + // Component.onCompleted: { + // if (Pipewire.nodes && Pipewire.nodes.values) { + // for (var i = 0; i < Pipewire.nodes.values.length; ++i) { + // var n = Pipewire.nodes.values[i]; + // } + // } + // } + // Component.onDestruction: { + // } + // onVisibleChanged: { + // if (!visible) + // panelClosed(); + + // } + + // // Bind all Pipewire nodes so their properties are valid + // PwObjectTracker { + // id: nodeTracker + + // objects: Pipewire.nodes + // } + + // Rectangle { + // color: Theme.backgroundPrimary + // radius: 20 + // width: 340 + // height: 340 + // anchors.top: parent.top + // anchors.right: parent.right + // anchors.topMargin: 4 + // anchors.rightMargin: 4 + + // // Prevent closing when clicking in the panel bg + // MouseArea { + // anchors.fill: parent + // } + + // ColumnLayout { + // anchors.fill: parent + // anchors.margins: 16 + // spacing: 10 + + // // Tabs centered inside the window + // RowLayout { + // Layout.fillWidth: true + // Layout.alignment: Qt.AlignHCenter + // spacing: 0 + + // Tabs { + // id: ioTabs + + // tabsModel: [{ + // "label": "Output", + // "icon": "volume_up" + // }, { + // "label": "Input", + // "icon": "mic" + // }] + // currentIndex: tabIndex + // onTabChanged: { + // tabIndex = currentIndex; + // } + // } + + // } + + // // Add vertical space between tabs and entries + // Item { + // height: 36 + // Layout.fillWidth: true + // } + + // // Output Devices + // Flickable { + // id: sinkList + + // visible: tabIndex === 0 + // contentHeight: sinkColumn.height + // clip: true + // interactive: contentHeight > height + // width: parent.width + // height: 220 + + // ColumnLayout { + // id: sinkColumn + + // width: sinkList.width + // spacing: 6 + + // Repeater { + // model: ioSelector.sinkNodes() + + // Rectangle { + // width: parent.width + // height: 36 + // color: "transparent" + // radius: 6 + + // RowLayout { + // anchors.fill: parent + // anchors.margins: 6 + // spacing: 8 + + // Text { + // text: "volume_up" + // font.family: "Material Symbols Outlined" + // font.pixelSize: 16 * Theme.scale(screen) + // color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary + // Layout.alignment: Qt.AlignVCenter + // } + + // ColumnLayout { + // Layout.fillWidth: true + // spacing: 1 + // Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button + + // Text { + // text: modelData.nickname || modelData.description || modelData.name + // font.bold: true + // font.pixelSize: 12 * Theme.scale(screen) + // color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary + // elide: Text.ElideRight + // maximumLineCount: 1 + // Layout.fillWidth: true + // } + + // Text { + // text: modelData.description !== modelData.nickname ? modelData.description : "" + // font.pixelSize: 10 * Theme.scale(screen) + // color: Theme.textSecondary + // elide: Text.ElideRight + // maximumLineCount: 1 + // Layout.fillWidth: true + // } + + // } + + // Rectangle { + // visible: Pipewire.preferredDefaultAudioSink !== modelData + // width: 60 + // height: 20 + // radius: 4 + // color: Theme.accentPrimary + // border.color: Theme.accentPrimary + // border.width: 1 + // Layout.alignment: Qt.AlignVCenter + + // Text { + // anchors.centerIn: parent + // text: "Set" + // color: Theme.onAccent + // font.pixelSize: 10 * Theme.scale(screen) + // font.bold: true + // } + + // MouseArea { + // anchors.fill: parent + // cursorShape: Qt.PointingHandCursor + // onClicked: Pipewire.preferredDefaultAudioSink = modelData + // } + + // } + + // Text { + // text: "(Current)" + // visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id + // color: Theme.accentPrimary + // font.pixelSize: 10 * Theme.scale(screen) + // Layout.alignment: Qt.AlignVCenter + // } + + // } + + // } + + // } + + // } + + // ScrollBar.vertical: ScrollBar { + // } + + // } + + // // Input Devices + // Flickable { + // id: sourceList + + // visible: tabIndex === 1 + // contentHeight: sourceColumn.height + // clip: true + // interactive: contentHeight > height + // width: parent.width + // height: 220 + + // ColumnLayout { + // id: sourceColumn + + // width: sourceList.width + // spacing: 6 + + // Repeater { + // model: ioSelector.sourceNodes() + + // Rectangle { + // width: parent.width + // height: 36 + // color: "transparent" + // radius: 6 + + // RowLayout { + // anchors.fill: parent + // anchors.margins: 6 + // spacing: 8 + + // Text { + // text: "mic" + // font.family: "Material Symbols Outlined" + // font.pixelSize: 16 * Theme.scale(screen) + // color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary + // Layout.alignment: Qt.AlignVCenter + // } + + // ColumnLayout { + // Layout.fillWidth: true + // spacing: 1 + // Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button + + // Text { + // text: modelData.nickname || modelData.description || modelData.name + // font.bold: true + // font.pixelSize: 12 * Theme.scale(screen) + // color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary + // elide: Text.ElideRight + // maximumLineCount: 1 + // Layout.fillWidth: true + // } + + // Text { + // text: modelData.description !== modelData.nickname ? modelData.description : "" + // font.pixelSize: 10 * Theme.scale(screen) + // color: Theme.textSecondary + // elide: Text.ElideRight + // maximumLineCount: 1 + // Layout.fillWidth: true + // } + + // } + + // Rectangle { + // visible: Pipewire.preferredDefaultAudioSource !== modelData + // width: 60 + // height: 20 + // radius: 4 + // color: Theme.accentPrimary + // border.color: Theme.accentPrimary + // border.width: 1 + // Layout.alignment: Qt.AlignVCenter + + // Text { + // anchors.centerIn: parent + // text: "Set" + // color: Theme.onAccent + // font.pixelSize: 10 * Theme.scale(screen) + // font.bold: true + // } + + // MouseArea { + // anchors.fill: parent + // cursorShape: Qt.PointingHandCursor + // onClicked: Pipewire.preferredDefaultAudioSource = modelData + // } + + // } + + // Text { + // text: "(Current)" + // visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id + // color: Theme.accentPrimary + // font.pixelSize: 10 * Theme.scale(screen) + // Layout.alignment: Qt.AlignVCenter + // } + + // } + + // } + + // } + + // } + + // ScrollBar.vertical: ScrollBar { + // } + + // } + + // } + + // } + + // Connections { + // function onReadyChanged() { + // if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) { + // for (var i = 0; i < Pipewire.nodes.values.length; ++i) { + // var n = Pipewire.nodes.values[i]; + // } + // } + // } + + // function onDefaultAudioSinkChanged() { + // } + + // function onDefaultAudioSourceChanged() { + // } + + // target: Pipewire + // } +} diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index c9c8365..f0f1b89 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -38,6 +38,7 @@ Variants { layer.enabled: true } + // Left Row { id: leftSection @@ -53,6 +54,7 @@ Variants { } } + // Center Row { id: centerSection @@ -64,6 +66,7 @@ Variants { Workspace {} } + // Right Row { id: rightSection @@ -77,6 +80,10 @@ Variants { anchors.verticalCenter: parent.verticalCenter } + Volume { + anchors.verticalCenter: parent.verticalCenter + } + Clock { anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml new file mode 100644 index 0000000..f00e09c --- /dev/null +++ b/Modules/Bar/Volume.qml @@ -0,0 +1,130 @@ +import QtQuick +import Quickshell +import qs.Services +import qs.Modules.Audio +import qs.Widgets + +Item { + id: volumeDisplay + property var shell + property int volume: 0 + property bool firstChange: true + + width: pillIndicator.width + height: pillIndicator.height + + function getVolumeColor() { + if (volume <= 100) + return Colors.accentPrimary + // Calculate interpolation factor (0 at 100%, 1 at 200%) + var factor = (volume - 100) / 100 + // Blend between accent and warning colors + return Qt.rgba(Colors.accentPrimary.r + (Colors.warning.r - Colors.accentPrimary.r) * factor, + Colors.accentPrimary.g + (Colors.warning.g - Colors.accentPrimary.g) * factor, + Colors.accentPrimary.b + (Colors.warning.b - Colors.accentPrimary.b) * factor, 1) + } + + function getIconColor() { + if (volume <= 100) + return Colors.textPrimary + return getVolumeColor() // Only use warning blend when >100% + } + + NPill { + id: pillIndicator + icon: shell && shell.defaultAudioSink && shell.defaultAudioSink.audio + && shell.defaultAudioSink.audio.muted ? "volume_off" : (volume === 0 ? "volume_off" : (volume < 30 ? "volume_down" : "volume_up")) + text: volume + "%" + + pillColor: Colors.surfaceVariant + iconCircleColor: getVolumeColor() + iconTextColor: Colors.backgroundPrimary + textColor: Colors.textPrimary + collapsedIconColor: getIconColor() + autoHide: true + + // StyledTooltip { + // id: volumeTooltip + // text: "Volume: " + volume + "%\nLeft click for advanced settings.\nScroll up/down to change volume." + // positionAbove: false + // tooltipVisible: !ioSelector.visible && volumeDisplay.containsMouse + // targetItem: pillIndicator + // delay: 1500 + // } + + // MouseArea { + // anchors.fill: parent + // hoverEnabled: true + // cursorShape: Qt.PointingHandCursor + // onClicked: { + // if (ioSelector.visible) { + // ioSelector.dismiss(); + // } else { + // ioSelector.show(); + // } + // } + // } + } + + Connections { + target: shell ?? null + function onVolumeChanged() { + if (shell) { + const clampedVolume = Math.max(0, Math.min(200, shell.volume)) + if (clampedVolume !== volume) { + volume = clampedVolume + pillIndicator.text = volume + "%" + pillIndicator.icon = shell.defaultAudioSink && shell.defaultAudioSink.audio + && shell.defaultAudioSink.audio.muted ? "volume_off" : (volume === 0 ? "volume_off" : (volume + < 30 ? "volume_down" : "volume_up")) + + if (firstChange) { + firstChange = false + } else { + pillIndicator.show() + } + } + } + } + } + + Component.onCompleted: { + if (shell && shell.volume !== undefined) { + volume = Math.max(0, Math.min(200, shell.volume)) + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + onEntered: { + volumeDisplay.containsMouse = true + pillIndicator.autoHide = false + pillIndicator.showDelayed() + } + onExited: { + volumeDisplay.containsMouse = false + pillIndicator.autoHide = true + pillIndicator.hide() + } + cursorShape: Qt.PointingHandCursor + onWheel: wheel => { + if (!shell) + return + let step = 5 + if (wheel.angleDelta.y > 0) { + shell.updateVolume(Math.min(200, shell.volume + step)) + } else if (wheel.angleDelta.y < 0) { + shell.updateVolume(Math.max(0, shell.volume - step)) + } + } + } + + // AudioDeviceSelector { + // id: ioSelector + // onPanelClosed: ioSelector.dismiss() + // } + property bool containsMouse: false +} diff --git a/Services/Scaling.qml b/Services/Scaling.qml index 912cee5..479b16c 100644 --- a/Services/Scaling.qml +++ b/Services/Scaling.qml @@ -57,6 +57,6 @@ Singleton { } // 3) Safe default - return 1.0 + return 1.4 } } diff --git a/Services/Style.qml b/Services/Style.qml index 44c2031..d737c72 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -62,6 +62,7 @@ Singleton { property int marginXL: 20 // Opacity + property real opacityNone: 0.0 property real opacityLight: 0.25 property real opacityMedium: 0.5 property real opacityHeavy: 0.75 diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index a809bfd..892489c 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -25,7 +25,7 @@ Item { // Exposed width logic readonly property int pillHeight: Style.baseWidgetSize * sizeMultiplier * scaling readonly property int iconSize: Style.baseWidgetSize * sizeMultiplier * scaling - readonly property int pillPaddingHorizontal: 14 * scaling + readonly property int pillPaddingHorizontal: Style.marginMedium * scaling readonly property int pillOverlap: iconSize * 0.5 readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) @@ -41,7 +41,7 @@ Item { width: showPill ? maxPillWidth : 1 height: pillHeight x: (iconCircle.x + iconCircle.width / 2) - width - opacity: showPill ? 1 : 0 + opacity: showPill ? Style.opacityFull : Style.opacityNone color: pillColor topLeftRadius: pillHeight * 0.5 bottomLeftRadius: pillHeight * 0.5 @@ -68,7 +68,7 @@ Item { Behavior on opacity { enabled: showAnim.running || hideAnim.running NumberAnimation { - duration: 250 + duration: Style.animationNormal easing.type: Easing.OutCubic } } @@ -78,14 +78,14 @@ Item { id: iconCircle width: iconSize height: iconSize - radius: width / 2 + radius: width * 0.5 color: showPill ? iconCircleColor : "transparent" anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right Behavior on color { ColorAnimation { - duration: 200 + duration: Style.animationNormal easing.type: Easing.InOutQuad } } @@ -134,8 +134,9 @@ Item { duration: 2500 } ScriptAction { - script: if (shouldAnimateHide) + script: if (shouldAnimateHide) { hideAnim.start() + } } }