diff --git a/shell/bar/volume/ApplicationMixer.qml b/shell/bar/volume/ApplicationMixer.qml new file mode 100644 index 0000000..584be77 --- /dev/null +++ b/shell/bar/volume/ApplicationMixer.qml @@ -0,0 +1,70 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets +import Quickshell.Services.Pipewire +import "../../widgets/" as Widgets +import "../.." + +ColumnLayout { + id: root + + Loader { + id: sinkLoader + active: sink + + property PwNode sink: Pipewire.defaultAudioSink + + sourceComponent: WrapperItem { + PwNodeLinkTracker { + id: linkTracker + node: sinkLoader.sink + } + + ColumnLayout { + Repeater { + model: linkTracker.linkGroups + + delegate: Loader { + id: nodeLoader + active: modelData.source !== null + Layout.preferredWidth: 350 + Layout.preferredHeight: 45 + + required property PwLinkGroup modelData + + sourceComponent: VolumeCard { + id: nodeCard + node: nodeLoader.modelData.source + text: node.properties["media.name"] ?? "" + + // if icon-name is undefined, just gonna fallback on the application name + icon: IconImage { + source: { + if (nodeCard.node.properties["application.icon-name"] !== undefined) + return `image://icon/${nodeCard.node.properties["application.icon-name"]}`; + + let applicationName = nodeCard.node.properties["application.name"]; + return `image://icon/${applicationName?.toLowerCase() ?? "image-missing"}`; + } + } + + button: Widgets.FontIconButton { + hoverEnabled: false + iconName: nodeCard.node.audio.muted ? "volume_off" : "volume_up" + checked: !nodeCard.node.audio.muted + inactiveColor: ShellSettings.colors["surface_container_highest"] + onClicked: { + nodeCard.node.audio.muted = !nodeCard.node.audio.muted; + } + } + + anchors.fill: parent + } + } + } + } + } + } +} diff --git a/shell/bar/volume/DeviceMixer.qml b/shell/bar/volume/DeviceMixer.qml new file mode 100644 index 0000000..e7076dc --- /dev/null +++ b/shell/bar/volume/DeviceMixer.qml @@ -0,0 +1,65 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.Pipewire +import Quickshell.Widgets +import qs +import qs.widgets + +ColumnLayout { + id: root + + // headphones + // don't load until the node is not null + Loader { + id: sinkLoader + active: sink !== null + Layout.preferredWidth: 350 + Layout.preferredHeight: 45 + + property PwNode sink: Pipewire.defaultAudioSink + + sourceComponent: VolumeCard { + id: sinkCard + node: sinkLoader.sink + button: StyledMouseArea { + property bool checked: !sinkCard.node.audio.muted + + // IconImage {} + + onClicked: { + sinkCard.node.audio.muted = !sinkCard.node.audio.muted; + } + } + + anchors.fill: parent + } + } + + // microphone, same as above + Loader { + id: sourceLoader + active: source !== null + Layout.preferredWidth: 350 + Layout.preferredHeight: 45 + + property PwNode source: Pipewire.defaultAudioSource + + sourceComponent: VolumeCard { + id: sourceCard + node: sourceLoader.source + button: StyledMouseArea { + property bool checked: !sourceCard.node.audio.muted + + // IconImage {} + + onClicked: { + sourceCard.node.audio.muted = !sourceCard.node.audio.muted; + } + } + + anchors.fill: parent + } + } +} diff --git a/shell/bar/volume/VolumeCard.qml b/shell/bar/volume/VolumeCard.qml index 9a47ba4..c431c30 100644 --- a/shell/bar/volume/VolumeCard.qml +++ b/shell/bar/volume/VolumeCard.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import QtQuick.Controls import Quickshell.Widgets import Quickshell.Services.Pipewire import qs.widgets @@ -12,34 +13,34 @@ Loader { active: node != null required property PwNode node - property string label: node.nickname === "" ? node.description : node.nickname + property string label: node.nickname - property Component leftWidget - - PwObjectTracker { - id: tracker - objects: [root.node] - } - - sourceComponent: WrapperItem { + sourceComponent: WrapperRectangle { + id: comp + color: ShellSettings.colors.surface_container_translucent + radius: 12 margin: 6 + border { + width: 1 + color: ShellSettings.colors.active_translucent + } + + // property Component button + // property Component icon + + PwObjectTracker { + id: tracker + objects: [root.node] + } + RowLayout { - spacing: 10 - - Loader { - id: leftWidget - sourceComponent: root.leftWidget - Layout.preferredWidth: this.height - Layout.fillHeight: true - } - ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true Text { - text: root.label + text: root.label color: ShellSettings.colors.active elide: Text.ElideRight Layout.fillWidth: true @@ -48,6 +49,8 @@ Loader { StyledSlider { value: root.node.audio.volume ?? 0 + // text: root.text + // icon: root.icon onValueChanged: { // only allow changes when the node is ready other wise you will combust @@ -61,6 +64,42 @@ Loader { Layout.fillHeight: true } } + + // StyledMouseArea { + // id: rightArrow + // Layout.preferredWidth: rightArrow.height + // // Layout.fillWidth: true + // Layout.fillHeight: true + // + // IconImage { + // source: "root:resources/general/right-arrow.svg" + // anchors.fill: parent + // } + // } + + // Loader { + // id: buttonLoader + // sourceComponent: root.button + // + // Layout.preferredWidth: this.height + // Layout.fillHeight: true + // } } } + + // sourceComponent: VolumeCard { + // id: sinkCard + // node: sinkLoader.sink + // button: StyledMouseArea { + // property bool checked: !sinkCard.node.audio.muted + // + // // IconImage {} + // + // onClicked: { + // sinkCard.node.audio.muted = !sinkCard.node.audio.muted; + // } + // } + // + // anchors.fill: parent + // } } diff --git a/shell/bar/volume/VolumeControl.qml b/shell/bar/volume/VolumeControl.qml new file mode 100644 index 0000000..bcb4f48 --- /dev/null +++ b/shell/bar/volume/VolumeControl.qml @@ -0,0 +1,36 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets +import qs.widgets + +DeviceMixer {} + +// WrapperItem { +// id: root +// +// ColumnLayout { +// spacing: 10 +// +// // TabBar { +// // id: tabBar +// // model: ["headphones", "tune"] +// // Layout.fillWidth: true +// // Layout.preferredHeight: 35 +// // } +// +// +// // StackLayout { +// // id: page +// // currentIndex: tabBar.currentIndex +// // Layout.fillWidth: true +// // Layout.preferredHeight: currentItem ? currentItem.implicitHeight : 0 +// // +// // readonly property Item currentItem: children[currentIndex] +// // +// // DeviceMixer {} +// // ApplicationMixer {} +// // } +// } +// } diff --git a/shell/bar/volume/VolumeIndicator.qml b/shell/bar/volume/VolumeIndicator.qml index f4b6785..20b51ef 100644 --- a/shell/bar/volume/VolumeIndicator.qml +++ b/shell/bar/volume/VolumeIndicator.qml @@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts -import QtQuick.Effects import Quickshell.Widgets import Quickshell.Services.Pipewire import qs.widgets import qs.bar +import qs StyledMouseArea { id: root @@ -14,19 +14,14 @@ StyledMouseArea { required property var bar property bool showMenu: false - property PwNode sink: Pipewire.defaultAudioSink IconImage { id: icon - anchors.fill: parent - source: if (root.sink.audio.muted) { - return "image://icon/audio-volume-muted"; - } else if (root.sink.audio.volume > 0.66) { - return "image://icon/audio-volume-high"; - } else if (root.sink.audio.volume > 0.33) { - return "image://icon/audio-volume-medium"; - } else { - return "image://icon/audio-volume-low"; + source: "root:resources/volume/volume-full.svg" + + anchors { + fill: parent + margins: 2 } } @@ -37,95 +32,66 @@ StyledMouseArea { show: root.showMenu onClosed: root.showMenu = false - implicitWidth: 275 + implicitWidth: 300 implicitHeight: container.implicitHeight + (2 * 8) - property real entryHeight: 40 + property PwNode sink: Pipewire.defaultAudioSink + property real entryHeight: 45 ColumnLayout { id: container - spacing: 2 + spacing: 4 anchors { fill: parent - margins: 4 + margins: 8 } // Default Audio VolumeCard { - id: defaultCard - node: root.sink + node: menu.sink Layout.fillWidth: true Layout.preferredHeight: menu.entryHeight + } - leftWidget: StyledMouseArea { - onClicked: defaultCard.node.audio.muted = !defaultCard.node.audio.muted - - IconImage { - anchors.fill: parent - source: if (root.sink.audio.muted) { - return "image://icon/audio-volume-muted"; - } else if (root.sink.audio.volume > 0.66) { - return "image://icon/audio-volume-high"; - } else if (root.sink.audio.volume > 0.33) { - return "image://icon/audio-volume-medium"; - } else { - return "image://icon/audio-volume-low"; - } - } - } + Rectangle { + color: ShellSettings.colors.active_translucent + radius: height / 2 + Layout.leftMargin: 3 + Layout.rightMargin: 3 + Layout.fillWidth: true + Layout.preferredHeight: 2 } // Application Mixer - PwNodeLinkTracker { - id: linkTracker - node: root.sink - } - - StyledListView { - id: appList - visible: linkTracker.linkGroups.length !== 0 - spacing: 2 - model: linkTracker.linkGroups - clip: true + Loader { + id: sinkLoader + active: menu.sink Layout.fillWidth: true - Layout.preferredHeight: { - const entryHeight = Math.min(5, linkTracker.linkGroups.length); + Layout.preferredHeight: 5 * menu.entryHeight - return entryHeight * (menu.entryHeight + appList.spacing); + PwNodeLinkTracker { + id: linkTracker + node: menu.sink } - delegate: VolumeCard { - id: appCard - node: modelData.source - label: node.properties["media.name"] ?? "" - width: ListView.view.width - height: menu.entryHeight + sourceComponent: ListView { + anchors.fill: parent + spacing: 6 + model: linkTracker.linkGroups - required property PwLinkGroup modelData + delegate: Loader { + id: nodeLoader + active: modelData.source != null + width: ListView.view.width + height: menu.entryHeight - leftWidget: StyledMouseArea { - onClicked: appCard.node.audio.muted = !appCard.node.audio.muted + required property PwLinkGroup modelData - IconImage { - id: appIcon - visible: false - anchors.fill: parent - - source: { - if (appCard.node.properties["application.icon-name"] !== undefined) - return `image://icon/${appCard.node.properties["application.icon-name"]}`; - - let applicationName = appCard.node.properties["application.name"]; - return `image://icon/${applicationName?.toLowerCase() ?? "image-missing"}`; - } - } - - MultiEffect { - source: appIcon - anchors.fill: appIcon - saturation: appCard.node.audio.muted ? -1.0 : 0.0 + sourceComponent: VolumeCard { + node: nodeLoader.modelData.source + label: node.properties["media.name"] ?? "" } } } diff --git a/shell/launcher/Controller.qml b/shell/launcher/Controller.qml index 099d407..e56f366 100644 --- a/shell/launcher/Controller.qml +++ b/shell/launcher/Controller.qml @@ -64,7 +64,7 @@ Singleton { anchors { horizontalCenter: parent.horizontalCenter top: parent.top - topMargin: screen.height / 3 + topMargin: screen.height / 2.75 } ColumnLayout { diff --git a/shell/resources/mask.png b/shell/resources/mask.png new file mode 100644 index 0000000..e6cac94 Binary files /dev/null and b/shell/resources/mask.png differ diff --git a/shell/resources/volume/microphone-full.svg b/shell/resources/volume/microphone-full.svg new file mode 100644 index 0000000..8d1a116 --- /dev/null +++ b/shell/resources/volume/microphone-full.svg @@ -0,0 +1,2 @@ + + diff --git a/shell/resources/volume/microphone-mute.svg b/shell/resources/volume/microphone-mute.svg new file mode 100644 index 0000000..8c2d3b5 --- /dev/null +++ b/shell/resources/volume/microphone-mute.svg @@ -0,0 +1,2 @@ + + diff --git a/shell/resources/volume/volume-full.svg b/shell/resources/volume/volume-full.svg new file mode 100644 index 0000000..19bbfb1 --- /dev/null +++ b/shell/resources/volume/volume-full.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/shell/resources/volume/volume-low.svg b/shell/resources/volume/volume-low.svg new file mode 100644 index 0000000..69e95e5 --- /dev/null +++ b/shell/resources/volume/volume-low.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/shell/resources/volume/volume-mute.svg b/shell/resources/volume/volume-mute.svg new file mode 100644 index 0000000..4b67e0c --- /dev/null +++ b/shell/resources/volume/volume-mute.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/shell/resources/volume/volume-off.svg b/shell/resources/volume/volume-off.svg new file mode 100644 index 0000000..f975a8b --- /dev/null +++ b/shell/resources/volume/volume-off.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/shell/shell.qml b/shell/shell.qml index 7986317..8527c2d 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -6,7 +6,7 @@ import QtQuick import "bar" import "notifications" as Notifications import "mpris" as Mpris -import "volosd" as VolumeOSD +import "volume-osd" as VolumeOSD import "settings" as Settings import "launcher" as Launcher import "lockscreen" as LockScreen diff --git a/shell/volosd/Controller.qml b/shell/volume-osd/Controller.qml similarity index 73% rename from shell/volosd/Controller.qml rename to shell/volume-osd/Controller.qml index 2b7f45e..5e6bd3e 100644 --- a/shell/volosd/Controller.qml +++ b/shell/volume-osd/Controller.qml @@ -21,11 +21,7 @@ Scope { target: Pipewire.defaultAudioSink?.audio function onVolumeChanged() { - root.shouldShowOsd = true; - hideTimer.restart(); - } - - function onMutedChanged() { + console.log("Volume Changed, showing OSD."); root.shouldShowOsd = true; hideTimer.restart(); } @@ -57,8 +53,6 @@ Scope { // radius: 8 RowLayout { - spacing: 10 - anchors { fill: parent leftMargin: 10 @@ -67,25 +61,15 @@ Scope { IconImage { implicitSize: 30 - source: if (Pipewire.defaultAudioSink?.audio.muted) { - return "image://icon/audio-volume-muted"; - } else if (Pipewire.defaultAudioSink?.audio.volume > 0.66) { - return "image://icon/audio-volume-high"; - } else if (Pipewire.defaultAudioSink?.audio.volume > 0.33) { - return "image://icon/audio-volume-medium"; - } else { - return "image://icon/audio-volume-low"; - } + source: "root:resources/volume/volume-full.svg" } Rectangle { id: sliderBackground Layout.fillWidth: true implicitHeight: 10 - radius: height / 2 - color: "transparent" - border.color: ShellSettings.colors.active_translucent - border.width: 1 + radius: 20 + color: ShellSettings.colors.inactive layer.enabled: true layer.effect: OpacityMask { @@ -99,13 +83,13 @@ Scope { Rectangle { color: ShellSettings.colors.active - implicitWidth: parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0) - anchors { left: parent.left top: parent.top bottom: parent.bottom } + + implicitWidth: parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0) } } } diff --git a/shell/widgets/StyledListView.qml b/shell/widgets/StyledListView.qml deleted file mode 100644 index c53f7f9..0000000 --- a/shell/widgets/StyledListView.qml +++ /dev/null @@ -1,53 +0,0 @@ -import QtQuick - -ListView { - id: root - - add: Transition { - NumberAnimation { - property: "opacity" - from: 0 - to: 1 - duration: 100 - } - } - - displaced: Transition { - NumberAnimation { - property: "y" - duration: 200 - easing.type: Easing.OutCubic - } - NumberAnimation { - property: "opacity" - to: 1 - duration: 100 - } - } - - move: Transition { - NumberAnimation { - property: "y" - duration: 200 - easing.type: Easing.OutCubic - } - NumberAnimation { - property: "opacity" - to: 1 - duration: 100 - } - } - - remove: Transition { - NumberAnimation { - property: "y" - duration: 200 - easing.type: Easing.OutCubic - } - NumberAnimation { - property: "opacity" - to: 0 - duration: 100 - } - } -} diff --git a/shell/widgets/StyledSlider.qml b/shell/widgets/StyledSlider.qml index d7b59c8..11b653a 100644 --- a/shell/widgets/StyledSlider.qml +++ b/shell/widgets/StyledSlider.qml @@ -7,18 +7,15 @@ import ".." Slider { id: slider - implicitHeight: 7 - + implicitHeight: 8 property var accentColor: ShellSettings.colors.active background: Rectangle { id: sliderContainer width: slider.availableWidth height: slider.implicitHeight - color: "transparent" - border.color: ShellSettings.colors.active_translucent - border.width: 1 - radius: 5 + color: ShellSettings.colors.inactive + radius: 4 anchors.verticalCenter: parent.verticalCenter layer.enabled: true @@ -42,7 +39,7 @@ Slider { id: fill width: slider.handle.width / 2 + slider.visualPosition * (sliderContainer.width - slider.handle.width) height: sliderContainer.height - color: ShellSettings.colors.active_translucent + color: Qt.color(slider.accentColor ?? "purple").darker(1.2) } }