Huge quickshell progress dump

Was requested
This commit is contained in:
outfoxxed 2024-06-17 00:49:34 -07:00
parent 57d9f9a72e
commit 945793973e
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
42 changed files with 2140 additions and 142 deletions

View file

@ -0,0 +1,161 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import ".."
import "../.."
BarWidgetInner {
id: root
border.color: "transparent"
property real renderWidth: width
property real renderHeight: height
property real blurRadius: 20;
property real blurSamples: 41;
property bool reverse: false;
function setArt(art: string, reverse: bool, immediate: bool) {
this.reverse = reverse;
if (art.length == 0) {
stack.replace(null);
} else {
stack.replace(component, { uri: art }, immediate)
}
}
property var component: Component {
Item {
id: componentRoot
property var uri: null;
readonly property bool svReady: image.status === Image.Ready;
Image {
id: image
anchors.centerIn: parent;
source: uri;
cache: false;
asynchronous: true;
fillMode: Image.PreserveAspectCrop;
sourceSize.width: width;
sourceSize.height: height;
width: stack.width + blurRadius * 2;
height: stack.height + blurRadius * 2;
}
property Component blurComponent: Item {
id: blur
//parent: blurContainment
// blur into the neighboring elements if applicable
x: componentRoot.x - blurRadius * 4
y: componentRoot.y + image.y
width: componentRoot.width + blurRadius * 8
height: image.height
onVisibleChanged: {
if (visible) blurSource.scheduleUpdate();
}
Connections {
target: image
function onStatusChanged() {
if (image.status == Image.Ready) {
blurSource.scheduleUpdate();
}
}
}
ShaderEffectSource {
id: blurSource
sourceItem: stack
sourceRect: Qt.rect(blur.x, blur.y, blur.width, blur.height);
live: false
anchors.fill: parent
visible: false
}
Item {
x: blurRadius
width: blur.width - blurRadius * 2
height: blur.height
clip: true
GaussianBlur {
source: blurSource
x: -parent.x
width: blur.width
height: blur.height
radius: root.blurRadius
samples: root.blurSamples
visible: true
}
}
}
// Weird crash if the blur is not owned by its visual parent,
// so it has to be a component.
property Item blur: blurComponent.createObject(blurContainment);
Component.onDestruction: blur.destroy();
}
}
SlideView {
id: stack;
height: renderHeight
width: renderWidth
anchors.centerIn: parent;
visible: false;
animate: root.visible;
readonly property real fromPos: (stack.width + blurRadius * 2) * (reverse ? -1 : 1);
enterTransition: PropertyAnimation {
property: "x"
from: stack.fromPos
to: 0
duration: 400
easing.type: Easing.OutExpo;
}
exitTransition: PropertyAnimation {
property: "x"
to: -stack.fromPos
duration: 400
easing.type: Easing.OutExpo;
}
}
Item {
id: blurContainment
x: stack.x
y: stack.y
width: stack.width
height: stack.height
}
readonly property Rectangle overlay: overlayItem;
Rectangle {
id: overlayItem
visible: false
anchors.fill: parent
border.color: ShellGlobals.colors.widgetOutlineSeparate
border.width: 0//1
radius: 0//root.radius
color: "transparent"
}
// slightly offset on the corners :/
layer.enabled: true
layer.effect: ShaderEffect {
fragmentShader: "radial_clip.frag.qsb"
// +1 seems to match Rectangle
property real radius: root.radius + 1
property size size: Qt.size(root.width, root.height)
property real borderWidth: 1//.5
property color borderColor: ShellGlobals.colors.widgetOutlineSeparate//"#ffff0000"
property color tint: overlayItem.color
}
}

View file

@ -0,0 +1,43 @@
import QtQuick
import QtQuick.Templates as T
T.Slider {
id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitHandleWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitHandleHeight + topPadding + bottomPadding)
background: Rectangle {
x: control.leftPadding
y: control.topPadding + control.availableHeight / 2 - height / 2
implicitWidth: 200
implicitHeight: 7
width: control.availableWidth
height: implicitHeight
radius: 5
color: "#30ceffff"
border.width: 0
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
}
width: control.handle.x + (control.handle.width / 2) - parent.x
radius: parent.radius
color: "#80ceffff"
}
}
handle: Rectangle {
x: control.leftPadding + control.visualPosition * (control.availableWidth - width)
y: control.topPadding + control.availableHeight / 2 - height / 2
implicitWidth: 16
implicitHeight: 16
radius: 8
}
}

View file

@ -0,0 +1,168 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Hyprland
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;
Component.onCompleted: {
for (const player of Mpris.players.values) {
if (player.playbackState == MprisPlaybackState.Playing) {
if (root.trackedPlayer == null) {
root.trackedPlayer = player;
}
}
player.playbackStateChanged.connect(() => {
if (root.trackedPlayer !== player) root.trackedPlayer = player;
});
}
}
Connections {
target: activePlayer
function onTrackChanged() {
root.updateTrack();
}
}
// Change the tracked player when one changes playback state or is created in a playing state.
Connections {
target: Mpris.players;
function onObjectInsertedPost(player: MprisPlayer) {
if (player.playbackState === MprisPlaybackState.Playing) {
if (root.trackedPlayer !== player) root.trackedPlayer = player;
}
player.playbackStateChanged.connect(() => {
if (root.trackedPlayer !== player) root.trackedPlayer = player;
});
}
function onObjectRemovedPre() {
console.log(`trackedPlayer: ${root.trackedPlayer}`)
if (root.trackedPlayer == null) {
for (const player of Mpris.players.values) {
if (player.playbackState === MprisPlaybackState.Playing) {
root.trackedPlayer = player;
break;
}
}
}
}
}
onActivePlayerChanged: this.updateTrack();
function updateTrack() {
const metadata = this.activePlayer?.metadata ?? {};
this.activeTrack = {
artUrl: metadata["mpris:artUrl"] ?? "",
title: metadata["xesam:title"] ?? "",
artist: metadata["xesam:artist"] ?? "",
};
this.trackChanged(__reverse);
this.__reverse = false;
}
property bool isPlaying: this.activePlayer && this.activePlayer.playbackState == MprisPlaybackState.Playing;
property bool canPlay: this.activePlayer?.canPlay ?? false;
function play() {
if (this.canPlay) this.activePlayer.playbackState = MprisPlaybackState.Playing;
}
property bool canPause: this.activePlayer?.canPause ?? false;
function pause() {
if (this.canPause) this.activePlayer.playbackState = MprisPlaybackState.Paused;
}
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 ?? MprisPlayer.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;
}
Shortcut {
name: "music-pauseall";
onPressed: {
for (let i = 0; i < Mpris.players.length; i++) {
const player = Mpris.players[i];
if (player.canPause) player.playbackState = MprisPlaybackState.Paused;
}
}
}
Shortcut {
name: "music-playpause";
onPressed: {
if (root.isPlaying) root.pause();
else root.play();
}
}
Shortcut {
name: "music-previous";
onPressed: root.previous();
}
Shortcut {
name: "music-next";
onPressed: root.next();
}
}

View file

@ -0,0 +1,7 @@
import Quickshell
import Quickshell.Services.Mpris
Scope {
required property MprisPlayer player;
}

View file

@ -0,0 +1,2 @@
import QtQuick
import

View file

@ -0,0 +1,527 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Services.Mpris
import ".."
import "../.."
MouseArea {
id: root
hoverEnabled: true
required property var bar;
implicitHeight: column.implicitHeight + 10
PersistentProperties {
id: persist
reloadableId: "MusicWidget";
property bool widgetOpen: false;
onReloaded: {
rightclickMenu.snapOpacity(widgetOpen ? 1.0 : 0.0)
}
}
property alias widgetOpen: persist.widgetOpen;
acceptedButtons: Qt.RightButton
onClicked: widgetOpen = !widgetOpen
onWheel: event => {
event.accepted = true;
if (MprisController.canChangeVolume) {
this.activePlayer.volume = Math.max(0, Math.min(1, this.activePlayer.volume + (event.angleDelta.y / 120) * 0.05));
}
}
readonly property var activePlayer: MprisController.activePlayer
Item {
id: widget
anchors.fill: parent
property real scaleMul: root.pressed || widgetOpen ? 100 : 1
Behavior on scaleMul { SmoothedAnimation { velocity: 600 } }
scale: scaleCurve.interpolate(scaleMul / 100, 1, (width - 6) / width)
EasingCurve {
id: scaleCurve
curve.type: Easing.Linear
}
implicitHeight: column.implicitHeight + 10
BackgroundArt {
id: bkg
anchors.fill: parent
function updateArt(reverse: bool) {
this.setArt(MprisController.activeTrack.artUrl, reverse, false)
}
Component.onCompleted: this.updateArt(false);
Connections {
target: MprisController
function onTrackChanged(reverse: bool) {
bkg.updateArt(reverse);
}
}
}
ColumnLayout {
id: column
anchors {
fill: parent
margins: 5;
}
ClickableIcon {
Layout.fillWidth: true
image: "root:icons/rewind.svg"
implicitHeight: width
scaleIcon: false
baseMargin: 3
enabled: MprisController.canGoPrevious;
onClicked: MprisController.previous();
}
ClickableIcon {
Layout.fillWidth: true
image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`;
implicitHeight: width
scaleIcon: false
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay;
onClicked: {
if (MprisController.isPlaying) MprisController.pause();
else MprisController.play();
}
}
ClickableIcon {
Layout.fillWidth: true
image: "root:icons/fast-forward.svg"
implicitHeight: width
scaleIcon: false
baseMargin: 3
enabled: MprisController.canGoNext;
onClicked: MprisController.next();
}
}
property var tooltip: TooltipItem {
id: tooltip
tooltip: bar.tooltip
owner: root
show: root.containsMouse && (activePlayer?.metadata["mpris:trackid"] ?? false)
//implicitHeight: root.height - 10
//implicitWidth: childrenRect.width
Item {
implicitWidth: 200
implicitHeight: 100
}
/*Loader {
active: tooltip.visible
sourceComponent: ColumnLayout {
height: root.height - 10
RowLayout {
Image {
Layout.fillHeight: true
source: mainPlayer.metadata["mpris:artUrl"] ?? ""
cache: false
fillMode: Image.PreserveAspectCrop
sourceSize.width: height
sourceSize.height: height
}
Label {
text: mainPlayer.identity
}
}
Slider {
Layout.fillWidth: true
}
}
}*/
}
property var rightclickMenu: TooltipItem {
id: rightclickMenu
tooltip: bar.tooltip
owner: root
isMenu: true
show: widgetOpen
onClose: widgetOpen = false
// some very large covers take a sec to appear in the background,
// so we'll try to preload them.
preloadBackground: root.containsMouse
backgroundComponent: BackgroundArt {
id: popupBkg
anchors.fill: parent
renderHeight: rightclickMenu.implicitHeight
renderWidth: rightclickMenu.implicitWidth
blurRadius: 100
blurSamples: 201
overlay.color: "#80000000"
Connections {
target: MprisController
function onTrackChanged(reverse: bool) {
popupBkg.setArt(MprisController.activeTrack.artUrl, reverse, false);
}
}
Component.onCompleted: {
setArt(MprisController.activeTrack.artUrl, false, true);
}
}
Loader {
width: 500
height: 650
active: rightclickMenu.visible
sourceComponent: ColumnLayout {
property var player: activePlayer;
anchors.fill: parent;
property int position: 0;
property int length: 0;
FrameAnimation {
id: posTracker;
running: player.playbackState == MprisPlaybackState.Playing && widgetOpen;
onTriggered: player.positionChanged();
}
Connections {
target: player
function onPositionChanged() {
const newPosition = Math.floor(player.position);
if (newPosition != position) position = newPosition;
}
function onLengthChanged() {
const newLength = Math.floor(player.length);
if (newLength != length) length = newLength;
}
}
Connections {
target: MprisController
function onTrackChanged(reverse: bool) {
trackStack.updateTrack(reverse, false);
length = Math.floor(player.length);
}
}
Component.onCompleted: {
position = Math.floor(player.position);
length = Math.floor(player.length);
}
function timeStr(time: int): string {
const seconds = time % 60;
const minutes = Math.floor(time / 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
Item {
id: playerSelectorContainment
Layout.fillWidth: true
implicitHeight: playerSelector.implicitHeight + 20
implicitWidth: playerSelector.implicitWidth
ScrollView {
id: playerSelector
anchors.centerIn: parent
width: Math.min(implicitWidth, playerSelectorContainment.width)
RowLayout {
Repeater {
model: Mpris.players
MouseArea {
required property MprisPlayer modelData;
readonly property bool selected: modelData == player;
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
onClicked: MprisController.setActivePlayer(modelData);
Rectangle {
implicitWidth: 50
implicitHeight: 50
radius: 5
color: selected ? "#20ceffff" : "transparent"
Image {
anchors.fill: parent
anchors.margins: 5
// lazy and wont always work, but good enough until a desktop entry impl.
source: {
const entry = DesktopEntries.byId(modelData.desktopEntry);
console.log(`ent ${entry} id ${modelData.desktopEntry}`)
if (!entry) return "image://icon/";
return `image://icon/${entry.icon}`;
}
//asynchronous: true
sourceSize.width: 50
sourceSize.height: 50
cache: false
}
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.bottomMargin: 10
Label {
anchors.centerIn: parent
text: activePlayer.identity
}
}
SlideView {
id: trackStack
Layout.fillWidth: true
implicitHeight: 400
property bool reverse: false;
Component.onCompleted: updateTrack(false, true);
function updateTrack(reverse: bool, immediate: bool) {
this.reverse = reverse;
this.replace(
trackComponent,
{ track: MprisController.activeTrack },
immediate
)
}
property var trackComponent: Component {
Flickable {
id: flickable
required property var track;
// in most cases this is ready around the same time as the background,
// but may take longer if the image is huge.
readonly property bool svReady: img.status === Image.Ready;
contentWidth: width + 1
onDragEnded: {
return;
console.log(`dragend ${contentX}`)
if (Math.abs(contentX) > 75) {
if (contentX < 0) MprisController.previous();
else if (contentX > 0) MprisController.next();
}
}
ColumnLayout {
id: trackContent
width: flickable.width
height: flickable.height
Item {
Layout.fillWidth: true
implicitHeight: 300//img.implicitHeight
implicitWidth: img.implicitWidth
Image {
id: img;
anchors.centerIn: parent;
source: track.artUrl ?? "";
//height: 300
//fillMode: Image.PreserveAspectFit
cache: false
asynchronous: true
sourceSize.height: 300
sourceSize.width: 300
}
}
Item {
Layout.fillWidth: true
Layout.topMargin: 20
Label {
anchors.centerIn: parent
text: track.title
}
}
Item {
Layout.fillWidth: true
Layout.topMargin: 20
Label {
anchors.centerIn: parent
text: track.artist
}
}
Item { Layout.fillHeight: true }
}
}
}
readonly property real fromPos: trackStack.width * (trackStack.reverse ? -1 : 1);
// intentionally slightly faster than the background
enterTransition: PropertyAnimation {
property: "x"
from: trackStack.fromPos
to: 0;
duration: 350;
easing.type: Easing.OutExpo;
}
exitTransition: PropertyAnimation {
property: "x"
to: target.x - trackStack.fromPos;
duration: 350;
easing.type: Easing.OutExpo;
}
}
Item { Layout.fillHeight: true }
Item {
Layout.fillWidth: true
implicitHeight: controlsRow.implicitHeight
RowLayout {
id: controlsRow
anchors.centerIn: parent
ClickableIcon {
image: {
switch (MprisController.loopState) {
case MprisLoopState.None: return "root:icons/repeat-none.svg";
case MprisLoopState.Playlist: return "root:icons/repeat-all.svg";
case MprisLoopState.Track: return "root:icons/repeat-once.svg";
}
}
implicitWidth: 50
implicitHeight: width
scaleIcon: false
baseMargin: 3
enabled: MprisController.loopSupported;
onClicked: {
let target = MprisLoopState.None;
switch (MprisController.loopState) {
case MprisLoopState.None: target = MprisLoopState.Playlist; break;
case MprisLoopState.Playlist: target = MprisLoopState.Track; break;
case MprisLoopState.Track: target = MprisLoopState.None; break;
}
MprisController.setLoopState(target);
}
}
ClickableIcon {
image: "root:icons/rewind.svg"
implicitWidth: 60
implicitHeight: width
scaleIcon: false
baseMargin: 3
enabled: MprisController.canGoPrevious;
onClicked: MprisController.previous();
}
ClickableIcon {
image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`;
Layout.leftMargin: -10
Layout.rightMargin: -10
implicitWidth: 80
implicitHeight: width
scaleIcon: false
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay
onClicked: {
if (MprisController.isPlaying) MprisController.pause();
else MprisController.play();
}
}
ClickableIcon {
image: "root:icons/fast-forward.svg"
implicitWidth: 60
implicitHeight: width
scaleIcon: false
baseMargin: 3
enabled: MprisController.canGoNext;
onClicked: MprisController.next();
}
ClickableIcon {
image: `root:icons/${MprisController.hasShuffle ? "shuffle" : "shuffle-off"}.svg`
implicitWidth: 50
implicitHeight: width
scaleIcon: false
enabled: MprisController.shuffleSupported;
onClicked: MprisController.setShuffle(!MprisController.hasShuffle);
}
}
}
RowLayout {
Label {
Layout.preferredWidth: lengthLabel.implicitWidth
text: timeStr(position)
}
MediaSlider {
id: slider
Layout.fillWidth: true
property var bindSlider: true;
enabled: player.canSeek
from: 0
to: player.length
onPressedChanged: {
if (!pressed) player.position = value;
bindSlider = !pressed;
}
Binding {
when: slider.bindSlider
slider.value: player.position
}
}
Label {
id: lengthLabel
text: timeStr(length)
}
}
}
}
}
}
}