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