diff --git a/nix/shell.nix b/nix/shell.nix index a1e85d0..3391bcf 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -11,6 +11,7 @@ pkgs.mkShell { # Required for qmlls to find the correct type declarations # Sadly Quickshell doesn't export some types declaratively export QMLLS_BUILD_DIRS=${pkgs.kdePackages.qtdeclarative}/lib/qt-6/qml/:${quickshell}/lib/qt-6/qml/ + export QML_IMPORT_PATH=$PWD/src ${pkgs.pre-commit}/bin/pre-commit install -f ''; } diff --git a/src/Dashboard.qml b/src/Dashboard.qml index 6fcadae..be04dad 100644 --- a/src/Dashboard.qml +++ b/src/Dashboard.qml @@ -3,6 +3,8 @@ import QtQuick import QtQuick.Layouts import QtQml import "base" +import "widgets/mpris" +import "provider" PanelWindow { id: homeWindow @@ -23,6 +25,7 @@ PanelWindow { MouseArea { id: mouse + anchors.fill: parent onClicked: homeWindow.root.enabled = false @@ -44,6 +47,23 @@ PanelWindow { Component.onCompleted: () => maxSize = homeWindow.screen.width * (2 / 7) + ListView { + width: parent.width + height: parent.height + model: Notifications.incoming + + delegate: Rectangle { + required property var modelData + width: 100 + height: 50 + Text { + text: parent.modelData.appName + width: parent.width + height: parent.height + } + } + } + Behavior on width { PropertyAnimation { id: anim @@ -69,26 +89,7 @@ PanelWindow { Layout.margins: 15 Layout.alignment: Qt.AlignBottom - BRectangle { - Layout.fillWidth: true - Layout.preferredHeight: 200 - radius: 15 - - RowLayout { - anchors.fill: parent - clip: true - Rectangle { - Layout.margins: 20 - Layout.preferredWidth: parent.height - (Layout.margins * 2) - Layout.preferredHeight: parent.height - (Layout.margins * 2) - Layout.maximumWidth: { - const mWidth = parent.width - (Layout.margins * 2); - return mWidth > 0 ? mWidth : 0; - } - Layout.fillHeight: true - } - } - } + MprisSmall {} } } } diff --git a/src/base/BRoundedImage.qml b/src/base/BRoundedImage.qml index 02c448d..3d5c70f 100644 --- a/src/base/BRoundedImage.qml +++ b/src/base/BRoundedImage.qml @@ -25,18 +25,13 @@ Rectangle { maskSource: mask } - Item { + Rectangle { id: mask width: image.width height: image.height layer.enabled: true visible: false - - Rectangle { - width: image.width - height: image.height - radius: roundedImage.radius - color: "black" - } + radius: roundedImage.radius + color: "black" } } diff --git a/src/base/BlurredImage.qml b/src/base/BlurredImage.qml new file mode 100644 index 0000000..e0a9098 --- /dev/null +++ b/src/base/BlurredImage.qml @@ -0,0 +1,44 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects + +Rectangle { + id: root + required property var source + color: "transparent" + + Image { + id: background + anchors.fill: parent + source: root.source + Layout.alignment: Qt.AlignHCenter + visible: false + } + + MultiEffect { + id: image + autoPaddingEnabled: false + source: background + anchors.fill: background + blurEnabled: true + blurMax: 64 + blurMultiplier: 2 + blur: 1 + brightness: -0.15 + contrast: -0.35 + maskEnabled: true + maskSource: mask + anchors.margins: root.border.width - 1 + } + + Rectangle { + id: mask + width: image.width + height: image.height + layer.enabled: true + visible: false + + radius: root.radius + color: "black" + } +} diff --git a/src/provider/Notifications.qml b/src/provider/Notifications.qml index e1a5241..eea7a2a 100644 --- a/src/provider/Notifications.qml +++ b/src/provider/Notifications.qml @@ -15,14 +15,13 @@ Singleton { bodyMarkupSupported: false bodySupported: true imageSupported: true - } - Item { - Component.onCompleted: () => { - notif._.notification.connect(n => { - list.push(n); - }); + onNotification: n => { + n.tracked = true; + incoming.push(n); } } - property var list: [] + + property list backlog: notif._.trackedNotifications + property list incoming: [] } diff --git a/src/provider/Player.qml b/src/provider/Player.qml index 4467539..dce69bd 100644 --- a/src/provider/Player.qml +++ b/src/provider/Player.qml @@ -9,15 +9,22 @@ Singleton { property var current: player.all[player.index] - property var all: Mpris.players.values + property list all: Mpris.players.values + property int index: { const ind = Mpris.players.values.findIndex(p => p.playbackState === MprisPlaybackState.Playing); return ind >= 0 ? ind : 0; } - property var next: () => { + + onIndexChanged: () => { + player.current = player.all[player.index] ?? player.current; + } + + function next(): void { player.index = (player.index + 1) % all.length; } - property var prev: () => { + + function prev(): void { const newInd = player.index - 1; player.index = newInd < 0 ? all.length - 1 : newInd; } diff --git a/src/provider/qmldir b/src/provider/qmldir new file mode 100644 index 0000000..b12d3c7 --- /dev/null +++ b/src/provider/qmldir @@ -0,0 +1,5 @@ +module Provider +singleton Player 0.1 Player.qml +singleton Inhibitor 0.1 Inhibitor.qml +singleton Notifications 0.1 Notifications.qml +singleton Time 0.1 Time.qml diff --git a/src/widgets/MprisBig/MprisWidget.qml b/src/widgets/MprisBig/MprisWidget.qml index 54484f9..da31f12 100644 --- a/src/widgets/MprisBig/MprisWidget.qml +++ b/src/widgets/MprisBig/MprisWidget.qml @@ -72,7 +72,7 @@ ColumnLayout { , ["media-playlist-repeat", MprisLoopState.Playlist] // ] property int index: map.findIndex(e => e[1] === Player.current?.loopState) - source: loopButton.visible ? Quickshell.iconPath(map[index][0]) : "" + source: loopButton.visible && map[index] && map[index][0] ? Quickshell.iconPath(map[index][0]) : "" onClicked: { const ind = (index + 1) % map.length; Player.current.loopState = map[ind][1]; diff --git a/src/widgets/mpris/MprisSmall.qml b/src/widgets/mpris/MprisSmall.qml new file mode 100644 index 0000000..65cac62 --- /dev/null +++ b/src/widgets/mpris/MprisSmall.qml @@ -0,0 +1,135 @@ +import QtQuick.Effects +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick +import Quickshell.Services.Mpris +import Quickshell +import "../../base" +import "../../provider" + +BRectangle { + id: mprisSmall + Layout.fillWidth: true + Layout.preferredHeight: 200 + radius: 15 + + visible: Player.current ?? false + + BlurredImage { + source: Player.current?.trackArtUrl ?? "" + anchors.fill: parent + radius: parent.radius + } + + RowLayout { + anchors.fill: parent + clip: true + BRoundedImage { + id: im + color: "transparent" + visible: false + source: Player.current?.trackArtUrl ?? "" + radius: 15 + } + + MultiEffect { + id: effect + source: im + autoPaddingEnabled: true + shadowBlur: 1.0 + shadowColor: 'black' + shadowEnabled: true + Layout.margins: 20 + Layout.preferredWidth: parent.height - (Layout.margins * 2) + Layout.preferredHeight: parent.height - (Layout.margins * 2) + Layout.maximumWidth: { + const mWidth = parent.width - (Layout.margins * 2); + return mWidth > 0 ? mWidth : 0; + } + Layout.fillHeight: true + } + + ColumnLayout { + Layout.maximumWidth: parent.width / 2 + Layout.fillWidth: true + clip: true + + Text { + text: Player.current?.trackTitle ?? "Unknown Track" + color: "white" + Layout.alignment: Qt.AlignCenter + Layout.maximumWidth: parent.width + elide: Text.ElideRight + } + + Text { + text: Player.current?.trackAlbum ?? "Unknown Album" + color: "white" + Layout.alignment: Qt.AlignCenter + Layout.maximumWidth: parent.width + elide: Text.ElideRight + } + + Text { + text: Player.current?.trackAlbumArtist ?? "Unknown Artist" + color: "white" + Layout.alignment: Qt.AlignCenter + Layout.maximumWidth: parent.width + elide: Text.ElideRight + } + + RowLayout { + Layout.alignment: Qt.AlignCenter + BIconButton { + source: Quickshell.iconPath("media-seek-backward") + onClicked: Player.current.previous() + size: 20 + } + BIconButton { + source: Quickshell.iconPath(Player.isPlaying ? "media-playback-pause" : "media-playback-start") + onClicked: Player.current.togglePlaying() + size: 20 + } + + BIconButton { + source: Quickshell.iconPath("media-seek-forward") + onClicked: Player.current.next() + size: 20 + } + } + + Slider { + id: slider + Layout.fillWidth: true + Layout.minimumWidth: 10 + Layout.minimumHeight: 3 + from: 0 + to: Player.current?.length ?? 0 + value: Player.current?.position ?? 0 + enabled: (Player.current?.canSeek && Player.current?.positionSupported) ?? false + + onMoved: { + if (Player.current) + Player.current.position = value; + } + + Component.onCompleted: { + const con = () => mprisSmall.player?.positionChanged.connect(() => { + slider.value = Player.current?.position; + }); + con(); + Player.currentChanged.connect(() => { + con(); + }); + } + + FrameAnimation { + // only emit the signal when the position is actually changing. + running: Player.current?.playbackState == MprisPlaybackState.Playing + // emit the positionChanged signal every frame. + onTriggered: Player.current?.positionChanged() + } + } + } + } +} diff --git a/src/windows/audioman/AudioManager.qml b/src/windows/audioman/AudioManager.qml index d0f8873..1f2cc4b 100644 --- a/src/windows/audioman/AudioManager.qml +++ b/src/windows/audioman/AudioManager.qml @@ -3,11 +3,9 @@ import Quickshell.Services.Pipewire import QtQuick import QtQuick.Layouts import QtQuick.Controls -import Quickshell.Services.Mpris -import "root:base" -import "root:provider" -import QtQuick.Effects -import "root:widgets/MprisBig" +import "../../base" +import "../../provider" +import "../../widgets/MprisBig" PanelWindow { id: audioman @@ -25,53 +23,15 @@ PanelWindow { anchors.fill: parent onClicked: audioman.visible = false - BRectangle { + BlurredImage { id: display - x: 10 y: 10 width: 500 height: 600 radius: 10 - - Image { - id: background - anchors.fill: parent - source: Player.current?.trackArtUrl ?? "" - Layout.alignment: Qt.AlignHCenter - visible: false - anchors.margins: display.border.width - 1 - } - - MultiEffect { - id: image - autoPaddingEnabled: false - source: background - anchors.fill: background - blurEnabled: true - blurMax: 64 - blurMultiplier: 2 - blur: 1 - brightness: -0.15 - contrast: -0.35 - maskEnabled: true - maskSource: mask - } - - Item { - id: mask - width: image.width - height: image.height - layer.enabled: true - visible: false - - Rectangle { - width: image.width - height: image.height - radius: display.radius - color: "black" - } - } + source: Player.current?.trackArtUrl ?? "" + color: "#BD93F9" ScrollView { id: test diff --git a/src/windows/notificationtoast/Toast.qml b/src/windows/notificationtoast/Toast.qml index b2a125e..9eae07b 100644 --- a/src/windows/notificationtoast/Toast.qml +++ b/src/windows/notificationtoast/Toast.qml @@ -9,7 +9,7 @@ import Quickshell.Services.Notifications MouseArea { id: toast - property int lifetime: 5000 + readonly property int lifetime: 5000 property int countdownTime: lifetime required property string appName @@ -21,6 +21,7 @@ MouseArea { required property bool hasActionIcons required property var actions required property int index + Component.onCompleted: () => console.log(toast.actions.values) function close(): void { popupcol.model.remove(toast.index, 1); @@ -125,22 +126,10 @@ MouseArea { Repeater { model: toast.actions - delegate: Button { - id: actionButton - + delegate: ToastAction { required property var modelData - - IconImage { - visible: toast.hasActionIcons - Component.onCompleted: () => { - if (toast.hasActionIcons) { - source = actionButton.modelData.identifier; - } - } - } - - text: modelData.text - onClicked: () => modelData?.invoke() + notifAction: modelData + hasIcons: toast.hasActionIcons } } diff --git a/src/windows/notificationtoast/ToastAction.qml b/src/windows/notificationtoast/ToastAction.qml new file mode 100644 index 0000000..2c222f1 --- /dev/null +++ b/src/windows/notificationtoast/ToastAction.qml @@ -0,0 +1,22 @@ +import QtQuick.Controls +import Quickshell.Widgets +import QtQuick + +Button { + id: actionButton + + required property var notifAction + required property bool hasIcons + + IconImage { + visible: parent.hasIcons + Component.onCompleted: () => { + if (parent.hasIcons) { + source = actionButton.notifAction?.identifier ?? ""; + } + } + } + + text: notifAction?.text ?? "" + onClicked: () => notifAction?.invoke() +}