Huge quickshell progress dump
Was requested
This commit is contained in:
parent
57d9f9a72e
commit
945793973e
42 changed files with 2140 additions and 142 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
|
||||
Scope {
|
||||
required property MprisPlayer player;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
import QtQuick
|
||||
import
|
||||
527
modules/user/modules/quickshell/shell/bar/mpris/Players.qml
Normal file
527
modules/user/modules/quickshell/shell/bar/mpris/Players.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue