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 }