diff --git a/.config/quickshell/modules/bar/Bar.qml b/.config/quickshell/modules/bar/Bar.qml index 6871a964f..0e7bcabd5 100644 --- a/.config/quickshell/modules/bar/Bar.qml +++ b/.config/quickshell/modules/bar/Bar.qml @@ -5,6 +5,10 @@ import QtQuick.Layouts import Quickshell Scope { + id: bar + readonly property int barHeight: 40 + readonly property int sideCenterModuleWidth: 360 + Variants { model: Quickshell.screens @@ -14,7 +18,7 @@ Scope { property var modelData screen: modelData - height: 40 + height: barHeight color: Appearance.colors.colLayer0 // Left section @@ -25,17 +29,21 @@ Scope { // Middle section RowLayout { anchors.centerIn: parent - implicitWidth: 500 spacing: 8 RowLayout { + Layout.preferredWidth: sideCenterModuleWidth spacing: 4 - Layout.fillWidth: true Layout.fillHeight: true + implicitWidth: 350 Resources { } + Media { + Layout.fillWidth: true + } + } RowLayout { @@ -50,12 +58,13 @@ Scope { } RowLayout { - Layout.fillWidth: true + Layout.preferredWidth: sideCenterModuleWidth Layout.fillHeight: true spacing: 4 ClockWidget { Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true } UtilButtons { diff --git a/.config/quickshell/modules/bar/Battery.qml b/.config/quickshell/modules/bar/Battery.qml index 215458ec6..b4cbea1f3 100644 --- a/.config/quickshell/modules/bar/Battery.qml +++ b/.config/quickshell/modules/bar/Battery.qml @@ -27,13 +27,12 @@ Rectangle { anchors.centerIn: parent Rectangle { - implicitWidth: (isCharging ? boltIcon.width : 0) - rowLayout.spacing + implicitWidth: (isCharging ? boltIcon.width : 0) Behavior on implicitWidth { NumberAnimation { duration: Appearance.animation.elementDecel.duration easing.type: Appearance.animation.elementDecel.type - easing.bezierCurve: Appearance.animation.elementDecel.bezierCurve } } diff --git a/.config/quickshell/modules/bar/Media.qml b/.config/quickshell/modules/bar/Media.qml new file mode 100644 index 000000000..44431f016 --- /dev/null +++ b/.config/quickshell/modules/bar/Media.qml @@ -0,0 +1,85 @@ +import "../common" +import "../common/widgets" +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Services.Mpris + +Rectangle { + readonly property MprisPlayer activePlayer: MprisController.activePlayer + readonly property string cleanedTitle: activePlayer?.trackTitle.replace(/【[^】]*】/, "") || "No media" + + Layout.fillHeight: true + implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2 + implicitHeight: 40 + color: "transparent" + + // Background + Rectangle { + anchors.centerIn: parent + width: parent.width + implicitHeight: 32 + color: Appearance.colors.colLayer1 + radius: Appearance.rounding.small + } + + Timer { + running: activePlayer?.playbackState == MprisPlaybackState.Playing + interval: 1000 + repeat: true + onTriggered: activePlayer.positionChanged() + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.MiddleButton | Qt.BackButton | Qt.ForwardButton | Qt.RightButton + onPressed: (event) => { + if (event.button === Qt.MiddleButton) { + activePlayer.togglePlaying(); + } else if (event.button === Qt.BackButton) { + activePlayer.previous(); + } else if (event.button === Qt.ForwardButton || event.button === Qt.RightButton) { + activePlayer.next(); + } + } + } + + RowLayout { + id: rowLayout + + spacing: 4 + anchors.fill: parent + + CircularProgress { + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: rowLayout.spacing + lineWidth: 2 + value: activePlayer?.position / activePlayer?.length + size: 26 + secondaryColor: Appearance.m3colors.m3secondaryContainer + primaryColor: Appearance.m3colors.m3onSecondaryContainer + + MaterialSymbol { + anchors.centerIn: parent + text: activePlayer?.isPlaying ? "pause" : "play_arrow" + font.pointSize: Appearance.font.pointSize.normal + color: Appearance.m3colors.m3onSecondaryContainer + } + + } + + StyledText { + width: rowLayout.width - (CircularProgress.size + rowLayout.spacing * 2) // TODO ADJUST THIS + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true // Ensures the text takes up available space + Layout.rightMargin: rowLayout.spacing + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight // Truncates the text on the right + color: Appearance.colors.colOnLayer1 + text: `${cleanedTitle} • ${activePlayer?.trackArtist}` + } + + } + +} diff --git a/.config/quickshell/modules/bar/Workspaces.qml b/.config/quickshell/modules/bar/Workspaces.qml index 6730a6515..26274d4a3 100644 --- a/.config/quickshell/modules/bar/Workspaces.qml +++ b/.config/quickshell/modules/bar/Workspaces.qml @@ -107,14 +107,12 @@ Rectangle { NumberAnimation { duration: Appearance.animation.elementDecel.duration easing.type: Appearance.animation.elementDecel.type - easing.bezierCurve: Appearance.animation.elementDecel.bezierCurve } } Behavior on radiusLeft { NumberAnimation { duration: Appearance.animation.elementDecel.duration easing.type: Appearance.animation.elementDecel.type - easing.bezierCurve: Appearance.animation.elementDecel.bezierCurve } } @@ -122,7 +120,6 @@ Rectangle { NumberAnimation { duration: Appearance.animation.elementDecel.duration easing.type: Appearance.animation.elementDecel.type - easing.bezierCurve: Appearance.animation.elementDecel.bezierCurve } } @@ -177,7 +174,6 @@ Rectangle { ColorAnimation { duration: Appearance.animation.elementDecel.duration easing.type: Appearance.animation.elementDecel.type - easing.bezierCurve: Appearance.animation.elementDecel.bezierCurve } } diff --git a/.config/quickshell/modules/common/Appearance.qml b/.config/quickshell/modules/common/Appearance.qml index 00783c13c..d0b62f3a4 100644 --- a/.config/quickshell/modules/common/Appearance.qml +++ b/.config/quickshell/modules/common/Appearance.qml @@ -141,13 +141,12 @@ Singleton { animation: QtObject { property QtObject elementDecel: QtObject { - property int duration: 100 - property int type: Easing.BezierSpline - property list bezierCurve: [0, 0.55, 0.45, 1] + property int duration: 180 + property int type: Easing.OutCirc } property QtObject menuDecel: QtObject { - property int duration: 250 - property int type: Easing.OutCubic + property int duration: 350 + property int type: Easing.OutQuint } } diff --git a/.config/quickshell/modules/common/MprisController.qml b/.config/quickshell/modules/common/MprisController.qml new file mode 100644 index 000000000..24977f749 --- /dev/null +++ b/.config/quickshell/modules/common/MprisController.qml @@ -0,0 +1,159 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQml.Models +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Mpris +import "../.." + +Singleton { + id: root; + property MprisPlayer trackedPlayer: null; + property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null; + signal trackChanged(reverse: bool); + + property bool __reverse: false; + + property var activeTrack; + + Instantiator { + model: Mpris.players; + + Connections { + required property MprisPlayer modelData; + target: modelData; + + Component.onCompleted: { + if (root.trackedPlayer == null || modelData.isPlaying) { + root.trackedPlayer = modelData; + } + } + + Component.onDestruction: { + if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) { + for (const player of Mpris.players.values) { + if (player.playbackState.isPlaying) { + root.trackedPlayer = player; + break; + } + } + + if (trackedPlayer == null && Mpris.players.values.length != 0) { + trackedPlayer = Mpris.players.values[0]; + } + } + } + + function onPlaybackStateChanged() { + if (root.trackedPlayer !== modelData) root.trackedPlayer = modelData; + } + } + } + + Connections { + target: activePlayer + + function onPostTrackChanged() { + root.updateTrack(); + } + + function onTrackArtUrlChanged() { + console.log("arturl:", activePlayer.trackArtUrl) + //root.updateTrack(); + if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) { + // cantata likes to send cover updates *BEFORE* updating the track info. + // as such, art url changes shouldn't be able to break the reverse animation + const r = root.__reverse; + root.updateTrack(); + root.__reverse = r; + + } + } + } + + onActivePlayerChanged: this.updateTrack(); + + function updateTrack() { + //console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`) + this.activeTrack = { + uniqueId: this.activePlayer?.uniqueId ?? 0, + artUrl: this.activePlayer?.trackArtUrl ?? "", + title: this.activePlayer?.trackTitle || "Unknown Title", + artist: this.activePlayer?.trackArtist || "Unknown Artist", + album: this.activePlayer?.trackAlbum || "Unknown Album", + }; + + this.trackChanged(__reverse); + this.__reverse = false; + } + + property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying; + property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false; + function togglePlaying() { + if (this.canTogglePlaying) this.activePlayer.togglePlaying(); + } + + property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false; + function previous() { + if (this.canGoPrevious) { + this.__reverse = true; + this.activePlayer.previous(); + } + } + + property bool canGoNext: this.activePlayer?.canGoNext ?? false; + function next() { + if (this.canGoNext) { + this.__reverse = false; + this.activePlayer.next(); + } + } + + property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl; + + property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl; + property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None; + function setLoopState(loopState: var) { + if (this.loopSupported) { + this.activePlayer.loopState = loopState; + } + } + + property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl; + property bool hasShuffle: this.activePlayer?.shuffle ?? false; + function setShuffle(shuffle: bool) { + if (this.shuffleSupported) { + this.activePlayer.shuffle = shuffle; + } + } + + function setActivePlayer(player: MprisPlayer) { + const targetPlayer = player ?? Mpris.players[0]; + console.log(`setactive: ${targetPlayer} from ${activePlayer}`) + + if (targetPlayer && this.activePlayer) { + this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer); + } else { + // always animate forward if going to null + this.__reverse = false; + } + + this.trackedPlayer = targetPlayer; + } + + IpcHandler { + target: "mpris" + + function pauseAll(): void { + for (const player of Mpris.players.values) { + if (player.canPause) player.pause(); + } + } + + function playPause(): void { root.togglePlaying(); } + function previous(): void { root.previous(); } + function next(): void { root.next(); } + } +} diff --git a/.config/quickshell/modules/common/widgets/SmallCircleButton.qml b/.config/quickshell/modules/common/widgets/SmallCircleButton.qml index cd881f38b..4cb2e9e61 100644 --- a/.config/quickshell/modules/common/widgets/SmallCircleButton.qml +++ b/.config/quickshell/modules/common/widgets/SmallCircleButton.qml @@ -24,7 +24,6 @@ Button { ColorAnimation { duration: Appearance.animation.elementDecel.duration easing.type: Appearance.animation.elementDecel.type - easing.bezierCurve: Appearance.animation.elementDecel.bezierCurve } }