mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
442 lines
12 KiB
QML
442 lines
12 KiB
QML
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
|
|
|
|
property real localVolume: AudioService.volume
|
|
|
|
Connections {
|
|
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
|
|
function onVolumeChanged() {
|
|
localVolume = AudioService.volume;
|
|
}
|
|
}
|
|
|
|
NHeader {
|
|
label: I18n.tr("settings.audio.volumes.section.label")
|
|
description: I18n.tr("settings.audio.volumes.section.description")
|
|
}
|
|
|
|
// Master Volume
|
|
ColumnLayout {
|
|
spacing: Style.marginXXS
|
|
Layout.fillWidth: true
|
|
|
|
NLabel {
|
|
label: I18n.tr("settings.audio.volumes.output-volume.label")
|
|
description: I18n.tr("settings.audio.volumes.output-volume.description")
|
|
}
|
|
|
|
// Pipewire seems a bit finicky, if we spam too many volume changes it breaks easily
|
|
// Probably because they have some quick fades in and out to avoid clipping
|
|
// We use a timer to space out the updates, to avoid lock up
|
|
Timer {
|
|
interval: 100
|
|
running: true
|
|
repeat: true
|
|
onTriggered: {
|
|
if (Math.abs(localVolume - AudioService.volume) >= 0.01) {
|
|
AudioService.setVolume(localVolume);
|
|
}
|
|
}
|
|
}
|
|
|
|
NValueSlider {
|
|
Layout.fillWidth: true
|
|
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
|
|
}
|
|
}
|
|
|
|
// Mute Toggle
|
|
ColumnLayout {
|
|
spacing: Style.marginS
|
|
Layout.fillWidth: true
|
|
|
|
NToggle {
|
|
label: I18n.tr("settings.audio.volumes.mute-output.label")
|
|
description: I18n.tr("settings.audio.volumes.mute-output.description")
|
|
checked: AudioService.muted
|
|
onToggled: checked => {
|
|
if (AudioService.sink && AudioService.sink.audio) {
|
|
AudioService.sink.audio.muted = checked;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Input Volume
|
|
ColumnLayout {
|
|
spacing: Style.marginXS
|
|
Layout.fillWidth: true
|
|
|
|
NLabel {
|
|
label: I18n.tr("settings.audio.volumes.input-volume.label")
|
|
description: I18n.tr("settings.audio.volumes.input-volume.description")
|
|
}
|
|
|
|
NValueSlider {
|
|
Layout.fillWidth: true
|
|
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("settings.audio.volumes.mute-input.label")
|
|
description: I18n.tr("settings.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("settings.audio.volumes.step-size.label")
|
|
description: I18n.tr("settings.audio.volumes.step-size.description")
|
|
minimum: 1
|
|
maximum: 25
|
|
value: Settings.data.audio.volumeStep
|
|
stepSize: 1
|
|
suffix: "%"
|
|
onValueChanged: Settings.data.audio.volumeStep = value
|
|
}
|
|
}
|
|
|
|
// Raise maximum volume above 100%
|
|
ColumnLayout {
|
|
spacing: Style.marginS
|
|
Layout.fillWidth: true
|
|
|
|
NToggle {
|
|
label: I18n.tr("settings.audio.volumes.volume-overdrive.label")
|
|
description: I18n.tr("settings.audio.volumes.volume-overdrive.description")
|
|
checked: Settings.data.audio.volumeOverdrive
|
|
onToggled: checked => Settings.data.audio.volumeOverdrive = checked
|
|
}
|
|
}
|
|
|
|
// External mixer command
|
|
NTextInput {
|
|
label: I18n.tr("settings.audio.external-mixer.label")
|
|
description: I18n.tr("settings.audio.external-mixer.description")
|
|
placeholderText: I18n.tr("settings.audio.external-mixer.placeholder")
|
|
text: Settings.data.audio.externalMixer
|
|
onTextChanged: Settings.data.audio.externalMixer = text
|
|
}
|
|
|
|
NDivider {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Style.marginL
|
|
Layout.bottomMargin: Style.marginL
|
|
}
|
|
|
|
// AudioService Devices
|
|
ColumnLayout {
|
|
spacing: Style.marginS
|
|
Layout.fillWidth: true
|
|
|
|
NHeader {
|
|
label: I18n.tr("settings.audio.devices.section.label")
|
|
description: I18n.tr("settings.audio.devices.section.description")
|
|
}
|
|
|
|
// -------------------------------
|
|
// Output Devices
|
|
ButtonGroup {
|
|
id: sinks
|
|
}
|
|
|
|
ColumnLayout {
|
|
spacing: Style.marginXS
|
|
Layout.fillWidth: true
|
|
Layout.bottomMargin: Style.marginL
|
|
|
|
NLabel {
|
|
label: I18n.tr("settings.audio.devices.output-device.label")
|
|
description: I18n.tr("settings.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);
|
|
localVolume = AudioService.volume;
|
|
}
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------
|
|
// Input Devices
|
|
ButtonGroup {
|
|
id: sources
|
|
}
|
|
|
|
ColumnLayout {
|
|
spacing: Style.marginXS
|
|
Layout.fillWidth: true
|
|
|
|
NLabel {
|
|
label: I18n.tr("settings.audio.devices.input-device.label")
|
|
description: I18n.tr("settings.audio.devices.input-device.description")
|
|
}
|
|
|
|
Repeater {
|
|
model: AudioService.sources
|
|
//Layout.fillWidth: true
|
|
NRadioButton {
|
|
ButtonGroup.group: sources
|
|
required property PwNode modelData
|
|
text: modelData.description
|
|
checked: AudioService.source?.id === modelData.id
|
|
onClicked: AudioService.setAudioSource(modelData)
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Divider
|
|
NDivider {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Style.marginL
|
|
Layout.bottomMargin: Style.marginL
|
|
}
|
|
|
|
// Media Player Preferences
|
|
ColumnLayout {
|
|
spacing: Style.marginL
|
|
|
|
NHeader {
|
|
label: I18n.tr("settings.audio.media.section.label")
|
|
description: I18n.tr("settings.audio.media.section.description")
|
|
}
|
|
|
|
// Preferred player
|
|
NTextInput {
|
|
label: I18n.tr("settings.audio.media.primary-player.label")
|
|
description: I18n.tr("settings.audio.media.primary-player.description")
|
|
placeholderText: I18n.tr("settings.audio.media.primary-player.placeholder")
|
|
text: Settings.data.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("settings.audio.media.excluded-player.label")
|
|
description: I18n.tr("settings.audio.media.excluded-player.description")
|
|
placeholderText: I18n.tr("settings.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
|
|
// Padding around the inner row
|
|
property real pad: Style.marginS
|
|
// Visuals
|
|
color: Qt.alpha(Color.mOnSurface, 0.125)
|
|
border.color: Qt.alpha(Color.mOnSurface, Style.opacityLight)
|
|
border.width: Style.borderS
|
|
|
|
// Content
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Intrinsic size derived from inner row + padding
|
|
implicitWidth: chipRow.implicitWidth + pad * 2
|
|
implicitHeight: Math.max(chipRow.implicitHeight + pad * 2, Style.baseWidgetSize * 0.8)
|
|
radius: Style.radiusM
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// AudioService Visualizer section
|
|
NComboBox {
|
|
label: I18n.tr("settings.audio.media.visualizer-type.label")
|
|
description: I18n.tr("settings.audio.media.visualizer-type.description")
|
|
model: [
|
|
{
|
|
"key": "none",
|
|
"name": I18n.tr("options.visualizer-types.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
|
|
onSelected: key => Settings.data.audio.visualizerType = key
|
|
}
|
|
|
|
NComboBox {
|
|
label: I18n.tr("settings.audio.media.visualizer-quality.label")
|
|
description: I18n.tr("settings.audio.media.visualizer-quality.description")
|
|
model: [
|
|
{
|
|
"key": "low",
|
|
"name": I18n.tr("options.visualizer-quality.low")
|
|
},
|
|
{
|
|
"key": "high",
|
|
"name": I18n.tr("options.visualizer-quality.high")
|
|
}
|
|
]
|
|
currentKey: Settings.data.audio.visualizerQuality
|
|
onSelected: key => Settings.data.audio.visualizerQuality = key
|
|
}
|
|
|
|
NComboBox {
|
|
label: I18n.tr("settings.audio.media.frame-rate.label")
|
|
description: I18n.tr("settings.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.cavaFrameRate
|
|
onSelected: key => Settings.data.audio.cavaFrameRate = key
|
|
}
|
|
}
|
|
|
|
NDivider {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Style.marginL
|
|
Layout.bottomMargin: Style.marginL
|
|
}
|
|
}
|