summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bar/Bar.qml3
-rw-r--r--bar/Mpris.qml57
-rw-r--r--bar/Workspace.qml18
-rw-r--r--services/SHyprland.qml (renamed from services/Hyprland.qml)0
-rw-r--r--services/SMpris.qml188
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();
+ }
+ }
+}