diff --git a/Modules/Audio/Cava.qml b/Modules/Audio/Cava.qml new file mode 100644 index 0000000..d0c53b4 --- /dev/null +++ b/Modules/Audio/Cava.qml @@ -0,0 +1,77 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Services + +Scope { + id: root + property int count: 44 + property int noiseReduction: 60 + property string channels: "mono" + property string monoOption: "average" + + property var config: ({ + "general": { + "bars": count, + "framerate": 30, + "autosens": 1 + }, + "smoothing": { + "monstercat": 1, + "gravity": 1000000, + "noise_reduction": noiseReduction + }, + "output": { + "method": "raw", + "bit_format": 8, + "channels": channels, + "mono_option": monoOption + } + }) + + property var values: Array(count).fill(0) + + Process { + id: process + property int index: 0 + stdinEnabled: true + running: MediaPlayer.isPlaying + command: ["cava", "-p", "/dev/stdin"] + onExited: { + stdinEnabled = true + index = 0 + values = Array(count).fill(0) + } + onStarted: { + for (const k in config) { + if (typeof config[k] !== "object") { + write(k + "=" + config[k] + "\n") + continue + } + write("[" + k + "]\n") + const obj = config[k] + for (const k2 in obj) { + write(k2 + "=" + obj[k2] + "\n") + } + } + stdinEnabled = false + } + stdout: SplitParser { + splitMarker: "" + onRead: data => { + const newValues = Array(count).fill(0) + for (var i = 0; i < values.length; i++) { + newValues[i] = values[i] + } + if (process.index + data.length > count) { + process.index = 0 + } + for (var i = 0; i < data.length; i += 1) { + newValues[process.index] = Math.min(data.charCodeAt(i), 128) / 128 + process.index = (process.index + 1) % count + } + values = newValues + } + } + } +} diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index f00e09c..b65130a 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -6,125 +6,99 @@ 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 getIcon() { + if (PipeWireAudio.muted) { + return "volume_off" + } + return PipeWireAudio.volume === 0 ? "volume_off" : (PipeWireAudio.volume < 0.33 ? "volume_down" : "volume_up") } function getIconColor() { - if (volume <= 100) + if (PipeWireAudio.volume <= 1.0) { return Colors.textPrimary - return getVolumeColor() // Only use warning blend when >100% + } + + // Indicate that the volume is over 100% + // Calculate interpolation factor (0 at 100%, 1.0 at 200%) + let factor = (PipeWireAudio.volume - 1) + + // Blend between accent and warning colors + return Qt.rgba(Colors.textPrimary.r + (Colors.warning.r - Colors.textPrimary.r) * factor, + Colors.textPrimary.g + (Colors.warning.g - Colors.textPrimary.g) * factor, + Colors.textPrimary.b + (Colors.warning.b - Colors.textPrimary.b) * factor, 1) } 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 + "%" + icon: getIcon() + text: Math.round(PipeWireAudio.volume * 100) + "%" + tooltipText: "Volume: " + Math.round( + PipeWireAudio.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." + onClicked: function () { + console.log("onClicked") + //if (ioSelector.visible) { + // ioSelector.dismiss(); + // } else { + // ioSelector.show(); + // } + } - 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(); - // } - // } - // } + // pillColor: Colors.surfaceVariant + // iconCircleColor: Colors.// getVolumeColor() + // iconTextColor: Colors.backgroundPrimary + // textColor: Colors.textPrimary + // collapsedIconColor: getIconColor() + // autoHide: true } - 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() - } - } - } - } + AudioDeviceSelector { + id: ioSelector + // onPanelClosed: ioSelector.dismiss() } - Component.onCompleted: { - if (shell && shell.volume !== undefined) { - volume = Math.max(0, Math.min(200, shell.volume)) - } - } + // Connections { + // target: PipeWireAudio + // function onVolumeChanged() { + // console.log("onVolumeChanged") + // } - 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)) - } - } - } + // function onSinkChanged() { + // console.log("onSinkChanged") + // } - // AudioDeviceSelector { - // id: ioSelector - // onPanelClosed: ioSelector.dismiss() + // } + + // 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)) + // } + // } // } - property bool containsMouse: false + + // property bool containsMouse: false } diff --git a/Services/MediaPlayer.qml b/Services/MediaPlayer.qml index 806142c..cae8464 100644 --- a/Services/MediaPlayer.qml +++ b/Services/MediaPlayer.qml @@ -1,161 +1,162 @@ pragma Singleton +import QtQuick import Quickshell import Quickshell.Services.Mpris import qs.Services +import qs.Modules.Audio Singleton { id: root - // property var currentPlayer: null - // property real currentPosition: 0 - // property int selectedPlayerIndex: 0 - // property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false - // property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : "" - // property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : "" - // property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : "" - // property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : "" - // property real trackLength: currentPlayer ? currentPlayer.length : 0 - // property bool canPlay: currentPlayer ? currentPlayer.canPlay : false - // property bool canPause: currentPlayer ? currentPlayer.canPause : false - // property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false - // property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false - // property bool canSeek: currentPlayer ? currentPlayer.canSeek : false - // property bool hasPlayer: getAvailablePlayers().length > 0 + property var currentPlayer: null + property real currentPosition: 0 + property int selectedPlayerIndex: 0 + property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false + property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : "" + property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : "" + property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : "" + property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : "" + property real trackLength: currentPlayer ? currentPlayer.length : 0 + property bool canPlay: currentPlayer ? currentPlayer.canPlay : false + property bool canPause: currentPlayer ? currentPlayer.canPause : false + property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false + property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false + property bool canSeek: currentPlayer ? currentPlayer.canSeek : false + property bool hasPlayer: getAvailablePlayers().length > 0 - // Item { - // Component.onCompleted: { - // updateCurrentPlayer() - // } - // } + // Expose cava values + property alias cavaValues: cava.values - // function getAvailablePlayers() { - // if (!Mpris.players || !Mpris.players.values) { - // return [] - // } + Item { + Component.onCompleted: { + updateCurrentPlayer() + } + } - // let allPlayers = Mpris.players.values - // let controllablePlayers = [] + function getAvailablePlayers() { + if (!Mpris.players || !Mpris.players.values) { + return [] + } - // for (var i = 0; i < allPlayers.length; i++) { - // let player = allPlayers[i] - // if (player && player.canControl) { - // controllablePlayers.push(player) - // } - // } + let allPlayers = Mpris.players.values + let controllablePlayers = [] - // return controllablePlayers - // } + for (var i = 0; i < allPlayers.length; i++) { + let player = allPlayers[i] + if (player && player.canControl) { + controllablePlayers.push(player) + } + } - // function findActivePlayer() { - // let availablePlayers = getAvailablePlayers() - // if (availablePlayers.length === 0) { - // return null - // } + return controllablePlayers + } - // if (selectedPlayerIndex < availablePlayers.length) { - // return availablePlayers[selectedPlayerIndex] - // } else { - // selectedPlayerIndex = 0 - // return availablePlayers[0] - // } - // } + function findActivePlayer() { + let availablePlayers = getAvailablePlayers() + if (availablePlayers.length === 0) { + return null + } - // // Switch to the most recently active player - // function updateCurrentPlayer() { - // let newPlayer = findActivePlayer() - // if (newPlayer !== currentPlayer) { - // currentPlayer = newPlayer - // currentPosition = currentPlayer ? currentPlayer.position : 0 - // } - // } + if (selectedPlayerIndex < availablePlayers.length) { + return availablePlayers[selectedPlayerIndex] + } else { + selectedPlayerIndex = 0 + return availablePlayers[0] + } + } - // function playPause() { - // if (currentPlayer) { - // if (currentPlayer.isPlaying) { - // currentPlayer.pause() - // } else { - // currentPlayer.play() - // } - // } - // } + // Switch to the most recently active player + function updateCurrentPlayer() { + let newPlayer = findActivePlayer() + if (newPlayer !== currentPlayer) { + currentPlayer = newPlayer + currentPosition = currentPlayer ? currentPlayer.position : 0 + } + } - // function play() { - // if (currentPlayer && currentPlayer.canPlay) { - // currentPlayer.play() - // } - // } + function playPause() { + if (currentPlayer) { + if (currentPlayer.isPlaying) { + currentPlayer.pause() + } else { + currentPlayer.play() + } + } + } - // function pause() { - // if (currentPlayer && currentPlayer.canPause) { - // currentPlayer.pause() - // } - // } + function play() { + if (currentPlayer && currentPlayer.canPlay) { + currentPlayer.play() + } + } - // function next() { - // if (currentPlayer && currentPlayer.canGoNext) { - // currentPlayer.next() - // } - // } + function pause() { + if (currentPlayer && currentPlayer.canPause) { + currentPlayer.pause() + } + } - // function previous() { - // if (currentPlayer && currentPlayer.canGoPrevious) { - // currentPlayer.previous() - // } - // } + function next() { + if (currentPlayer && currentPlayer.canGoNext) { + currentPlayer.next() + } + } - // function seek(position) { - // if (currentPlayer && currentPlayer.canSeek) { - // currentPlayer.position = position - // currentPosition = position - // } - // } + function previous() { + if (currentPlayer && currentPlayer.canGoPrevious) { + currentPlayer.previous() + } + } - // // Seek to position based on ratio (0.0 to 1.0) - // function seekByRatio(ratio) { - // if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) { - // let seekPosition = ratio * currentPlayer.length - // currentPlayer.position = seekPosition - // currentPosition = seekPosition - // } - // } + function seek(position) { + if (currentPlayer && currentPlayer.canSeek) { + currentPlayer.position = position + currentPosition = position + } + } - // // Update progress bar every second while playing - // Timer { - // id: positionTimer - // interval: 1000 - // running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0 - // && currentPlayer.playbackState === MprisPlaybackState.Playing - // repeat: true - // onTriggered: { - // if (currentPlayer && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) { - // currentPosition = currentPlayer.position - // } else { - // running = false - // } - // } - // } + // Seek to position based on ratio (0.0 to 1.0) + function seekByRatio(ratio) { + if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) { + let seekPosition = ratio * currentPlayer.length + currentPlayer.position = seekPosition + currentPosition = seekPosition + } + } - // // Reset position when switching to inactive player - // onCurrentPlayerChanged: { - // if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { - // currentPosition = 0 - // } - // } + // Update progress bar every second while playing + Timer { + id: positionTimer + interval: 1000 + running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0 + && currentPlayer.playbackState === MprisPlaybackState.Playing + repeat: true + onTriggered: { + if (currentPlayer && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) { + currentPosition = currentPlayer.position + } else { + running = false + } + } + } - // // Update current player when available players change - // Connections { - // target: Mpris.players - // function onValuesChanged() { - // updateCurrentPlayer() - // } - // } + // Reset position when switching to inactive player + onCurrentPlayerChanged: { + if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { + currentPosition = 0 + } + } - // Cava { - // id: cava - // count: 44 - // } + // Update current player when available players change + Connections { + target: Mpris.players + function onValuesChanged() { + updateCurrentPlayer() + } + } - // // Expose cava values - // property alias cavaValues: cava.values + Cava { + id: cava + } } diff --git a/Services/PipeWireAudio.qml b/Services/PipeWireAudio.qml new file mode 100644 index 0000000..8ef7104 --- /dev/null +++ b/Services/PipeWireAudio.qml @@ -0,0 +1,30 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire + +Singleton { + id: root + + // Ensure the volume is readonly from outside + readonly property alias volume: root._volume + property real _volume: 0 + + readonly property alias muted: root._muted + property bool _muted: false + + PwObjectTracker { + objects: [Pipewire.defaultAudioSink] + } + + Connections { + target: Pipewire.defaultAudioSink?.audio ? Pipewire.defaultAudioSink?.audio : null + + function onVolumeChanged() { + root._volume = (Pipewire.defaultAudioSink?.audio.volume ?? 0) + console.log("onVolumeChanged: " + volume) + } + } +} diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 21b3644..175e5e9 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -64,10 +64,10 @@ Rectangle { root.onExited() } onClicked: { - root.onClicked() if (tooltipText) { tooltip.hide() } + root.onClicked() } } } diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 892489c..9e98abc 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -18,6 +18,10 @@ Item { property real sizeMultiplier: 0.8 property bool autoHide: false + property var onEntered: function () {} + property var onExited: function () {} + property var onClicked: function () {} + // Internal state property bool showPill: false property bool shouldAnimateHide: false @@ -190,10 +194,15 @@ Item { onEntered: { showDelayed() tooltip.show() + root.onEntered() } onExited: { hide() tooltip.hide() + root.onExited() + } + onClicked: { + root.onClicked() } } diff --git a/shell.qml b/shell.qml index 66ae026..a1544a5 100644 --- a/shell.qml +++ b/shell.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Widgets +import Quickshell.Services.Pipewire import qs.Widgets import qs.Modules.Bar import qs.Modules.DemoPanel @@ -13,7 +14,7 @@ import qs.Modules.Notification import qs.Services ShellRoot { - id: root + id: shellRoot Background {} Overview {}