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 }