forked from Shinonome/dots-hyprland
bar: media indicator
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user