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

0001 /*
0002  *   SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
0003  *   SPDX-FileCopyrightText: 2022 Nate Graham <nate@kde.org>
0004  *   SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
0005  *
0006  *   SPDX-License-Identifier: LGPL-2.0-or-later
0007  */
0008 
0009 pragma ComponentBehavior: Bound
0010 
0011 import QtQuick
0012 import QtQuick.Controls as QQC2
0013 import QtQuick.Layouts
0014 import org.kde.discover as Discover
0015 import org.kde.discover.app as DiscoverApp
0016 import org.kde.kirigami as Kirigami
0017 import org.kde.purpose as Purpose
0018 
0019 DiscoverPage {
0020     id: appInfo
0021 
0022     title: "" // It would duplicate the text in the header right below it
0023     clip: true
0024 
0025     required property Discover.AbstractResource application
0026 
0027     readonly property int visibleReviews: 3
0028     readonly property int internalSpacings: Kirigami.Units.largeSpacing
0029     readonly property int pageContentMargins: Kirigami.Units.gridUnit
0030     readonly property bool availableFromOnlySingleSource: !originsMenuAction.visible
0031 
0032     // Usually this page is not the top level page, but when we are, isHome being
0033     // true will ensure that the search field suggests we are searching in the list
0034     // of available apps, not inside the app page itself. This will happen when
0035     // Discover is launched e.g. from krunner or otherwise requested to show a
0036     // specific application on launch.
0037     readonly property bool isHome: true
0038 
0039     readonly property bool isOfflineUpgrade: application.packageName === "discover-offline-upgrade"
0040 
0041     ReviewsPage {
0042         id: reviewsSheet
0043         parent: appInfo.QQC2.Overlay.overlay
0044         model: Discover.ReviewsModel {
0045             id: reviewsModel
0046             resource: appInfo.application
0047             preferredSortRole: reviewsSheet.sortRole
0048         }
0049         Component.onCompleted: reviewsSheet.sortRole = reviewsModel.preferredSortRole
0050     }
0051 
0052     actions: [
0053         appbutton.isActive ? appbutton.cancelAction : appbutton.action,
0054         invokeAction,
0055         originsMenuAction
0056     ]
0057 
0058     QQC2.ActionGroup {
0059         id: sourcesGroup
0060         exclusive: true
0061     }
0062 
0063     // Multi-source origin display and switcher
0064     Kirigami.Action {
0065         id: originsMenuAction
0066 
0067         text: i18nc("@item:inlistbox %1 is the name of an app source e.g. \"Flathub\" or \"Ubuntu\"", "From %1", appInfo.application.displayOrigin)
0068         visible: children.length > 1
0069         children: sourcesGroup.actions
0070     }
0071 
0072     Instantiator {
0073         // alternativeResourcesModel
0074         model: Discover.ResourcesProxyModel {
0075             allBackends: true
0076             resourcesUrl: appInfo.application.url
0077         }
0078         delegate: QQC2.Action {
0079             required property var model
0080 
0081             QQC2.ActionGroup.group: sourcesGroup
0082             text: model.availableVersion
0083                 ? i18n("%1 - %2", model.displayOrigin, model.availableVersion)
0084                 : model.displayOrigin
0085             icon.name: model.sourceIcon
0086             checkable: true
0087             checked: appInfo.application === model.application
0088             onTriggered: {
0089                 appInfo.application = model.application
0090             }
0091         }
0092     }
0093 
0094     Kirigami.Action {
0095         id: invokeAction
0096         visible: application.isInstalled && application.canExecute && !appbutton.isActive
0097         text: application.executeLabel
0098         icon.name: "media-playback-start"
0099         onTriggered: application.invokeApplication()
0100     }
0101 
0102     InstallApplicationButton {
0103         id: appbutton
0104         Layout.rightMargin: Kirigami.Units.smallSpacing
0105         application: appInfo.application
0106         visible: false
0107         availableFromOnlySingleSource: appInfo.availableFromOnlySingleSource
0108     }
0109 
0110     Kirigami.ImageColors {
0111         id: appImageColorExtractor
0112         source: appInfo.application.icon
0113     }
0114 
0115     padding: 0
0116     topPadding: undefined
0117     leftPadding: undefined
0118     rightPadding: undefined
0119     bottomPadding: undefined
0120     verticalPadding: undefined
0121     horizontalPadding: undefined
0122 
0123     // Scrollable page content
0124     ColumnLayout {
0125         id: pageLayout
0126 
0127         anchors {
0128             top: parent.top
0129             left: parent.left
0130             right: parent.right
0131         }
0132         spacing: appInfo.internalSpacings
0133 
0134         // Colored header with app icon, name, and metadata
0135         Rectangle {
0136             Layout.fillWidth: true
0137             implicitHeight: headerLayout.implicitHeight + (headerLayout.anchors.topMargin * 2)
0138             color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, appImageColorExtractor.dominant, 0.1)
0139 
0140             GridLayout {
0141                 id: headerLayout
0142 
0143                 readonly property bool stackedMode: appBasicInfoLayout.implicitWidth + columnSpacing + appMetadataLayout.implicitWidth > (pageLayout.width - anchors.leftMargin - anchors.rightMargin)
0144 
0145                 columns: stackedMode ? 1 : 2
0146                 rows: stackedMode ? 2 : 1
0147                 columnSpacing: appInfo.internalSpacings
0148                 rowSpacing: appInfo.internalSpacings
0149 
0150                 anchors {
0151                     top: parent.top
0152                     topMargin: appInfo.internalSpacings
0153                     left: parent.left
0154                     leftMargin: appInfo.internalSpacings
0155                     right: parent.right
0156                     rightMargin: appInfo.internalSpacings
0157                 }
0158 
0159 
0160                 // App icon, name, author, and rating
0161                 RowLayout {
0162                     id: appBasicInfoLayout
0163                     Layout.maximumWidth: headerLayout.implicitWidth
0164                     Layout.alignment: headerLayout.stackedMode ? Qt.AlignHCenter : Qt.AlignLeft
0165                     spacing: appInfo.internalSpacings
0166 
0167                     // App icon
0168                     Kirigami.Icon {
0169                         implicitWidth: Kirigami.Units.iconSizes.huge
0170                         implicitHeight: Kirigami.Units.iconSizes.huge
0171                         source: appInfo.application.icon
0172                     }
0173 
0174                     // App name, author, and rating
0175                     ColumnLayout {
0176 
0177                         spacing: 0
0178 
0179                         // App name
0180                         Kirigami.Heading {
0181                             Layout.fillWidth: true
0182                             text: appInfo.application.name
0183                             type: Kirigami.Heading.Type.Primary
0184                             wrapMode: Text.Wrap
0185                             maximumLineCount: 5
0186                             elide: Text.ElideRight
0187                         }
0188 
0189                         // Author (for apps) or upgrade info (for offline upgrades)
0190                         QQC2.Label {
0191                             id: author
0192 
0193                             Layout.fillWidth: true
0194                             visible: text.length > 0
0195 
0196                             text: {
0197                                 if (appInfo.isOfflineUpgrade) {
0198                                     return appInfo.application.upgradeText.length > 0 ? appInfo.application.upgradeText : "";
0199                                 } else if (appInfo.application.author.length > 0) {
0200                                     return appInfo.application.author;
0201                                 } else {
0202                                     return i18n("Unknown author");
0203                                 }
0204                             }
0205                             wrapMode: Text.Wrap
0206                             maximumLineCount: 5
0207                             elide: Text.ElideRight
0208                         }
0209 
0210                         // Rating
0211                         RowLayout {
0212 
0213                             // Not relevant to the offline upgrade use case
0214                             visible: !appInfo.isOfflineUpgrade
0215 
0216                             Rating {
0217                                 value: appInfo.application.rating ? appInfo.application.rating.sortableRating : 0
0218                                 starSize: author.font.pointSize
0219                                 precision: Rating.Precision.HalfStar
0220                             }
0221 
0222                             QQC2.Label {
0223                                 text: appInfo.application.rating ? i18np("%1 rating", "%1 ratings", appInfo.application.rating.ratingCount) : i18n("No ratings yet")
0224                             }
0225                         }
0226                     }
0227                 }
0228 
0229                 // Metadata
0230                 // Not using Kirigami.FormLayout here because we never want it to move into Mobile
0231                 // mode and we also want to customize the spacing, neither of which it lets us do
0232                 GridLayout {
0233                     id: appMetadataLayout
0234 
0235                     Layout.alignment: headerLayout.stackedMode ? Qt.AlignHCenter : Qt.AlignRight
0236 
0237                     columns: 2
0238                     rows: Math.ceil(appMetadataLayout.visibleChildren.count / 2)
0239                     columnSpacing: Kirigami.Units.smallSpacing
0240                     rowSpacing: 0
0241 
0242                     // Not relevant to offline updates
0243                     visible: !appInfo.isOfflineUpgrade
0244 
0245                     // Version
0246                     QQC2.Label {
0247                         text: i18n("Version:")
0248                         Layout.alignment: Qt.AlignRight
0249                     }
0250                     QQC2.Label {
0251                         text: appInfo.application.versionString
0252                         wrapMode: Text.Wrap
0253                         maximumLineCount: 3
0254                         elide: Text.ElideRight
0255                     }
0256 
0257                     // Size
0258                     QQC2.Label {
0259                         text: i18n("Size:")
0260                         Layout.alignment: Qt.AlignRight
0261                     }
0262                     QQC2.Label {
0263                         text: appInfo.application.sizeDescription
0264                         wrapMode: Text.Wrap
0265                         maximumLineCount: 3
0266                         elide: Text.ElideRight
0267                     }
0268 
0269                     // Licenses
0270                     QQC2.Label {
0271                         text: i18np("License:", "Licenses:", appInfo.application.licenses.length)
0272                         Layout.alignment: Qt.AlignRight
0273                     }
0274                     RowLayout {
0275                         spacing: Kirigami.Units.smallSpacing
0276 
0277                         QQC2.Label {
0278                             visible : appInfo.application.licenses.length === 0
0279                             text: i18nc("The app does not provide any licenses", "Unknown")
0280                             wrapMode: Text.Wrap
0281                             elide: Text.ElideRight
0282                         }
0283 
0284                         Repeater {
0285                             visible: appInfo.application.licenses.length > 0
0286                             model: appInfo.application.licenses.slice(0, 2)
0287                             delegate: RowLayout {
0288                                 id: delegate
0289 
0290                                 required property var modelData
0291 
0292                                 spacing: 0
0293 
0294                                 Kirigami.UrlButton {
0295                                     enabled: url !== ""
0296                                     text: delegate.modelData.name
0297                                     url: delegate.modelData.url
0298                                     horizontalAlignment: Text.AlignHCenter
0299                                     verticalAlignment: Text.AlignTop
0300                                     wrapMode: Text.Wrap
0301                                     maximumLineCount: 3
0302                                     elide: Text.ElideRight
0303                                     color: !delegate.modelData.hasFreedom ? Kirigami.Theme.neutralTextColor : (enabled ? Kirigami.Theme.linkColor : Kirigami.Theme.textColor)
0304                                 }
0305 
0306                                 // Button to open "What's the risk of proprietary software?" sheet
0307                                 QQC2.ToolButton {
0308                                     visible: !delegate.modelData.hasFreedom
0309                                     icon.name: "help-contextual"
0310                                     onClicked: properietarySoftwareRiskExplanationDialog.open();
0311 
0312                                     QQC2.ToolTip {
0313                                         text: i18n("What does this mean?")
0314                                     }
0315                                 }
0316                             }
0317                         }
0318 
0319                         // "See More licenses" link, in case there are a lot of them
0320                         Kirigami.LinkButton {
0321                             visible: application.licenses.length > 3
0322                             text: i18np("See more…", "See more…", appInfo.application.licenses.length)
0323                             horizontalAlignment: Text.AlignHCenter
0324                             verticalAlignment: Text.AlignTop
0325                             elide: Text.ElideRight
0326                             onClicked: allLicensesSheet.open();
0327                         }
0328                     }
0329 
0330                     // Content Rating
0331                     QQC2.Label {
0332                         visible: appInfo.application.contentRatingText.length > 0
0333                         text: i18n("Content Rating:")
0334                         Layout.alignment: Qt.AlignRight
0335                     }
0336                     RowLayout {
0337                         visible: appInfo.application.contentRatingText.length > 0
0338                         spacing: Kirigami.Units.smallSpacing
0339 
0340                         QQC2.Label {
0341                             visible: text.length > 0
0342                             text: appInfo.application.contentRatingMinimumAge === 0 ? "" : i18n("Age: %1+", appInfo.application.contentRatingMinimumAge)
0343                             horizontalAlignment: Text.AlignHCenter
0344                             verticalAlignment: Text.AlignTop
0345                             wrapMode: Text.Wrap
0346                             maximumLineCount: 3
0347                             elide: Text.ElideRight
0348                         }
0349 
0350                         QQC2.Label {
0351                             text: appInfo.application.contentRatingText
0352                             wrapMode: Text.Wrap
0353                             maximumLineCount: 3
0354                             elide: Text.ElideRight
0355 
0356                             readonly property var colors: [ Kirigami.Theme.textColor, Kirigami.Theme.neutralTextColor ]
0357                             color: colors[appInfo.application.contentRatingIntensity]
0358                         }
0359 
0360                         Kirigami.LinkButton {
0361                             visible: appInfo.application.contentRatingDescription.length > 0
0362                             text: i18nc("@action", "See details…")
0363                             elide: Text.ElideRight
0364                             onClicked: contentRatingDialog.open();
0365                         }
0366                     }
0367                 }
0368             }
0369 
0370             Kirigami.Separator {
0371                 width: parent.width
0372                 anchors.top: parent.bottom
0373             }
0374         }
0375 
0376         // Screenshots
0377         Kirigami.PlaceholderMessage {
0378             Layout.fillWidth: true
0379             Layout.leftMargin: appInfo.pageContentMargins
0380             Layout.rightMargin: appInfo.pageContentMargins
0381 
0382             visible: carousel.hasFailed
0383             icon.name: "image-missing"
0384             text: i18nc("@info placeholder message", "Screenshots not available for %1", appInfo.application.name)
0385         }
0386 
0387         CarouselInlineView {
0388             id: carousel
0389 
0390             Layout.fillWidth: true
0391             // This roughly replicates scaling formula for the screenshots
0392             // gallery on FlatHub website, adjusted to scale with gridUnit
0393             Layout.minimumHeight: Math.round((16 + 1/9) * Kirigami.Units.gridUnit)
0394             Layout.maximumHeight: 30 * Kirigami.Units.gridUnit
0395             Layout.preferredHeight: Math.round(width / 2) + Math.round((2 + 7/9) * Kirigami.Units.gridUnit)
0396             Layout.topMargin: appInfo.internalSpacings
0397 
0398             edgeMargin: appInfo.internalSpacings
0399             visible: carouselModel.count > 0 && !hasFailed
0400 
0401             carouselModel: Discover.ScreenshotsModel {
0402                 application: appInfo.application
0403             }
0404         }
0405 
0406         ColumnLayout {
0407             id: topObjectsLayout
0408 
0409             // InlineMessage components are supposed to manage their spacing
0410             // internally. However, at least for now they require some
0411             // assistance from outside to stack them one after another.
0412             spacing: 0
0413 
0414             Layout.fillWidth: true
0415 
0416             // Cancel out parent layout's spacing, making this component effectively zero-sized when empty.
0417             // When non-empty, the very first top margin is provided by this layout, but bottom margins
0418             // are implemented by Loaders that have visible loaded items.
0419             Layout.topMargin: hasVisibleObjects ? 0 : -pageLayout.spacing
0420             Layout.bottomMargin: -pageLayout.spacing
0421 
0422             property bool hasVisibleObjects: false
0423 
0424             function bindVisibility() {
0425                 hasVisibleObjects = Qt.binding(() => {
0426                     for (let i = 0; i < topObjectsRepeater.count; i++) {
0427                         const loader = topObjectsRepeater.itemAt(i);
0428                         const item = loader.item;
0429                         if (item?.visible) {
0430                             return true;
0431                         }
0432                     }
0433                     return false;
0434                 });
0435             }
0436 
0437             Timer {
0438                 id: bindVisibilityTimer
0439 
0440                 running: false
0441                 repeat: false
0442                 interval: 0
0443 
0444                 onTriggered: topObjectsLayout.bindVisibility()
0445             }
0446 
0447             Repeater {
0448                 id: topObjectsRepeater
0449 
0450                 model: appInfo.application.topObjects
0451 
0452                 delegate: Loader {
0453                     required property string modelData
0454 
0455                     Layout.fillWidth: item?.Layout.fillWidth ?? false
0456                     Layout.topMargin: 0
0457                     Layout.leftMargin: appInfo.pageContentMargins
0458                     Layout.rightMargin: appInfo.pageContentMargins
0459                     Layout.bottomMargin: item?.visible ? appInfo.internalSpacings : 0
0460                     Layout.preferredHeight: item?.visible ? item.implicitHeight : 0
0461 
0462                     onModelDataChanged: {
0463                         setSource(modelData, { resource: Qt.binding(() => appInfo.application) });
0464                     }
0465                 }
0466                 onItemAdded: (index, item) => {
0467                     bindVisibilityTimer.start();
0468                 }
0469                 onItemRemoved: (index, item) => {
0470                     bindVisibilityTimer.start();
0471                 }
0472             }
0473         }
0474 
0475         // Layout for textual content; this isn't in the main ColumnLayout
0476         // because we want it to be bounded to a maximum width
0477         ColumnLayout {
0478             id: textualContentLayout
0479 
0480             Layout.fillWidth: true
0481             Layout.margins: appInfo.pageContentMargins
0482             Layout.alignment: Qt.AlignHCenter
0483 
0484             spacing: appInfo.internalSpacings
0485 
0486             // Short description
0487             // Not using Kirigami.Heading here because that component doesn't
0488             // support selectable text, and we want this to be selectable because
0489             // it's also used to show the path for local packages, and that makes
0490             // sense to be selectable
0491             Kirigami.SelectableLabel {
0492                 Layout.fillWidth: true
0493                 // Not relevant to the offline upgrade use case because we
0494                 // display the info in the header instead
0495                 visible: !appInfo.isOfflineUpgrade
0496                 text: appInfo.application.comment
0497                 wrapMode: Text.Wrap
0498 
0499                 // Match `level: 1` in Kirigami.Heading
0500                 font.pointSize: Kirigami.Theme.defaultFont.pointSize * 1.35
0501                 font.weight: Font.DemiBold
0502 
0503                 Accessible.role: Accessible.Heading
0504             }
0505 
0506             // Long app description
0507             Kirigami.SelectableLabel {
0508                 objectName: "applicationDescription" // for appium tests
0509                 Layout.fillWidth: true
0510                 wrapMode: Text.WordWrap
0511                 text: appInfo.application.longDescription
0512                 textFormat: TextEdit.RichText
0513                 onLinkActivated: link => Qt.openUrlExternally(link);
0514             }
0515 
0516             // External resources
0517             GridLayout {
0518                 id: externalResourcesLayout
0519                 readonly property int visibleButtons: (helpButton.visible ? 1 : 0)
0520                                                     + (homepageButton.visible ? 1: 0)
0521                                                     + (addonsButton.visible ? 1 : 0)
0522                                                     + (shareButton.visible ? 1 : 0)
0523                 readonly property int buttonWidth: Math.round(textualContentLayout.width / columns)
0524                 readonly property int tallestButtonHeight: Math.max(helpButton.implicitHeight,
0525                                                                     homepageButton.implicitHeight,
0526                                                                     shareButton.implicitHeight,
0527                                                                     addonsButton.implicitHeight)
0528                 readonly property int minWidth: Math.max(helpButton.visible ? helpButton.implicitMinWidth : 0,
0529                                                                   homepageButton.visible ? homepageButton.implicitMinWidth: 0,
0530                                                                   addonsButton.visible ? addonsButton.implicitMinWidth : 0,
0531                                                                   shareButton.visible ? shareButton.implicitMinWidth : 0)
0532                 readonly property bool stackedlayout: minWidth > Math.round(textualContentLayout.width / visibleButtons) -
0533                                                         (columnSpacing * (visibleButtons + 1))
0534 
0535                 Layout.fillWidth: true
0536                 Layout.bottomMargin: appInfo.internalSpacings * 2
0537 
0538 
0539                 rows: stackedlayout ? visibleButtons : 1
0540                 columns: stackedlayout ? 1: visibleButtons
0541                 rowSpacing: Kirigami.Units.smallSpacing
0542                 columnSpacing: Kirigami.Units.smallSpacing
0543 
0544                 visible: visibleButtons > 0
0545 
0546                 ApplicationResourceButton {
0547                     id: helpButton
0548 
0549                     Layout.fillWidth: true
0550                     Layout.maximumWidth: externalResourcesLayout.buttonWidth
0551                     Layout.minimumHeight: externalResourcesLayout.tallestButtonHeight
0552 
0553                     visible: application.helpURL.toString() !== ""
0554 
0555                     buttonIcon: "documentation"
0556                     title: i18n("Documentation")
0557                     subtitle: i18n("Read the project's official documentation")
0558                     tooltipText: application.helpURL.toString()
0559 
0560                     onClicked: Qt.openUrlExternally(application.helpURL);
0561                 }
0562 
0563                 ApplicationResourceButton {
0564                     id: homepageButton
0565 
0566                     Layout.fillWidth: true
0567                     Layout.maximumWidth: externalResourcesLayout.buttonWidth
0568                     Layout.minimumHeight: externalResourcesLayout.tallestButtonHeight
0569 
0570                     visible: application.homepage.toString() !== ""
0571 
0572                     buttonIcon: "internet-services"
0573                     title: i18n("Website")
0574                     subtitle: i18n("Visit the project's website")
0575                     tooltipText: application.homepage.toString()
0576 
0577                     onClicked: Qt.openUrlExternally(application.homepage);
0578                 }
0579 
0580                 ApplicationResourceButton {
0581                     id: addonsButton
0582 
0583                     Layout.fillWidth: true
0584                     Layout.maximumWidth: externalResourcesLayout.buttonWidth
0585                     Layout.minimumHeight: externalResourcesLayout.tallestButtonHeight
0586 
0587                     visible: addonsView.containsAddons
0588 
0589                     buttonIcon: "extension-symbolic"
0590                     title: i18n("Addons")
0591                     subtitle: i18n("Install or remove additional functionality")
0592 
0593                     onClicked: {
0594                         if (addonsView.addonsCount === 0) {
0595                             Navigation.openExtends(application.appstreamId, appInfo.application.name)
0596                         } else {
0597                             addonsView.visible = true
0598                         }
0599                     }
0600                 }
0601 
0602                 ApplicationResourceButton {
0603                     id: shareButton
0604 
0605                     Layout.fillWidth: true
0606                     Layout.maximumWidth: externalResourcesLayout.buttonWidth
0607                     Layout.minimumHeight: externalResourcesLayout.tallestButtonHeight
0608 
0609                     buttonIcon: "document-share"
0610                     title: i18nc("Exports the application's URL to an external service", "Share")
0611                     subtitle: i18n("Send a link for this application")
0612                     tooltipText: application.url.toString()
0613                     visible: tooltipText.length > 0 && !appInfo.isOfflineUpgrade
0614 
0615                     Kirigami.PromptDialog {
0616                         id: shareSheet
0617                         parent: appInfo.QQC2.Overlay.overlay
0618                         title: shareButton.title
0619                         standardButtons: QQC2.Dialog.NoButton
0620 
0621                         Purpose.AlternativesView {
0622                             id: alts
0623                             implicitWidth: Kirigami.Units.gridUnit
0624                             pluginType: "ShareUrl"
0625                             inputData: {
0626                                 "urls": [ application.url.toString() ],
0627                                 "title": i18nc("The subject line for an email. %1 is the name of an application", "Check out the %1 app!", application.name)
0628                             }
0629                             onFinished: (/*var*/ output, /*int*/ error, /*string*/ message) => {
0630                                 shareSheet.close()
0631                                 if (error !== 0) {
0632                                     console.error("job finished with error", error, message)
0633                                 }
0634                                 alts.reset()
0635                             }
0636                         }
0637                     }
0638 
0639                     onClicked: {
0640                         shareSheet.open();
0641                     }
0642                 }
0643             }
0644 
0645             Kirigami.Heading {
0646                 visible: changelogLabel.visible
0647                 text: i18n("What's New")
0648                 level: 2
0649                 type: Kirigami.Heading.Type.Primary
0650                 wrapMode: Text.Wrap
0651             }
0652 
0653             // Changelog text
0654             QQC2.Label {
0655                 id: changelogLabel
0656 
0657                 Layout.fillWidth: true
0658                 Layout.bottomMargin: appInfo.internalSpacings * 2
0659 
0660                 // Some backends are known to produce empty line break as a text
0661                 visible: text !== "" && text !== "<br />"
0662                 wrapMode: Text.WordWrap
0663 
0664                 Component.onCompleted: appInfo.application.fetchChangelog()
0665                 Connections {
0666                     target: appInfo.application
0667                     function onChangelogFetched(changelog) {
0668                         changelogLabel.text = changelog
0669                     }
0670                 }
0671             }
0672 
0673 
0674             Kirigami.Heading {
0675                 Layout.fillWidth: true
0676                 visible: reviewsSheet.sortModel.count > 0 && !reviewsLoadingPlaceholder.visible && !reviewsError.visible
0677                 text: i18n("Reviews")
0678                 level: 2
0679                 type: Kirigami.Heading.Type.Primary
0680                 wrapMode: Text.Wrap
0681             }
0682 
0683             Kirigami.LoadingPlaceholder {
0684                 id: reviewsLoadingPlaceholder
0685                 Layout.alignment: Qt.AlignHCenter
0686                 Layout.maximumWidth: Kirigami.Units.gridUnit * 15
0687                 Layout.bottomMargin: appInfo.internalSpacings * 2
0688                 visible: reviewsModel.fetching
0689                 text: i18n("Loading reviews for %1", appInfo.application.name)
0690             }
0691 
0692             Kirigami.PlaceholderMessage {
0693                 id: reviewsError
0694                 Layout.fillWidth: true
0695                 visible: reviewsModel.backend && reviewsModel.backend.errorMessage.length > 0 && text.length > 0 && reviewsModel.count === 0 && !reviewsLoadingPlaceholder.visible
0696                 icon.name: "text-unflow"
0697                 text: i18nc("@info placeholder message", "Reviews for %1 are temporarily unavailable", appInfo.application.name)
0698                 explanation: reviewsModel.backend ? reviewsModel.backend.errorMessage : ""
0699             }
0700 
0701             ReviewsStats {
0702                 visible: reviewsModel.count > 3
0703                 Layout.fillWidth: true
0704                 application: appInfo.application
0705                 reviewsModel: reviewsModel
0706                 sortModel: reviewsSheet.sortModel
0707                 visibleReviews: appInfo.visibleReviews
0708                 compact: appInfo.compact
0709             }
0710 
0711             // Review-related buttons
0712             Flow {
0713                 Layout.fillWidth: true
0714                 Layout.bottomMargin: appInfo.internalSpacings * 2
0715 
0716                 spacing: appInfo.internalSpacings
0717 
0718                 QQC2.Button {
0719                     visible: reviewsModel.count > visibleReviews
0720 
0721                     text: i18nc("@action:button", "Show All Reviews")
0722                     icon.name: "view-visible"
0723 
0724                     onClicked: {
0725                         reviewsSheet.open()
0726                     }
0727                 }
0728 
0729                 QQC2.Button {
0730                     visible: appbutton.isStateAvailable && reviewsModel.backend && !reviewsError.visible && reviewsModel.backend.isResourceSupported(appInfo.application)
0731                     enabled: appInfo.application.isInstalled
0732 
0733                     text: appInfo.application.isInstalled ? i18n("Write a Review") : i18n("Install to Write a Review")
0734                     icon.name: "document-edit"
0735 
0736                     onClicked: {
0737                         reviewsSheet.openReviewDialog()
0738                     }
0739                 }
0740             }
0741 
0742             // "Get Involved" section
0743             Kirigami.Heading {
0744                 visible: getInvolvedLayout.visible
0745                 text: i18n("Get Involved")
0746                 level: 2
0747                 type: Kirigami.Heading.Type.Primary
0748                 wrapMode: Text.Wrap
0749             }
0750 
0751             GridLayout {
0752                 id: getInvolvedLayout
0753 
0754                 readonly property int visibleButtons: (donateButton.visible ? 1 : 0)
0755                                                     + (bugButton.visible ? 1 : 0)
0756                                                     + (contributeButton.visible ? 1 : 0)
0757                 readonly property int buttonWidth: Math.round(textualContentLayout.width / columns)
0758                 readonly property int tallestButtonHeight: Math.max(donateButton.implicitHeight,
0759                                                                     bugButton.implicitHeight,
0760                                                                     contributeButton.implicitHeight)
0761                 readonly property int minWidth: Math.max(donateButton.visible ? donateButton.implicitMinWidth : 0,
0762                                                           bugButton.visible ? bugButton.implicitMinWidth: 0,
0763                                                           contributeButton.visible ? contributeButton.implicitMinWidth : 0)
0764                 readonly property bool stackedlayout: minWidth > Math.round(textualContentLayout.width / visibleButtons) -
0765                                                         (columnSpacing * (visibleButtons + 1))
0766 
0767                 Layout.fillWidth: true
0768                 Layout.bottomMargin: appInfo.internalSpacings * 2
0769 
0770                 rows: stackedlayout ? visibleButtons : 1
0771                 columns: stackedlayout ? 1: visibleButtons
0772                 rowSpacing: Kirigami.Units.smallSpacing
0773                 columnSpacing: Kirigami.Units.smallSpacing
0774 
0775                 visible: visibleButtons > 0
0776 
0777                 ApplicationResourceButton {
0778                     id: donateButton
0779 
0780                     Layout.fillWidth: true
0781                     Layout.maximumWidth: getInvolvedLayout.buttonWidth
0782                     Layout.minimumHeight: getInvolvedLayout.tallestButtonHeight
0783 
0784                     visible: application.donationURL.toString() !== ""
0785 
0786                     buttonIcon: "help-donate"
0787                     title: i18n("Donate")
0788                     subtitle: i18n("Support and thank the developers by donating to their project")
0789                     tooltipText: application.donationURL.toString()
0790 
0791                     onClicked: Qt.openUrlExternally(application.donationURL);
0792                 }
0793 
0794                 ApplicationResourceButton {
0795                     id: bugButton
0796 
0797                     Layout.fillWidth: true
0798                     Layout.maximumWidth: getInvolvedLayout.buttonWidth
0799                     Layout.minimumHeight: getInvolvedLayout.tallestButtonHeight
0800 
0801                     visible: application.bugURL.toString() !== ""
0802 
0803                     buttonIcon: "tools-report-bug"
0804                     title: i18n("Report Bug")
0805                     subtitle: i18n("Log an issue you found to help get it fixed")
0806                     tooltipText: application.bugURL.toString()
0807 
0808                     onClicked: Qt.openUrlExternally(application.bugURL);
0809                 }
0810 
0811                 ApplicationResourceButton {
0812                     id: contributeButton
0813 
0814                     Layout.fillWidth: true
0815                     Layout.maximumWidth: getInvolvedLayout.buttonWidth
0816                     Layout.minimumHeight: getInvolvedLayout.tallestButtonHeight
0817 
0818                     visible: application.contributeURL.toString() !== ""
0819 
0820                     buttonIcon: "project-development"
0821                     title: i18n("Contribute")
0822                     subtitle: i18n("Help the developers by coding, designing, testing, or translating")
0823                     tooltipText: application.contributeURL.toString()
0824 
0825                     onClicked: Qt.openUrlExternally(application.contributeURL);
0826                 }
0827             }
0828 
0829             Repeater {
0830                 model: appInfo.application.bottomObjects
0831 
0832                 delegate: Loader {
0833                     required property string modelData
0834 
0835                     Layout.fillWidth: true
0836 
0837                     onModelDataChanged: {
0838                         setSource(modelData, { resource: Qt.binding(() => appInfo.application) });
0839                     }
0840                 }
0841             }
0842         }
0843     }
0844 
0845     AddonsView {
0846         id: addonsView
0847 
0848         application: appInfo.application
0849         parent: appInfo.QQC2.Overlay.overlay
0850     }
0851 
0852     Kirigami.Dialog {
0853         id: allLicensesSheet
0854         title: i18n("All Licenses")
0855         standardButtons: Kirigami.Dialog.NoButton
0856         preferredWidth: Kirigami.Units.gridUnit * 16
0857         maximumHeight: Kirigami.Units.gridUnit * 20
0858 
0859         ColumnLayout {
0860             spacing: 0
0861 
0862             Repeater {
0863                 model: appInfo.application.licenses
0864 
0865                 delegate: QQC2.ItemDelegate {
0866                     id: delegate
0867 
0868                     required property var modelData
0869 
0870                     contentItem: Kirigami.UrlButton {
0871                         enabled: url !== ""
0872                         text: delegate.modelData.name
0873                         url: delegate.modelData.url
0874                         horizontalAlignment: Text.AlignLeft
0875                         color: !delegate.modelData.hasFreedom
0876                             ? Kirigami.Theme.neutralTextColor
0877                             : (enabled ? Kirigami.Theme.linkColor : Kirigami.Theme.textColor)
0878                     }
0879                 }
0880             }
0881         }
0882     }
0883 
0884     Kirigami.PromptDialog {
0885         id: contentRatingDialog
0886         parent: appInfo.QQC2.Overlay.overlay
0887         title: i18n("Content Rating")
0888         preferredWidth: Kirigami.Units.gridUnit * 25
0889         standardButtons: Kirigami.Dialog.NoButton
0890 
0891         QQC2.Label {
0892             text: appInfo.application.contentRatingDescription
0893             textFormat: Text.MarkdownText
0894             wrapMode: Text.Wrap
0895         }
0896     }
0897 
0898     Kirigami.PromptDialog {
0899         id: properietarySoftwareRiskExplanationDialog
0900         parent: appInfo.QQC2.Overlay.overlay
0901         preferredWidth: Kirigami.Units.gridUnit * 25
0902         standardButtons: Kirigami.Dialog.NoButton
0903 
0904         title: i18n("Risks of proprietary software")
0905 
0906         TextEdit {
0907             readonly property string proprietarySoftwareExplanationPage: "https://www.gnu.org/proprietary"
0908 
0909             text: homepageButton.visible
0910                 ? xi18nc("@info", "This application's source code is partially or entirely closed to public inspection and improvement. That means third parties and users like you cannot verify its operation, security, and trustworthiness, or modify and redistribute it without the authors' express permission.<nl/><nl/>The application may be perfectly safe to use, or it may be acting against you in various ways—such as harvesting your personal information, tracking your location, or transmitting the contents of your files to someone else. There is no easy way to be sure, so you should only install this application if you fully trust its authors (<link url='%1'>%2</link>).<nl/><nl/>You can learn more at <link url='%3'>%3</link>.", application.homepage, author.text, proprietarySoftwareExplanationPage)
0911                 : xi18nc("@info", "This application's source code is partially or entirely closed to public inspection and improvement. That means third parties and users like you cannot verify its operation, security, and trustworthiness, or modify and redistribute it without the authors' express permission.<nl/><nl/>The application may be perfectly safe to use, or it may be acting against you in various ways—such as harvesting your personal information, tracking your location, or transmitting the contents of your files to someone else. There is no easy way to be sure, so you should only install this application if you fully trust its authors (%1).<nl/><nl/>You can learn more at <link url='%2'>%2</link>.", author.text, proprietarySoftwareExplanationPage)
0912             wrapMode: Text.Wrap
0913             textFormat: TextEdit.RichText
0914             readOnly: true
0915 
0916             color: Kirigami.Theme.textColor
0917             selectedTextColor: Kirigami.Theme.highlightedTextColor
0918             selectionColor: Kirigami.Theme.highlightColor
0919 
0920             onLinkActivated: url => Qt.openUrlExternally(url)
0921 
0922             HoverHandler {
0923                 acceptedButtons: Qt.NoButton
0924                 cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
0925             }
0926         }
0927     }
0928 }