Warning, /multimedia/elisa/src/qml/HeaderBar.qml is written in an unsupported language. File is not indexed.
0001 /* 0002 SPDX-FileCopyrightText: 2016 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr> 0003 SPDX-FileCopyrightText: 2023 (c) Fushan Wen <qydwhotmail@gmail.com> 0004 0005 SPDX-License-Identifier: LGPL-3.0-or-later 0006 */ 0007 0008 import QtQuick 2.7 0009 import QtQuick.Layouts 1.2 0010 import QtQuick.Controls 2.15 0011 import QtQuick.Window 2.2 0012 import Qt5Compat.GraphicalEffects 0013 import org.kde.kirigami 2.5 as Kirigami 0014 import org.kde.elisa 1.0 0015 0016 FocusScope { 0017 id: headerBar 0018 0019 property string title 0020 property string artist 0021 property string albumArtist 0022 property string album 0023 property string image 0024 property int trackRating 0025 property int albumID 0026 property bool ratingVisible 0027 property alias playerControl: playControlItem 0028 property alias isMaximized: playControlItem.isMaximized 0029 property int imageSourceSize: 512 0030 0031 property bool portrait: (contentZone.height/contentZone.width) > 0.7 0032 property bool transitionsEnabled: true 0033 0034 property double smallerDimension: Math.min(contentZone.height, contentZone.width) - 4 * Kirigami.Units.largeSpacing 0035 0036 property int handlePosition: implicitHeight 0037 0038 signal openArtist() 0039 signal openAlbum() 0040 signal openNowPlaying() 0041 0042 onImageChanged: { 0043 if (changeBackgroundTransition.running) { 0044 changeBackgroundTransition.complete() 0045 } 0046 0047 loadImage(); 0048 0049 if (transitionsEnabled) { 0050 changeBackgroundTransition.start() 0051 } 0052 } 0053 0054 function loadImage() { 0055 if (background.pendingImageIncubator) { 0056 background.pendingImageIncubator.forceCompletion(); 0057 background.pendingImageIncubator.object.statusChanged.disconnect(replaceWhenLoaded); 0058 background.pendingImageIncubator.object.destroy(); 0059 background.pendingImageIncubator = undefined; 0060 } 0061 0062 if (images.pendingImageIncubator) { 0063 images.pendingImageIncubator.forceCompletion(); 0064 images.pendingImageIncubator.object.statusChanged.disconnect(replaceIconWhenLoaded); 0065 images.pendingImageIncubator.object.destroy(); 0066 images.pendingImageIncubator = undefined; 0067 } 0068 0069 background.doesSkipAnimation = background.currentItem == undefined || !headerBar.transitionsEnabled; 0070 background.pendingImageIncubator = backgroundComponent.incubateObject(background, { 0071 "source": image, 0072 "opacity": 0, 0073 }); 0074 images.pendingImageIncubator = mainIconComponent.incubateObject(images, { 0075 "source": image, 0076 "opacity": 0, 0077 }); 0078 } 0079 0080 function replaceWhenLoaded() { 0081 background.pendingImageIncubator.object.statusChanged.disconnect(replaceWhenLoaded); 0082 background.replace(background.pendingImageIncubator.object, {}, StackView.Transition); 0083 background.pendingImageIncubator = undefined; 0084 } 0085 0086 function replaceIconWhenLoaded() { 0087 images.pendingImageIncubator.object.statusChanged.disconnect(replaceIconWhenLoaded); 0088 images.replace(images.pendingImageIncubator.object, {}, StackView.Transition); 0089 images.pendingImageIncubator = undefined; 0090 } 0091 0092 StackView { 0093 id: background 0094 0095 anchors.fill: parent 0096 visible: headerBar.height > playControlItem.height 0097 0098 property var pendingImageIncubator 0099 property bool doesSkipAnimation: true 0100 0101 replaceEnter: Transition { 0102 OpacityAnimator { 0103 id: replaceEnterOpacityAnimator 0104 from: 0 0105 to: 1 0106 // 1 is HACK for https://bugreports.qt.io/browse/QTBUG-106797 to avoid flickering 0107 duration: background.doesSkipAnimation ? 1 : Kirigami.Units.longDuration 0108 } 0109 } 0110 // Keep the old image around till the new one is fully faded in 0111 // If we fade both at the same time you can see the background behind glimpse through 0112 replaceExit: Transition { 0113 PauseAnimation { 0114 duration: replaceEnterOpacityAnimator.duration 0115 } 0116 } 0117 0118 layer.enabled: true 0119 layer.effect: HueSaturation { 0120 cached: true 0121 0122 lightness: -0.5 0123 saturation: 0.9 0124 0125 layer.enabled: true 0126 layer.effect: FastBlur { 0127 cached: true 0128 radius: 64 0129 transparentBorder: false 0130 } 0131 } 0132 } 0133 0134 Component { 0135 id: backgroundComponent 0136 0137 ImageWithFallback { 0138 fallback: Qt.resolvedUrl(elisaTheme.defaultBackgroundImage) 0139 asynchronous: true 0140 0141 // make the FastBlur effect more strong 0142 sourceSize.height: 10 0143 // Switch to Stretch if the effective height of the image to blur would be 0 with PreserveAspectCrop 0144 // We need to know the aspect ratio, which we compute from oldMainIcon.painted*, because: 0145 // - QML does not currently provide a direct way to get original source dimensions 0146 // - painted* of this image won't get us the right value when fillMode is set to Stretch 0147 // - oldMainIcon uses the same source and is set to preserve aspect ratio 0148 fillMode: width / height < (images.currentItem ? images.currentItem.paintedWidth / images.currentItem.paintedHeight * sourceSize.height : 1) 0149 ? Image.PreserveAspectCrop : Image.Stretch 0150 0151 StackView.onRemoved: { 0152 destroy(); 0153 } 0154 0155 Component.onCompleted: { 0156 if (status === Image.Loading) { 0157 statusChanged.connect(headerBar.replaceWhenLoaded); 0158 } else { 0159 headerBar.replaceWhenLoaded(); 0160 } 0161 } 0162 } 0163 } 0164 // Not a flat button because we need a background to ensure adequate contrast 0165 // against the HeaderBar's album art background 0166 ButtonWithToolTip { 0167 anchors.top: parent.top 0168 anchors.right: parent.right 0169 0170 visible: mainWindow.visibility == Window.FullScreen 0171 0172 text: i18nc("@action:button", "Exit Full Screen") 0173 icon.name: "view-restore" 0174 0175 onClicked: mainWindow.restorePreviousStateBeforeFullScreen(); 0176 } 0177 0178 MediaPlayerControl { 0179 id: playControlItem 0180 0181 focus: true 0182 z: 1 0183 0184 anchors.left: background.left 0185 anchors.right: background.right 0186 anchors.bottom: background.bottom 0187 0188 height: elisaTheme.mediaPlayerControlHeight 0189 isTranslucent: headerBar.height > elisaTheme.mediaPlayerControlHeight 0190 isNearCollapse: headerBar.height < elisaTheme.mediaPlayerControlHeight * 2 0191 0192 onHandlePositionChanged: (y, offset) => { 0193 const newHeight = headerBar.height - offset + y 0194 handlePosition = Math.max(newHeight, 0) 0195 } 0196 } 0197 0198 ColumnLayout { 0199 id: contentZone 0200 0201 anchors.top: parent.top 0202 anchors.left: parent.left 0203 anchors.right: parent.right 0204 anchors.bottom: playControlItem.top 0205 0206 spacing: 0 0207 0208 // Hardcoded because the headerbar blur always makes a dark-ish 0209 // background, so we don't want to use a color scheme color that 0210 // might also be dark. This is the text color of Breeze 0211 Kirigami.Theme.textColor: "#eff0f1" 0212 0213 0214 GridLayout { 0215 id: gridLayoutContent 0216 visible: contentZone.height > mainLabel.height 0217 0218 columns: portrait? 1: 2 0219 0220 columnSpacing: Kirigami.Units.largeSpacing * (isMaximized ? 4 : 1) 0221 rowSpacing: Kirigami.Units.largeSpacing 0222 0223 Layout.alignment: Qt.AlignVCenter 0224 Layout.fillWidth: true 0225 Layout.fillHeight: true 0226 Layout.rightMargin: (LayoutMirroring.enabled && !portrait && !isMaximized)? contentZone.width * 0.15: 4 * Kirigami.Units.largeSpacing 0227 Layout.leftMargin: (!LayoutMirroring.enabled && !portrait && !isMaximized)? contentZone.width * 0.15: 4 * Kirigami.Units.largeSpacing 0228 Layout.topMargin: isMaximized? 4 * Kirigami.Units.largeSpacing : 0 0229 Layout.bottomMargin: isMaximized? 4 * Kirigami.Units.largeSpacing : 0 0230 Layout.maximumWidth: contentZone.width - 2 * ((!portrait && !isMaximized) ? contentZone.width * 0.15 : 4 * Kirigami.Units.largeSpacing) 0231 0232 Behavior on Layout.topMargin { 0233 enabled: transitionsEnabled 0234 NumberAnimation { 0235 easing.type: Easing.InOutQuad 0236 duration: Kirigami.Units.shortDuration 0237 } 0238 } 0239 0240 Behavior on Layout.leftMargin { 0241 enabled: transitionsEnabled 0242 NumberAnimation { 0243 easing.type: Easing.InOutQuad 0244 duration: Kirigami.Units.shortDuration 0245 } 0246 } 0247 0248 StackView { 0249 id: images 0250 Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter 0251 property double imageSize: Math.min(smallerDimension * 0.9, portrait ? gridLayoutContent.height/3 : gridLayoutContent.width/2) 0252 property var pendingImageIncubator 0253 0254 Layout.preferredHeight: imageSize 0255 Layout.preferredWidth: imageSize 0256 Layout.minimumHeight: mainLabel.height - Kirigami.Units.smallSpacing 0257 Layout.minimumWidth: mainLabel.height - Kirigami.Units.smallSpacing 0258 0259 replaceEnter: Transition { 0260 OpacityAnimator { 0261 from: 0 0262 to: 1 0263 duration: replaceEnterOpacityAnimator.duration 0264 } 0265 } 0266 // Keep the old image around till the new one is fully faded in 0267 // If we fade both at the same time you can see the background behind glimpse through 0268 replaceExit: Transition { 0269 PauseAnimation { 0270 duration: replaceEnterOpacityAnimator.duration 0271 } 0272 } 0273 } 0274 0275 Component { 0276 id: mainIconComponent 0277 0278 ImageWithFallback { 0279 asynchronous: true 0280 mipmap: true 0281 0282 fallback: Qt.resolvedUrl(elisaTheme.defaultAlbumImage) 0283 0284 sourceSize { 0285 width: imageSourceSize * Screen.devicePixelRatio 0286 height: imageSourceSize * Screen.devicePixelRatio 0287 } 0288 0289 fillMode: Image.PreserveAspectFit 0290 0291 StackView.onRemoved: { 0292 destroy(); 0293 } 0294 0295 Component.onCompleted: { 0296 if (status === Image.Loading) { 0297 statusChanged.connect(headerBar.replaceIconWhenLoaded); 0298 } else { 0299 headerBar.replaceIconWhenLoaded(); 0300 } 0301 } 0302 } 0303 } 0304 0305 ColumnLayout { 0306 // fillHeight needs to adapt to playlist height when isMaximized && !portrait 0307 Layout.fillHeight: trackInfoGrid.height + playLoader.implicitHeight > images.height 0308 Layout.alignment: Qt.AlignTop 0309 Grid { 0310 // Part of HeaderBar that shows track information. Depending on it's size, that information is 0311 // shown in a Column, Row or completely hidden (visibility handled in parent). 0312 id: trackInfoGrid 0313 flow: Grid.TopToBottom 0314 rows: 4 0315 columns: 1 0316 verticalItemAlignment: Grid.AlignVCenter 0317 horizontalItemAlignment: portrait && isMaximized ? Grid.AlignHCenter : Grid.AlignLeft 0318 0319 rowSpacing: Kirigami.Units.largeSpacing 0320 columnSpacing: Kirigami.Units.largeSpacing * 6 0321 Layout.alignment: (portrait && isMaximized ? Qt.AlignHCenter: Qt.AlignLeft) | Qt.AlignTop 0322 0323 Layout.fillWidth: !(portrait && isMaximized) 0324 Layout.fillHeight: isMaximized 0325 Layout.maximumHeight: { 0326 var h = headerBar.height - playControlItem.height - 8 * Kirigami.Units.largeSpacing 0327 if (h < gridLayoutContent.height) 0328 return h 0329 return gridLayoutContent.height + 8 0330 } 0331 0332 states: State { 0333 name: "leftToRight" 0334 when: contentZone.height < (mainLabel.height * 3 + Kirigami.Units.largeSpacing * 4 0335 + (ratingVisible ? mainRating.height + Kirigami.Units.largeSpacing : 0) ) 0336 PropertyChanges { 0337 target: trackInfoGrid 0338 flow: Grid.LeftToRight 0339 Layout.alignment: (portrait? Qt.AlignHCenter: Qt.AlignLeft) | Qt.AlignVCenter 0340 Layout.maximumHeight: gridLayoutContent.height 0341 rows: 1 0342 columns: 4 0343 } 0344 } 0345 0346 move: Transition { 0347 NumberAnimation { 0348 // if Maximized, this would happen after change from and to portrait-layout, which we don't want 0349 properties: isMaximized ? "" : "x,y" 0350 easing.type: Easing.InOutCubic 0351 duration: Kirigami.Units.longDuration 0352 } 0353 } 0354 0355 LabelWithToolTip { 0356 id: mainLabel 0357 text: title 0358 Layout.alignment: (portrait? Qt.AlignHCenter: Qt.AlignLeft) | Qt.AlignVCenter 0359 Layout.fillWidth: true 0360 horizontalAlignment: portrait? Text.AlignHCenter : Text.AlignLeft 0361 level: 1 0362 font.bold: true 0363 0364 MouseArea { 0365 id: titleMouseArea 0366 width: Math.min(parent.implicitWidth, parent.width) 0367 height: parent.height 0368 cursorShape: Qt.PointingHandCursor 0369 onClicked: { 0370 openNowPlaying() 0371 } 0372 } 0373 } 0374 0375 LabelWithToolTip { 0376 id: authorLabel 0377 text: artist 0378 Layout.alignment: portrait? Qt.AlignHCenter: Qt.AlignLeft | Qt.AlignVCenter 0379 Layout.fillWidth: false 0380 horizontalAlignment: portrait? Text.AlignHCenter : Text.AlignLeft 0381 0382 level: 3 0383 0384 MouseArea { 0385 id: authorMouseArea 0386 width: Math.min(parent.implicitWidth, parent.width) 0387 height: parent.height 0388 cursorShape: Qt.PointingHandCursor 0389 onClicked: { 0390 openArtist() 0391 } 0392 } 0393 } 0394 0395 LabelWithToolTip { 0396 id: albumLabel 0397 text: album 0398 Layout.alignment: (portrait? Qt.AlignHCenter: Qt.AlignLeft) | Qt.AlignVCenter 0399 Layout.fillWidth: true 0400 horizontalAlignment: portrait? Text.AlignHCenter : Text.AlignLeft 0401 0402 level: 3 0403 0404 MouseArea { 0405 id: albumMouseArea 0406 width: Math.min(parent.implicitWidth, parent.width) 0407 height: parent.height 0408 cursorShape: Qt.PointingHandCursor 0409 onClicked: { 0410 openAlbum() 0411 } 0412 } 0413 } 0414 0415 RatingStar { 0416 id: mainRating 0417 visible: ratingVisible 0418 starRating: trackRating 0419 Layout.fillWidth: true 0420 Layout.alignment: (portrait? Qt.AlignHCenter: Qt.AlignLeft) | Qt.AlignVCenter 0421 } 0422 } 0423 Loader { 0424 id: playLoader 0425 active: headerBar.isMaximized 0426 asynchronous: true 0427 visible: headerBar.isMaximized 0428 0429 Layout.fillWidth: true 0430 Layout.fillHeight: true 0431 Layout.alignment: Qt.AlignRight | Qt.AlignTop 0432 Layout.topMargin: Kirigami.Units.largeSpacing 0433 0434 sourceComponent: SimplePlayListView { 0435 model: ElisaApplication.mediaPlayListProxyModel 0436 } 0437 } 0438 } 0439 } 0440 } 0441 0442 SequentialAnimation { 0443 id: changeBackgroundTransition 0444 0445 ParallelAnimation { 0446 NumberAnimation { 0447 targets: [mainLabel, authorLabel, albumLabel] 0448 property: 'opacity' 0449 from: 0 0450 to: 1 0451 duration: Kirigami.Units.longDuration 0452 easing.type: Easing.Linear 0453 } 0454 } 0455 } 0456 0457 Component.onCompleted: { 0458 loadImage(); 0459 } 0460 }