1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-02 02:22:06 -04:00

Compare commits

...

2 Commits

Author SHA1 Message Date
bbedward
722b3fd1e8 audio: defensive checks on PwNode objects 2026-04-15 14:16:45 -04:00
dev
2728296cbd README.md - Update AUR badge to Arch (#2228)
The AUR dms-shell-bin package is replaced by dms-shell in the Arch Extra package repository. The AUR package has been removed.
2026-04-15 13:26:23 -04:00
9 changed files with 65 additions and 35 deletions

View File

@@ -13,7 +13,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
[![GitHub stars](https://img.shields.io/github/stars/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=ffd700)](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
[![GitHub License](https://img.shields.io/github/license/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=b9c8da)](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/v/release/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/releases)
[![AUR version](https://img.shields.io/aur/version/dms-shell-bin?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://aur.archlinux.org/packages/dms-shell-bin)
[![Arch version](https://img.shields.io/archlinux/v/extra/x86_64/dms-shell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://archlinux.org/packages/extra/x86_64/dms-shell/)
[![AUR version (git)](<https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git)>)](https://aur.archlinux.org/packages/dms-shell-git)
[![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Fdanklinux)](https://ko-fi.com/danklinux)

View File

@@ -260,7 +260,7 @@ Column {
}
case "audioOutput":
{
if (!AudioService.sink)
if (!AudioService.sink?.audio)
return "volume_off";
let volume = AudioService.sink.audio.volume;
let muted = AudioService.sink.audio.muted;
@@ -276,7 +276,7 @@ Column {
}
case "audioInput":
{
if (!AudioService.source)
if (!AudioService.source?.audio)
return "mic_off";
let muted = AudioService.source.audio.muted;
return muted ? "mic_off" : "mic";
@@ -369,7 +369,7 @@ Column {
}
case "audioOutput":
{
if (!AudioService.sink)
if (!AudioService.sink?.audio)
return I18n.tr("Select device", "audio status");
if (AudioService.sink.audio.muted)
return I18n.tr("Muted", "audio status");
@@ -380,7 +380,7 @@ Column {
}
case "audioInput":
{
if (!AudioService.source)
if (!AudioService.source?.audio)
return I18n.tr("Select device", "audio status");
if (AudioService.source.audio.muted)
return I18n.tr("Muted", "audio status");
@@ -412,9 +412,9 @@ Column {
case "bluetooth":
return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled);
case "audioOutput":
return !!(AudioService.sink && !AudioService.sink.audio.muted);
return !!(AudioService.sink?.audio && !AudioService.sink.audio.muted);
case "audioInput":
return !!(AudioService.source && !AudioService.source.audio.muted);
return !!(AudioService.source?.audio && !AudioService.source.audio.muted);
default:
return false;
}

View File

@@ -351,8 +351,8 @@ Rectangle {
deviceRipple.trigger(mapped.x, mapped.y);
}
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSource = modelData;
if (modelData && modelData.name) {
AudioService.setDefaultSourceByName(modelData.name);
}
}
}

View File

@@ -355,8 +355,8 @@ Rectangle {
deviceRipple.trigger(mapped.x, mapped.y);
}
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSink = modelData;
if (modelData && modelData.name) {
AudioService.setDefaultSinkByName(modelData.name);
}
}
}

View File

@@ -35,7 +35,7 @@ Row {
cursorShape: Qt.PointingHandCursor
onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
onClicked: {
if (defaultSink) {
if (defaultSink?.audio) {
SessionData.suppressOSDTemporarily();
defaultSink.audio.muted = !defaultSink.audio.muted;
}
@@ -45,7 +45,7 @@ Row {
DankIcon {
anchors.centerIn: parent
name: {
if (!defaultSink)
if (!defaultSink?.audio)
return "volume_off";
let volume = defaultSink.audio.volume;
@@ -62,18 +62,18 @@ Row {
return "volume_up";
}
size: Theme.iconSize
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
color: defaultSink?.audio && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
id: volumeSlider
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
readonly property real actualVolumePercent: defaultSink?.audio ? Math.round(defaultSink.audio.volume * 100) : 0
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: defaultSink !== null
enabled: defaultSink?.audio != null
minimum: 0
maximum: AudioService.sinkMaxVolume
showValue: true
@@ -83,7 +83,7 @@ Row {
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
onSliderValueChanged: function (newValue) {
if (defaultSink) {
if (defaultSink?.audio) {
SessionData.suppressOSDTemporarily();
defaultSink.audio.volume = newValue / 100.0;
if (newValue > 0 && defaultSink.audio.muted) {
@@ -97,7 +97,7 @@ Row {
Binding {
target: volumeSlider
property: "value"
value: defaultSink ? Math.min(AudioService.sinkMaxVolume, Math.round(defaultSink.audio.volume * 100)) : 0
value: defaultSink?.audio ? Math.min(AudioService.sinkMaxVolume, Math.round(defaultSink.audio.volume * 100)) : 0
when: !volumeSlider.isDragging
}
}

View File

@@ -35,7 +35,7 @@ Row {
cursorShape: Qt.PointingHandCursor
onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
onClicked: {
if (defaultSource) {
if (defaultSource?.audio) {
SessionData.suppressOSDTemporarily();
defaultSource.audio.muted = !defaultSource.audio.muted;
}
@@ -45,7 +45,7 @@ Row {
DankIcon {
anchors.centerIn: parent
name: {
if (!defaultSource)
if (!defaultSource?.audio)
return "mic_off";
let volume = defaultSource.audio.volume;
@@ -56,26 +56,26 @@ Row {
return "mic";
}
size: Theme.iconSize
color: defaultSource && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText
color: defaultSource?.audio && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
readonly property real actualVolumePercent: defaultSource ? Math.round(defaultSource.audio.volume * 100) : 0
readonly property real actualVolumePercent: defaultSource?.audio ? Math.round(defaultSource.audio.volume * 100) : 0
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: defaultSource !== null
enabled: defaultSource?.audio != null
minimum: 0
maximum: 100
value: defaultSource ? Math.min(100, Math.round(defaultSource.audio.volume * 100)) : 0
value: defaultSource?.audio ? Math.min(100, Math.round(defaultSource.audio.volume * 100)) : 0
showValue: true
unit: "%"
valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceContainer
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
onSliderValueChanged: function (newValue) {
if (defaultSource) {
if (defaultSource?.audio) {
SessionData.suppressOSDTemporarily();
defaultSource.audio.volume = newValue / 100.0;
if (newValue > 0 && defaultSource.audio.muted) {

View File

@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
@@ -25,7 +24,7 @@ Item {
}
property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)
property bool volumeAvailable: !!((activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio))
property var availableDevices: {
const hidden = SessionData.hiddenOutputDeviceNames ?? [];
return Pipewire.nodes.values.filter(node => {
@@ -336,8 +335,8 @@ Item {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSink = modelData;
if (modelData && modelData.name) {
AudioService.setDefaultSinkByName(modelData.name);
root.deviceSelected(modelData);
}
}

View File

@@ -57,7 +57,7 @@ Item {
const id = activePlayer.identity.toLowerCase();
return id.includes("chrome") || id.includes("chromium");
}
readonly property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)
readonly property bool volumeAvailable: !!((activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio))
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)

View File

@@ -71,18 +71,48 @@ Singleton {
// Used in playLoginSoundIfApplicable()
Process {
id: loginSoundChecker
onExited: (exitCode) => {
onExited: exitCode => {
if (exitCode === 0) {
playLoginSound();
}
}
}
}
function getAvailableSinks() {
const hidden = SessionData.hiddenOutputDeviceNames ?? [];
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name));
}
// Resolve a PwNode by name from the live typed list and assign it as the
// default sink. Going through Pipewire.nodes.values directly (no .filter
// / spread / .sort / property var) avoids QML type erasure to QObject*,
// which newer quickshell rejects when assigning to preferredDefaultAudioSink.
function setDefaultSinkByName(name) {
if (!name)
return false;
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i];
if (node && node.name === name && node.audio && node.isSink && !node.isStream) {
Pipewire.preferredDefaultAudioSink = node;
return true;
}
}
return false;
}
function setDefaultSourceByName(name) {
if (!name)
return false;
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i];
if (node && node.name === name && node.audio && !node.isSink && !node.isStream) {
Pipewire.preferredDefaultAudioSource = node;
return true;
}
}
return false;
}
function cycleAudioOutput() {
const sinks = getAvailableSinks();
if (sinks.length < 2)
@@ -92,7 +122,8 @@ Singleton {
const currentIndex = sinks.findIndex(s => s.name === currentName);
const nextIndex = (currentIndex + 1) % sinks.length;
const nextSink = sinks[nextIndex];
Pipewire.preferredDefaultAudioSink = nextSink;
if (!setDefaultSinkByName(nextSink.name))
Pipewire.preferredDefaultAudioSink = nextSink;
const name = displayName(nextSink);
audioOutputCycled(name, sinkIcon(nextSink));
return name;
@@ -650,7 +681,6 @@ EOFCONFIG
}
}
`, root, "AudioService.LoginSound");
} catch (e) {
console.warn("AudioService: Error creating sound players:", e);
}
@@ -704,7 +734,8 @@ EOFCONFIG
const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR");
const sessionId = Quickshell.env("XDG_SESSION_ID") || "0";
if (!runtimeDir) return;
if (!runtimeDir)
return;
const loginFile = `${runtimeDir}/danklinux.login-${sessionId}`;