Rearrange for tidier structure (#2212)

This commit is contained in:
clsty
2025-10-16 07:19:55 +08:00
parent 13065d7e5a
commit 8b493e091d
529 changed files with 165 additions and 138 deletions
@@ -0,0 +1,55 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
RippleButton {
id: button
required property bool input
buttonRadius: Appearance.rounding.small
colBackground: Appearance.colors.colLayer2
colBackgroundHover: Appearance.colors.colLayer2Hover
colRipple: Appearance.colors.colLayer2Active
implicitHeight: contentItem.implicitHeight + 6 * 2
implicitWidth: contentItem.implicitWidth + 6 * 2
contentItem: RowLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
MaterialSymbol {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: false
Layout.leftMargin: 5
color: Appearance.colors.colOnLayer2
iconSize: Appearance.font.pixelSize.hugeass
text: input ? "mic_external_on" : "media_output"
}
ColumnLayout {
Layout.fillWidth: true
Layout.rightMargin: 5
spacing: 0
StyledText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pixelSize: Appearance.font.pixelSize.normal
text: input ? Translation.tr("Input") : Translation.tr("Output")
color: Appearance.colors.colOnLayer2
}
StyledText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pixelSize: Appearance.font.pixelSize.smaller
text: (input ? Pipewire.defaultAudioSource?.description : Pipewire.defaultAudioSink?.description) ?? Translation.tr("Unknown")
color: Appearance.m3colors.m3outline
animateChange: true
}
}
}
}
@@ -0,0 +1,275 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
Item {
id: root
property bool showDeviceSelector: false
property bool deviceSelectorInput
property int dialogMargins: 16
property PwNode selectedDevice
readonly property list<PwNode> appPwNodes: Pipewire.nodes.values.filter((node) => {
// return node.type == "21" // Alternative, not as clean
return node.isSink && node.isStream
})
function showDeviceSelectorDialog(input: bool) {
root.selectedDevice = null
root.showDeviceSelector = true
root.deviceSelectorInput = input
}
Keys.onPressed: (event) => {
// Close dialog on pressing Esc if open
if (event.key === Qt.Key_Escape && root.showDeviceSelector) {
root.showDeviceSelector = false
event.accepted = true;
}
}
ColumnLayout {
anchors.fill: parent
Item {
Layout.fillWidth: true
Layout.fillHeight: true
StyledListView {
id: listView
model: root.appPwNodes
clip: true
anchors {
fill: parent
topMargin: 10
bottomMargin: 10
}
spacing: 6
delegate: VolumeMixerEntry {
// Layout.fillWidth: true
anchors {
left: parent.left
right: parent.right
leftMargin: 10
rightMargin: 10
}
required property var modelData
node: modelData
}
}
// Placeholder when list is empty
Item {
anchors.fill: listView
visible: opacity > 0
opacity: (root.appPwNodes.length === 0) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.menuDecel.duration
easing.type: Appearance.animation.menuDecel.type
}
}
ColumnLayout {
anchors.centerIn: parent
spacing: 5
MaterialSymbol {
Layout.alignment: Qt.AlignHCenter
iconSize: 55
color: Appearance.m3colors.m3outline
text: "brand_awareness"
}
StyledText {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3outline
horizontalAlignment: Text.AlignHCenter
text: Translation.tr("No audio source")
}
}
}
}
// Device selector
RowLayout {
id: deviceSelectorRowLayout
Layout.fillWidth: true
Layout.fillHeight: false
uniformCellSizes: true
AudioDeviceSelectorButton {
Layout.fillWidth: true
input: false
downAction: () => root.showDeviceSelectorDialog(input)
}
AudioDeviceSelectorButton {
Layout.fillWidth: true
input: true
downAction: () => root.showDeviceSelectorDialog(input)
}
}
}
// Device selector dialog
Item {
anchors.fill: parent
z: 9999
visible: opacity > 0
opacity: root.showDeviceSelector ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
Rectangle { // Scrim
id: scrimOverlay
anchors.fill: parent
radius: Appearance.rounding.small
color: Appearance.colors.colScrim
MouseArea {
hoverEnabled: true
anchors.fill: parent
preventStealing: true
propagateComposedEvents: false
}
}
Rectangle { // The dialog
id: dialog
color: Appearance.colors.colSurfaceContainerHigh
radius: Appearance.rounding.normal
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 30
implicitHeight: dialogColumnLayout.implicitHeight
ColumnLayout {
id: dialogColumnLayout
anchors.fill: parent
spacing: 16
StyledText {
id: dialogTitle
Layout.topMargin: dialogMargins
Layout.leftMargin: dialogMargins
Layout.rightMargin: dialogMargins
Layout.alignment: Qt.AlignLeft
color: Appearance.m3colors.m3onSurface
font.pixelSize: Appearance.font.pixelSize.larger
text: root.deviceSelectorInput ? Translation.tr("Select input device") : Translation.tr("Select output device")
}
Rectangle {
color: Appearance.m3colors.m3outline
implicitHeight: 1
Layout.fillWidth: true
Layout.leftMargin: dialogMargins
Layout.rightMargin: dialogMargins
}
StyledFlickable {
id: dialogFlickable
Layout.fillWidth: true
clip: true
implicitHeight: Math.min(scrimOverlay.height - dialogMargins * 8 - dialogTitle.height - dialogButtonsRowLayout.height, devicesColumnLayout.implicitHeight)
contentHeight: devicesColumnLayout.implicitHeight
ColumnLayout {
id: devicesColumnLayout
anchors.fill: parent
Layout.fillWidth: true
spacing: 0
Repeater {
model: ScriptModel {
values: Pipewire.nodes.values.filter(node => {
return !node.isStream && node.isSink !== root.deviceSelectorInput && node.audio
})
}
// This could and should be refractored, but all data becomes null when passed wtf
delegate: StyledRadioButton {
id: radioButton
required property var modelData
Layout.leftMargin: root.dialogMargins
Layout.rightMargin: root.dialogMargins
Layout.fillWidth: true
description: modelData.description
checked: modelData.id === Pipewire.defaultAudioSink?.id
Connections {
target: root
function onShowDeviceSelectorChanged() {
if(!root.showDeviceSelector) return;
radioButton.checked = (modelData.id === Pipewire.defaultAudioSink?.id)
}
}
onCheckedChanged: {
if (checked) {
root.selectedDevice = modelData
}
}
}
}
Item {
implicitHeight: dialogMargins
}
}
}
Rectangle {
color: Appearance.m3colors.m3outline
implicitHeight: 1
Layout.fillWidth: true
Layout.leftMargin: dialogMargins
Layout.rightMargin: dialogMargins
}
RowLayout {
id: dialogButtonsRowLayout
Layout.bottomMargin: dialogMargins
Layout.leftMargin: dialogMargins
Layout.rightMargin: dialogMargins
Layout.alignment: Qt.AlignRight
DialogButton {
buttonText: Translation.tr("Cancel")
onClicked: {
root.showDeviceSelector = false
}
}
DialogButton {
buttonText: Translation.tr("OK")
onClicked: {
root.showDeviceSelector = false
if (root.selectedDevice) {
if (root.deviceSelectorInput) {
Pipewire.preferredDefaultAudioSource = root.selectedDevice
} else {
Pipewire.preferredDefaultAudioSink = root.selectedDevice
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,63 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
Item {
id: root
required property PwNode node
PwObjectTracker {
objects: [node]
}
implicitHeight: rowLayout.implicitHeight
RowLayout {
id: rowLayout
anchors.fill: parent
spacing: 6
Image {
property real size: slider.height * 0.9
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
visible: source != ""
sourceSize.width: size
sourceSize.height: size
source: {
let icon;
icon = AppSearch.guessIcon(root.node.properties["application.icon-name"]);
if (AppSearch.iconExists(icon))
return Quickshell.iconPath(icon, "image-missing");
icon = AppSearch.guessIcon(root.node.properties["node.name"]);
return Quickshell.iconPath(icon, "image-missing");
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: -4
StyledText {
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colSubtext
elide: Text.ElideRight
text: {
// application.name -> description -> name
const app = root.node.properties["application.name"] ?? (root.node.description != "" ? root.node.description : root.node.name);
const media = root.node.properties["media.name"];
return media != undefined ? `${app} ${media}` : app;
}
}
StyledSlider {
id: slider
value: root.node.audio.volume
onMoved: root.node.audio.volume = value
}
}
}
}