Warning, /plasma/discover/discover/qml/CarouselDelegate.qml is written in an unsupported language. File is not indexed.

0001 /*
0002  *   SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
0003  *
0004  *   SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 import QtQuick
0008 import QtQuick.Controls as QQC2
0009 import QtQuick.Templates as T
0010 import Qt5Compat.GraphicalEffects as GE
0011 import org.kde.kirigami as Kirigami
0012 
0013 Item {
0014     id: delegate
0015 
0016     signal activated()
0017 
0018     required property int index
0019 
0020     required property url small_image_url
0021     required property url large_image_url
0022 
0023     readonly property url smallImageUrlIfNeeded: {
0024         if (small_image_url.toString() !== ""
0025             && (!loadLargeImage
0026                 || !controlRoot.largeImageView
0027                 || controlRoot.largeImageView.status !== Image.Ready)) {
0028             return small_image_url;
0029         }
0030         return "";
0031     }
0032 
0033     required property bool isAnimated
0034     readonly property bool isProbablyAnimated: isAnimated || large_image_url.toString().endsWith(".gif")
0035 
0036     property bool dim: true
0037 
0038     property bool loadLargeImage: false
0039 
0040     readonly property real widestRatio: 2/1
0041 
0042     anchors.verticalCenter: parent?.verticalCenter
0043     height: {
0044         if (!ListView.view) {
0045             return 0;
0046         }
0047         const widthForWidestRatio = Math.round(ListView.view.height * widestRatio);
0048         if (widthForWidestRatio > ListView.view.width) {
0049             const ratio = widthForWidestRatio / ListView.view.width;
0050             return Math.floor(ListView.view.height / ratio);
0051         } else {
0052             return ListView.view.height;
0053         }
0054     }
0055     width: Math.round(height * widestRatio)
0056 
0057     readonly property bool isCurrentItem: ListView.isCurrentItem
0058 
0059     readonly property alias activeImage: controlRoot.activeImage
0060     readonly property alias activeAnimatedImage: controlRoot.activeAnimatedImage
0061 
0062     QQC2.AbstractButton {
0063         id: controlRoot
0064 
0065         property real radius: Kirigami.Units.largeSpacing
0066 
0067         readonly property real minimumRatio: 1/2
0068         readonly property real maximumRatio: 2/1
0069 
0070         readonly property real preferredRatio: {
0071             if (!activeImage || activeImage.status !== Image.Ready || activeImage.implicitHeight === 0) {
0072                 return 3/2;
0073             }
0074             return activeImage.implicitWidth / activeImage.implicitHeight;
0075         }
0076 
0077         readonly property real ratio: {
0078             return Math.max(minimumRatio, Math.min(maximumRatio, preferredRatio));
0079         }
0080 
0081         readonly property bool smallImageFailed: smallImageView !== null && smallImageView.status === Image.Error
0082 
0083         // Purely for graphical purposes, in case small image is not
0084         // available. Does not affect playback behavior.
0085         readonly property bool effectiveLoadLargeImage: delegate.loadLargeImage || smallImageFailed
0086 
0087         readonly property Image activeImage: effectiveLoadLargeImage ? largeImageView : smallImageView
0088 
0089         readonly property AnimatedImage activeAnimatedImage: activeImage as AnimatedImage
0090 
0091         readonly property Image largeImageView: largeImageLoader.item
0092         readonly property Image smallImageView: smallImageLoader.item
0093 
0094         width: Math.round(height * ratio)
0095         height: parent.height - backgroundShadow.shadow.size
0096 
0097         anchors.centerIn: parent
0098         anchors.verticalCenterOffset: -backgroundShadow.shadow.yOffset
0099 
0100         padding: 0
0101         topPadding: undefined
0102         leftPadding: undefined
0103         rightPadding: undefined
0104         bottomPadding: undefined
0105         verticalPadding: undefined
0106         horizontalPadding: undefined
0107 
0108         contentItem: Item {
0109             id: content
0110 
0111             implicitWidth: controlRoot.activeImage?.implicitWidth ?? 0
0112             implicitHeight: controlRoot.activeImage?.implicitHeight ?? 0
0113 
0114             layer.enabled: true
0115             layer.effect: GE.OpacityMask {
0116                 maskSource: Rectangle {
0117                     width: content.width
0118                     height: content.height
0119 
0120                     color: "black"
0121                     radius: controlRoot.radius
0122                 }
0123             }
0124 
0125             Rectangle {
0126                 anchors.fill: parent
0127 
0128                 color: Qt.tint(controlRoot.Kirigami.Theme.backgroundColor, Qt.alpha(controlRoot.Kirigami.Theme.textColor, 0.14))
0129 
0130                 QQC2.BusyIndicator {
0131                     anchors.centerIn: parent
0132                     running: controlRoot.activeImage?.status === Image.Loading
0133                 }
0134 
0135                 Kirigami.Icon {
0136                     anchors.centerIn: parent
0137                     implicitWidth: Kirigami.Units.iconSizes.large
0138                     implicitHeight: Kirigami.Units.iconSizes.large
0139                     visible: controlRoot.activeImage?.status === Image.Error
0140                     source: "image-missing"
0141                 }
0142             }
0143 
0144             ConditionalLoader {
0145                 id: smallImageLoader
0146 
0147                 anchors.fill: parent
0148                 z: 1
0149 
0150                 condition: delegate.isProbablyAnimated
0151 
0152                 componentTrue: AnimatedImage {
0153                     fillMode: Image.PreserveAspectFit
0154                     source: delegate.smallImageUrlIfNeeded
0155 
0156                     playing: true
0157                     paused: true
0158                 }
0159 
0160                 componentFalse: Image {
0161                     fillMode: Image.PreserveAspectFit
0162                     source: delegate.smallImageUrlIfNeeded
0163                 }
0164             }
0165 
0166             ConditionalLoader {
0167                 id: largeImageLoader
0168 
0169                 anchors.fill: parent
0170                 z: 1
0171 
0172                 active: controlRoot.effectiveLoadLargeImage
0173                 condition: delegate.isProbablyAnimated
0174 
0175                 componentTrue: AnimatedImage {
0176                     fillMode: Image.PreserveAspectFit
0177                     source: delegate.large_image_url
0178 
0179                     playing: true
0180                     paused: true
0181                 }
0182 
0183                 componentFalse: Image {
0184                     fillMode: Image.PreserveAspectFit
0185                     source: delegate.large_image_url
0186                 }
0187             }
0188 
0189             opacity: (!delegate.dim || delegate.isCurrentItem) ? 1 : controlRoot.hovered ? 0.8 : 0.66
0190 
0191             Behavior on opacity {
0192                 NumberAnimation {
0193                     duration: Kirigami.Units.longDuration
0194                     easing.type: Easing.InOutCubic
0195                 }
0196             }
0197 
0198             QQC2.RoundButton {
0199                 id: playPauseButton
0200 
0201                 anchors.centerIn: parent
0202                 z: 100
0203 
0204                 display: T.AbstractButton.IconOnly
0205                 action: QQC2.Action {
0206                     text: {
0207                         const player = delegate.activeAnimatedImage;
0208                         if (!player) {
0209                             return "";
0210                         }
0211                         if (player.paused) {
0212                             return i18nc("@action:button Start playing media", "Play");
0213                         } else {
0214                             return i18nc("@action:button Pause any media that is playing", "Pause");
0215                         }
0216                     }
0217                     icon.name: {
0218                         const player = delegate.activeAnimatedImage;
0219                         if (!player) {
0220                             return "";
0221                         }
0222                         if (player.paused) {
0223                             return mirrored ? "media-playback-start-rtl-symbolic" : "media-playback-start-symbolic";
0224                         } else {
0225                             return mirrored ? "media-playback-pause-rtl-symbolic" : "media-playback-pause-symbolic";
0226                         }
0227                     }
0228                     enabled: delegate.activeAnimatedImage && delegate.isCurrentItem
0229                     shortcut: "Space"
0230                 }
0231                 // other icon properties are not bound automatically because RaoundButton overrides them
0232                 icon.width: Kirigami.Units.iconSizes.large
0233                 icon.height: Kirigami.Units.iconSizes.large
0234                 icon.color: "white"
0235 
0236                 transform: []
0237                 background: Rectangle {
0238                     radius: width
0239 
0240                     border.width: 3
0241                     border.color: "white"
0242                     color: Qt.rgba(0, 0, 0, playPauseButton.down ? 0.5 : 0.3)
0243                 }
0244 
0245                 visible: delegate.isProbablyAnimated && opacity !== 0
0246 
0247                 function show(animated: bool, autohide: bool) {
0248                     autohidePlayPauseButtonTimer.stop();
0249                     hidePlayPauseAnimator.stop();
0250                     if (animated) {
0251                         showPlayPauseAnimator.start();
0252                     } else {
0253                         showPlayPauseAnimator.stop();
0254                         opacity = 1;
0255                     }
0256                     if (autohide) {
0257                         this.autohide();
0258                     }
0259                 }
0260 
0261                 function hide(animated: bool) {
0262                     autohidePlayPauseButtonTimer.stop();
0263                     showPlayPauseAnimator.stop();
0264                     if (animated) {
0265                         hidePlayPauseAnimator.start();
0266                     } else {
0267                         hidePlayPauseAnimator.stop();
0268                         opacity = 0;
0269                     }
0270                 }
0271 
0272                 function autohide() {
0273                     autohidePlayPauseButtonTimer.restart();
0274                 }
0275 
0276                 function play(animated: bool) {
0277                     const player = delegate.activeAnimatedImage;
0278                     if (!player) {
0279                         return;
0280                     }
0281                     player.paused = false;
0282                     if (animated) {
0283                         autohide();
0284                     } else {
0285                         hide(false);
0286                     }
0287                 }
0288 
0289                 function pause(animated: bool) {
0290                     const player = delegate.activeAnimatedImage;
0291                     if (!player) {
0292                         return;
0293                     }
0294                     player.paused = true;
0295                     show(animated, false);
0296                 }
0297 
0298                 function toggle(animated: bool) {
0299                     const player = delegate.activeAnimatedImage;
0300                     if (!player) {
0301                         return;
0302                     }
0303                     if (player.paused) {
0304                         play(animated);
0305                     } else {
0306                         pause(animated);
0307                     }
0308                 }
0309 
0310                 function toggleOrActivate() {
0311                     if (delegate.loadLargeImage && delegate.activeAnimatedImage) {
0312                         toggle(true);
0313                     } else {
0314                         delegate.activated();
0315                     }
0316                 }
0317 
0318                 Timer {
0319                     id: autohidePlayPauseButtonTimer
0320                     interval: Kirigami.Units.humanMoment
0321                     running: false
0322                     onTriggered: {
0323                         playPauseButton.hide(true);
0324                     }
0325                 }
0326 
0327                 OpacityAnimator {
0328                     id: showPlayPauseAnimator
0329                     target: playPauseButton
0330                     to: 1
0331                     running: false
0332                     duration: Kirigami.Units.longDuration
0333                     easing.type: Easing.InOutQuad
0334                 }
0335 
0336                 OpacityAnimator {
0337                     id: hidePlayPauseAnimator
0338                     target: playPauseButton
0339                     to: 0
0340                     running: false
0341                     duration: Kirigami.Units.shortDuration
0342                     easing.type: Easing.InOutQuad
0343                 }
0344 
0345                 onClicked: {
0346                     toggleOrActivate();
0347                 }
0348             }
0349         }
0350 
0351         background: Kirigami.ShadowedRectangle {
0352             id: backgroundShadow
0353 
0354             color: "transparent"
0355             radius: controlRoot.radius
0356 
0357             shadow.size: 20
0358             shadow.xOffset: 0
0359             shadow.yOffset: 5
0360             shadow.color: Qt.rgba(0, 0, 0, 0.4)
0361         }
0362 
0363         onClicked: {
0364             playPauseButton.toggleOrActivate();
0365         }
0366 
0367         MouseArea {
0368             anchors.fill: parent
0369             z: -1
0370             hoverEnabled: !Kirigami.Settings.hasTransientTouchInput
0371             visible: delegate.activeAnimatedImage && delegate.loadLargeImage && delegate.isCurrentItem
0372             onPositionChanged: mouse => {
0373                 playPauseButton.show(/*animated*/true, /*autohide*/true);
0374             }
0375         }
0376     }
0377 
0378     function play() {
0379         if (loadLargeImage) {
0380             playPauseButton.play(/*animated*/true);
0381         }
0382     }
0383 
0384     function __initPlayPause() {
0385         if (activeAnimatedImage) {
0386             playPauseButton.show(/*animated*/false, /*autohide*/false);
0387         } else {
0388             playPauseButton.hide(/*animated*/false);
0389         }
0390     }
0391 
0392     onIsCurrentItemChanged: {
0393         if (!isCurrentItem) {
0394             playPauseButton.pause(true);
0395         }
0396     }
0397 
0398     Component.onCompleted: {
0399         __initPlayPause();
0400     }
0401 }