bar: media indicator

This commit is contained in:
end-4
2025-04-11 14:54:22 +02:00
parent 3bb67a9a4f
commit d9ed5434ac
7 changed files with 262 additions and 16 deletions
+13 -4
View File
@@ -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 {
+1 -2
View File
@@ -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
}
}
+85
View File
@@ -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}`
}
}
}
@@ -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
}
}
@@ -141,13 +141,12 @@ Singleton {
animation: QtObject {
property QtObject elementDecel: QtObject {
property int duration: 100
property int type: Easing.BezierSpline
property list<real> 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
}
}
@@ -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(); }
}
}
@@ -24,7 +24,6 @@ Button {
ColorAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
easing.bezierCurve: Appearance.animation.elementDecel.bezierCurve
}
}