Warning, /plasma/plasma-workspace/applets/mediacontroller/package/contents/ui/AlbumArtStackView.qml is written in an unsupported language. File is not indexed.
0001 /*
0002 SPDX-FileCopyrightText: 2013 Marco Martin <mart@kde.org>
0003 SPDX-FileCopyrightText: 2014 Sebastian Kügler <sebas@kde.org>
0004 SPDX-FileCopyrightText: 2014 Kai Uwe Broulik <kde@privat.broulik.de>
0005
0006 SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008
0009 import QtQuick 2.15
0010 import QtQuick.Controls 2.15
0011 import QtQuick.Window 2.15
0012
0013 import org.kde.plasma.plasmoid 2.0
0014 import org.kde.plasma.components 3.0 as PC3
0015 import org.kde.plasma.extras 2.0 as PlasmaExtras
0016 import org.kde.plasma.private.mpris as Mpris
0017 import org.kde.kirigami 2.20 as Kirigami
0018
0019 Item {
0020 id: container
0021
0022 /**
0023 * Whether the album art image is available or in loading status
0024 */
0025 readonly property bool hasImage: (pendingImage !== null && (pendingImage.status === Image.Ready || pendingImage.status === Image.Loading))
0026 || (albumArt.currentItem instanceof Image && albumArt.currentItem.status === Image.Ready)
0027
0028 readonly property bool animating: exitTransition.running || popExitTransition.running
0029
0030 /**
0031 * Whether the component is used in the compact representation
0032 */
0033 property bool inCompactRepresentation: false
0034
0035 /**
0036 * Provides source item for \ShaderEffectSource
0037 */
0038 readonly property alias albumArt: albumArt
0039
0040 property Image pendingImage: null
0041
0042 function loadAlbumArt() {
0043 if (pendingImage !== null) {
0044 pendingImage.destroy();
0045 pendingImage = null;
0046 }
0047
0048 if (!root.albumArt) {
0049 albumArt.clear(StackView.PopTransition);
0050 return;
0051 }
0052
0053 const oldImageRatio = albumArt.currentItem instanceof Image ? albumArt.currentItem.sourceSize.width / albumArt.currentItem.sourceSize.height : 1;
0054 pendingImage = albumArtComponent.createObject(albumArt, {
0055 "source": root.albumArt,
0056 "sourceSize": Qt.size(container.width * Screen.devicePixelRatio, container.height * Screen.devicePixelRatio),
0057 "opacity": 0,
0058 });
0059
0060 function replaceWhenLoaded() {
0061 // There can be a potential race: when the previous player is gone but the pending image is just ready in time,
0062 // pendingImage.destroy() -> QQuickImage::deleteLater(), so in the event queue statusChanged may be emitted
0063 // before pendingImage is deleted, but pendingImage is already set to null when the previous player is gone.
0064 if (pendingImage === null || pendingImage.status === Image.Loading) {
0065 return;
0066 }
0067
0068 if (pendingImage.status === Image.Ready) {
0069 const newImageRatio = pendingImage.sourceSize.width / pendingImage.sourceSize.height;
0070 exitTransitionOpacityAnimator.duration = oldImageRatio === newImageRatio ? 0 : Kirigami.Units.longDuration;
0071 } else {
0072 // Load placeholder icon, but keep the invalid image to avoid flashing application icons
0073 exitTransitionOpacityAnimator.duration = 0;
0074 }
0075
0076 pendingImage.statusChanged.disconnect(replaceWhenLoaded);
0077 albumArt.replace(pendingImage, {}, StackView.ReplaceTransition);
0078 pendingImage = null;
0079 }
0080
0081 pendingImage.statusChanged.connect(replaceWhenLoaded);
0082 replaceWhenLoaded();
0083 }
0084
0085 Connections {
0086 enabled: root.expanded // BUG 477866
0087 target: container
0088 function onWidthChanged() {
0089 geometryChangeTimer.restart();
0090 }
0091 function onHeightChanged() {
0092 geometryChangeTimer.restart();
0093 }
0094 }
0095
0096 // Reload album art when size of container changes
0097 Timer {
0098 id: geometryChangeTimer
0099 interval: 250
0100 onTriggered: container.loadAlbumArt();
0101 }
0102
0103 StackView {
0104 id: albumArt
0105
0106 anchors.fill: parent
0107
0108 readonly property string icon: root.iconName || "media-album-cover"
0109
0110 replaceEnter: Transition {
0111 OpacityAnimator {
0112 from: 0
0113 to: 1
0114 duration: Kirigami.Units.longDuration
0115 }
0116 }
0117
0118 replaceExit: Transition {
0119 id: exitTransition
0120
0121 SequentialAnimation {
0122 PauseAnimation {
0123 duration: Kirigami.Units.longDuration
0124 }
0125
0126 /**
0127 * If the new ratio and the old ratio are different,
0128 * perform a fade-out animation for the old image
0129 * to prevent it from suddenly disappearing.
0130 */
0131 OpacityAnimator {
0132 id: exitTransitionOpacityAnimator
0133 from: 1
0134 to: 0
0135 duration: 0
0136 }
0137 }
0138 }
0139
0140 popExit: Transition {
0141 id: popExitTransition
0142
0143 OpacityAnimator {
0144 from: 1
0145 to: 0
0146 duration: Kirigami.Units.longDuration
0147 }
0148 }
0149
0150 Component {
0151 id: albumArtComponent
0152
0153 Image { // Album Art
0154 horizontalAlignment: Image.AlignHCenter
0155 verticalAlignment: Image.AlignVCenter
0156 fillMode: container.inCompactRepresentation ? Image.PreserveAspectCrop : Image.PreserveAspectFit
0157
0158 asynchronous: true
0159 cache: false
0160
0161 // onRemoved only fires when all transitions end. If a user switches songs quickly this adds up
0162 // Given it's such a heavy item, try to cleanup as early as possible
0163 StackView.onDeactivated: destroy()
0164 StackView.onRemoved: destroy()
0165 }
0166 }
0167
0168 Component {
0169 id: fallbackIconItem
0170
0171 Kirigami.Icon { // Fallback
0172 id: fallbackIcon
0173
0174 anchors.margins: Kirigami.Units.gridUnit * 2
0175 opacity: 0
0176 source: albumArt.icon
0177
0178 NumberAnimation {
0179 duration: Kirigami.Units.longDuration
0180 easing.type: Easing.OutCubic
0181 property: "opacity"
0182 running: true
0183 target: fallbackIcon
0184 to: 1
0185 }
0186 }
0187 }
0188
0189 // "No media playing" placeholder message
0190 Component {
0191 id: placeholderMessage
0192
0193 // Put PlaceholderMessage in Item so PlaceholderMessage will not fill its parent.
0194 Item {
0195 property alias source: message.iconName
0196
0197 PlasmaExtras.PlaceholderMessage {
0198 id: message
0199 anchors.centerIn: parent
0200 width: parent.width // For text wrap
0201 iconName: albumArt.icon
0202 text: root.playbackStatus > Mpris.PlaybackStatus.Stopped ? i18n("No title") : i18n("No media playing")
0203 }
0204 }
0205 }
0206
0207 Component {
0208 id: busyComponent
0209
0210 Item {
0211 PC3.BusyIndicator {
0212 id: busyIndicator
0213 anchors.centerIn: parent
0214 running: false
0215 }
0216
0217 SequentialAnimation {
0218 running: true
0219
0220 PauseAnimation {
0221 duration: Kirigami.Units.longDuration
0222 }
0223 PropertyAction {
0224 property: "running"
0225 target: busyIndicator
0226 value: true
0227 }
0228 }
0229 }
0230
0231 }
0232 }
0233
0234 Loader {
0235 anchors.fill: parent
0236 visible: active
0237
0238 readonly property bool isLoadingImage: pendingImage !== null && pendingImage.status === Image.Loading
0239
0240 active: (inCompactRepresentation || root.expanded) && (!container.hasImage || isLoadingImage)
0241 asynchronous: true
0242 sourceComponent: root.track ? (isLoadingImage ? busyComponent : fallbackIconItem) : placeholderMessage
0243 }
0244 }
0245