add noctalia and fuzzel

This commit is contained in:
2026-05-23 21:20:58 +02:00
parent 364801f1a3
commit 9a3eceb3ab
599 changed files with 204318 additions and 0 deletions
@@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.volumes")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.devices")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.media")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
NTabButton {
text: I18n.tr("common.visualizer")
tabIndex: 3
checked: subTabBar.currentIndex === 3
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
VolumesSubTab {}
DevicesSubTab {}
MediaSubTab {}
VisualizerSubTab {}
}
}
@@ -0,0 +1,70 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Services.Media
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
// Output Devices
ButtonGroup {
id: sinks
}
ColumnLayout {
spacing: Style.marginXS
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL
NLabel {
label: I18n.tr("panels.audio.devices-output-device-label")
description: I18n.tr("panels.audio.devices-output-device-description")
}
Repeater {
model: AudioService.sinks
NRadioButton {
ButtonGroup.group: sinks
required property PwNode modelData
text: modelData.description
checked: AudioService.sink?.id === modelData.id
onClicked: {
AudioService.setAudioSink(modelData);
}
Layout.fillWidth: true
}
}
}
// Input Devices
ButtonGroup {
id: sources
}
ColumnLayout {
spacing: Style.marginXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.audio.devices-input-device-label")
description: I18n.tr("panels.audio.devices-input-device-description")
}
Repeater {
model: AudioService.sources
NRadioButton {
ButtonGroup.group: sources
required property PwNode modelData
text: modelData.description
checked: AudioService.source?.id === modelData.id
onClicked: AudioService.setAudioSource(modelData)
Layout.fillWidth: true
}
}
}
}
@@ -0,0 +1,104 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Media
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
// Preferred player
NTextInput {
label: I18n.tr("panels.audio.media-primary-player-label")
description: I18n.tr("panels.audio.media-primary-player-description")
placeholderText: I18n.tr("panels.audio.media-primary-player-placeholder")
text: Settings.data.audio.preferredPlayer
defaultValue: Settings.getDefaultValue("audio.preferredPlayer")
onTextChanged: {
Settings.data.audio.preferredPlayer = text;
MediaService.updateCurrentPlayer();
}
}
// Blacklist editor
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NTextInputButton {
id: blacklistInput
label: I18n.tr("panels.audio.media-excluded-player-label")
description: I18n.tr("panels.audio.media-excluded-player-description")
placeholderText: I18n.tr("panels.audio.media-excluded-player-placeholder")
buttonIcon: "add"
Layout.fillWidth: true
onButtonClicked: {
const val = (blacklistInput.text || "").trim();
if (val !== "") {
const arr = (Settings.data.audio.mprisBlacklist || []);
if (!arr.find(x => String(x).toLowerCase() === val.toLowerCase())) {
Settings.data.audio.mprisBlacklist = [...arr, val];
blacklistInput.text = "";
MediaService.updateCurrentPlayer();
}
}
}
}
// Current blacklist entries
Flow {
Layout.fillWidth: true
Layout.leftMargin: Style.marginS
spacing: Style.marginS
Repeater {
model: Settings.data.audio.mprisBlacklist
delegate: Rectangle {
required property string modelData
property real pad: Style.marginS
color: Qt.alpha(Color.mOnSurface, 0.125)
border.color: Qt.alpha(Color.mOnSurface, Style.opacityLight)
border.width: Style.borderS
RowLayout {
id: chipRow
spacing: Style.marginXS
anchors.fill: parent
anchors.margins: pad
NText {
text: modelData
color: Color.mOnSurface
pointSize: Style.fontSizeS
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: Style.marginXS
onClicked: {
const arr = (Settings.data.audio.mprisBlacklist || []);
const idx = arr.findIndex(x => String(x) === modelData);
if (idx >= 0) {
arr.splice(idx, 1);
Settings.data.audio.mprisBlacklist = arr;
MediaService.updateCurrentPlayer();
}
}
}
}
implicitWidth: chipRow.implicitWidth + pad * 2
implicitHeight: Math.max(chipRow.implicitHeight + pad * 2, Style.baseWidgetSize * 0.8)
radius: Style.radiusM
}
}
}
}
}
@@ -0,0 +1,97 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NComboBox {
label: I18n.tr("panels.audio.visualizer-type-label")
description: I18n.tr("panels.audio.visualizer-type-description")
model: [
{
"key": "none",
"name": I18n.tr("common.none")
},
{
"key": "linear",
"name": I18n.tr("options.visualizer-types.linear")
},
{
"key": "mirrored",
"name": I18n.tr("options.visualizer-types.mirrored")
},
{
"key": "wave",
"name": I18n.tr("options.visualizer-types.wave")
}
]
currentKey: Settings.data.audio.visualizerType
defaultValue: Settings.getDefaultValue("audio.visualizerType")
onSelected: key => Settings.data.audio.visualizerType = key
}
NToggle {
label: I18n.tr("panels.audio.spectrum-mirrored-label")
description: I18n.tr("panels.audio.spectrum-mirrored-description")
checked: Settings.data.audio.spectrumMirrored
defaultValue: Settings.getDefaultValue("audio.spectrumMirrored")
onToggled: Settings.data.audio.spectrumMirrored = checked
}
NComboBox {
label: I18n.tr("panels.audio.media-frame-rate-label")
description: I18n.tr("panels.audio.media-frame-rate-description")
model: [
{
"key": "30",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "30"
})
},
{
"key": "60",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "60"
})
},
{
"key": "100",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "100"
})
},
{
"key": "120",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "120"
})
},
{
"key": "144",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "144"
})
},
{
"key": "165",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "165"
})
},
{
"key": "240",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "240"
})
}
]
currentKey: Settings.data.audio.spectrumFrameRate
defaultValue: Settings.getDefaultValue("audio.spectrumFrameRate")
onSelected: key => Settings.data.audio.spectrumFrameRate = key
}
}
@@ -0,0 +1,196 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Media
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property real localVolume: AudioService.volume
Connections {
target: AudioService
function onSinkChanged() {
localVolume = AudioService.volume;
}
function onVolumeChanged() {
localVolume = AudioService.volume;
}
}
Connections {
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
function onVolumeChanged() {
localVolume = AudioService.volume;
}
}
// Output Volume
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.osd.types-volume-label")
description: I18n.tr("panels.audio.volumes-output-volume-description")
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: localVolume
stepSize: 0.01
text: Math.round(AudioService.volume * 100) + "%"
onMoved: value => localVolume = value
}
Timer {
interval: 100
running: true
repeat: true
onTriggered: {
if (!AudioService.isSwitchingSink && Math.abs(localVolume - AudioService.volume) >= 0.01) {
AudioService.setVolume(localVolume);
}
}
}
}
// Mute Toggle
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.audio.volumes-mute-output-label")
description: I18n.tr("panels.audio.volumes-mute-output-description")
checked: AudioService.muted
onToggled: checked => AudioService.setOutputMuted(checked)
}
}
// Volume Feedback sound Toggle
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.audio.volumes-volume-feedback-label")
description: I18n.tr("panels.audio.volumes-volume-feedback-description")
checked: Settings.data.audio.volumeFeedback
defaultValue: Settings.getDefaultValue("audio.volumeFeedback")
onToggled: checked => Settings.data.audio.volumeFeedback = checked
}
ColumnLayout {
enabled: SoundService.multimediaAvailable && Settings.data.audio.volumeFeedback
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.audio.volumes-feedback-sound-file-label")
description: I18n.tr("panels.audio.volumes-feedback-sound-file-description")
}
NTextInputButton {
enabled: parent.enabled
Layout.fillWidth: true
placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder")
text: Settings.data.audio.volumeFeedbackSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file")
onInputTextChanged: text => Settings.data.audio.volumeFeedbackSoundFile = text
onButtonClicked: volumeFeedbackFilePicker.open()
}
}
}
NDivider {
Layout.fillWidth: true
}
// Input Volume
ColumnLayout {
spacing: Style.marginXS
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.osd.types-input-volume-label")
description: I18n.tr("panels.audio.volumes-input-volume-description")
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: AudioService.inputVolume
stepSize: 0.01
text: Math.round(AudioService.inputVolume * 100) + "%"
onMoved: value => AudioService.setInputVolume(value)
}
}
// Input Mute Toggle
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.audio.volumes-mute-input-label")
description: I18n.tr("panels.audio.volumes-mute-input-description")
checked: AudioService.inputMuted
onToggled: checked => AudioService.setInputMuted(checked)
}
}
// Volume Step Size
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NSpinBox {
Layout.fillWidth: true
label: I18n.tr("panels.audio.volumes-step-size-label")
description: I18n.tr("panels.audio.volumes-step-size-description")
minimum: 1
maximum: 25
value: Settings.data.audio.volumeStep
stepSize: 1
suffix: "%"
defaultValue: Settings.getDefaultValue("audio.volumeStep")
onValueChanged: Settings.data.audio.volumeStep = value
}
}
NDivider {
Layout.fillWidth: true
}
// Raise maximum volume above 100%
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.audio.volumes-volume-overdrive-label")
description: I18n.tr("panels.audio.volumes-volume-overdrive-description")
checked: Settings.data.audio.volumeOverdrive
defaultValue: Settings.getDefaultValue("audio.volumeOverdrive")
onToggled: checked => Settings.data.audio.volumeOverdrive = checked
}
}
NFilePicker {
id: volumeFeedbackFilePicker
title: I18n.tr("panels.audio.volumes-feedback-sound-file-select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.audio.volumeFeedbackSoundFile = paths[0];
}
}
}
}