diff options
| -rw-r--r-- | bar/Bar.qml | 3 | ||||
| -rw-r--r-- | bar/Mpris.qml | 57 | ||||
| -rw-r--r-- | bar/Workspace.qml | 18 | ||||
| -rw-r--r-- | services/SHyprland.qml (renamed from services/Hyprland.qml) | 0 | ||||
| -rw-r--r-- | services/SMpris.qml | 188 |
5 files changed, 259 insertions, 7 deletions
diff --git a/bar/Bar.qml b/bar/Bar.qml index 4bb56e5..8bef666 100644 --- a/bar/Bar.qml +++ b/bar/Bar.qml @@ -43,16 +43,19 @@ Scope { id: leftRow Layout.preferredWidth: 1 Layout.fillWidth: true + spacing: 40 // Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Workspace { id: workspace Layout.preferredWidth: 3 + Layout.fillHeight: true Layout.fillWidth: true } Mpris { id: mpris Layout.fillWidth: true + Layout.fillHeight: true Layout.preferredWidth: 1 } } diff --git a/bar/Mpris.qml b/bar/Mpris.qml new file mode 100644 index 0000000..880456b --- /dev/null +++ b/bar/Mpris.qml @@ -0,0 +1,57 @@ +import QtQuick.Layouts +import QtQuick +import Quickshell +import Quickshell.Services.Mpris +import qs.services + +// import Quickshell.Utils + +Item { + id: root + implicitWidth: parent.width + height: Bar.height + readonly property MprisPlayer activePlayer: SMpris.activePlayer + readonly property string cleanedTitle: cleanMusicTitle(activePlayer?.trackTitle) || qsTr("No media") + // length broken (https://github.com/quickshell-mirror/quickshell/issues/109) + readonly property string position: friendlyTimeForSeconds(activePlayer?.position) || "0:00" + readonly property string length: friendlyTimeForSeconds(activePlayer?.length) || "0:00" + readonly property string progress: activePlayer.positionSupported && position != length ? position + "/" + length : activePlayer.positionSupported && !activePlayer.lengthSupported ? !activePlayer.positionSupported && activePlayer.lengthSupported ? "" : position : activePlayer.lengthSupported ? length : "" + readonly property string state: activePlayer?.isPlaying ? "Playing" : "Paused" + + function cleanMusicTitle(title: string): string { + if (!title) + return ""; + title = title.replace(/\s*\([^)]*\)/g, " "); // Round brackets + title = title.replace(/\s*\[[^\]]*\]/g, " "); // Square brackets + title = title.replace(/\s*\{[^\}]*\}/g, " "); // Curly brackets + title = title.replace(/\s*【[^】]*】/g, " "); // Touhou + title = title.replace(/\s*《[^》]*》/g, " "); + title = title.replace(/\s*「[^」]*」/g, " "); + title = title.replace(/\s*『[^』]*』/g, " "); + + return title.trim(); + } + + function friendlyTimeForSeconds(seconds) { + if (isNaN(seconds) || seconds < 0) + return "0:00"; + seconds = Math.floor(seconds); + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + if (h > 0) { + return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; + } else { + return `${m}:${s.toString().padStart(2, '0')}`; + } + } + + Text { + anchors.centerIn: parent + text: root.state + " " + root.cleanedTitle + " [" + root.progress + "]" + } + // hover + // Text { + // text: root.activePlayer.trackTitle + " by " + root.activePlayer.trackArtist + " - " + root.activePlayer.trackAlbum + root.activePlayer.position + "/" + root.activePlayer.length + // } +} diff --git a/bar/Workspace.qml b/bar/Workspace.qml index fd9d521..de63eba 100644 --- a/bar/Workspace.qml +++ b/bar/Workspace.qml @@ -5,29 +5,29 @@ import qs.services import qs.config Item { - Layout.fillHeight: true + id: root property list<bool> workspaceOccupied: [] function updateWorkspaceOccupied() { // workspaceOccupied = Array.from({ length: Config.options.bar.workspaces.shown }, (_, i) => { - // return Hyprland.workspaces.values.some(ws => ws.id === workspaceGroup * Config.options.bar.workspaces.shown + i + 1); + // return SHyprland.workspaces.values.some(ws => ws.id === workspaceGroup * Config.options.bar.workspaces.shown + i + 1); // }) workspaceOccupied = Array.from({ length: Bar.workspaces }, (_, i) => { - return Hyprland.workspaces.values.some(ws => ws.id === (i + 1)); + return SHyprland.workspaces.values.some(ws => ws.id === (i + 1)); }); } function isActive(index: int): bool { - Hyprland.focusedWorkspace.id.toString() == (index + 1).toString(); + SHyprland.focusedWorkspace.id.toString() == (index + 1).toString(); } function isOccupied(index: int): bool { return workspaceOccupied[index] === true; } Connections { - target: Hyprland.workspaces + target: SHyprland.workspaces function onValuesChanged() { updateWorkspaceOccupied(); } @@ -36,14 +36,18 @@ Item { Component.onCompleted: updateWorkspaceOccupied() RowLayout { - anchors.fill: parent + id: layout + implicitWidth: parent.width + height: Bar.height + spacing: 0 + Repeater { model: Bar.workspaces Rectangle { Text { - text: Hyprland.focusedWorkspace?.id.toString() == (index + 1).toString() + text: SHyprland.focusedWorkspace?.id.toString() == (index + 1).toString() // text: isActive(index) } implicitWidth: 10 diff --git a/services/Hyprland.qml b/services/SHyprland.qml index dfbc874..dfbc874 100644 --- a/services/Hyprland.qml +++ b/services/SHyprland.qml diff --git a/services/SMpris.qml b/services/SMpris.qml new file mode 100644 index 0000000..012a354 --- /dev/null +++ b/services/SMpris.qml @@ -0,0 +1,188 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +// From https://git.outfoxxed.me/outfoxxed/nixnew +// It does not have a license, but the author is okay with redistribution. + +import QtQml.Models +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Mpris + +/** + * A service that provides easy access to the active Mpris player. + */ +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 + + Timer { + running: root.activePlayer?.playbackState == MprisPlaybackState.Playing && root.activePlayer?.positionSupported + interval: 1000 + repeat: true + onTriggered: root.activePlayer.positionChanged() + } + + 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; + } + } + // not working https://github.com/quickshell-mirror/quickshell/issues/111 + // function onPositionChanged() { + // } + } + + 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 || qsTr("Unknown Title"), + artist: this.activePlayer?.trackArtist || qsTr("Unknown Artist"), + album: this.activePlayer?.trackAlbum || qsTr("Unknown Album"), + position: this.activePlayer?.position ?? 0, + length: this.activePlayer?.length ?? 0 + }; + + this.trackChanged(__reverse); + this.__reverse = false; + } + + function updateProgress() { + this.activePlayer.position; + } + + 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(`[Mpris] Active player ${targetPlayer} << ${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(); + } + } +} |
