1/* 2 * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez <aleixpol@blue-systems.com> 3 * 4 * SPDX-License-Identifier: LGPL-2.0-or-later 5 */ 6 7import QtQuick 2.15 8import QtQuick.Controls 2.3 9import QtQuick.Window 2.1 10import QtQuick.Layouts 1.1 11import QtGraphicalEffects 1.12 12import org.kde.discover 2.0 13import org.kde.discover.app 1.0 14import org.kde.kirigami 2.14 as Kirigami 15import "navigation.js" as Navigation 16 17DiscoverPage { 18 id: appInfo 19 property QtObject application: null 20 readonly property int visibleReviews: 3 21 title: appInfo.application.name 22 clip: true 23 24 // Usually this page is not the top level page, but when we are, isHome being 25 // true will ensure that the search field suggests we are searching in the list 26 // of available apps, not inside the app page itself. This will happen when 27 // Discover is launched e.g. from krunner or otherwise requested to show a 28 // specific application on launch. 29 readonly property bool isHome: true 30 function searchFor(text) { 31 if (text.length === 0) 32 return; 33 Navigation.openCategory(null, "") 34 } 35 36 ReviewsPage { 37 id: reviewsSheet 38 model: ReviewsModel { 39 id: reviewsModel 40 resource: appInfo.application 41 } 42 } 43 44 contextualActions: [originsMenuAction] 45 46 ActionGroup { 47 id: sourcesGroup 48 exclusive: true 49 } 50 51 Kirigami.Action { 52 id: originsMenuAction 53 54 text: i18n("Sources") 55 visible: children.length>1 56 children: sourcesGroup.actions 57 readonly property var r0: Instantiator { 58 model: ResourcesProxyModel { 59 id: alternativeResourcesModel 60 allBackends: true 61 resourcesUrl: appInfo.application.url 62 } 63 delegate: Action { 64 ActionGroup.group: sourcesGroup 65 text: model.application.availableVersion ? i18n("%1 - %2", displayOrigin, model.application.availableVersion) : displayOrigin 66 icon.name: sourceIcon 67 checkable: true 68 checked: appInfo.application === model.application 69 onTriggered: if(index>=0) { 70 appInfo.application = model.application 71 } 72 } 73 } 74 } 75 76 Kirigami.Action { 77 id: invokeAction 78 visible: application.isInstalled && application.canExecute && !appbutton.isActive 79 text: application.executeLabel 80 icon.name: "media-playback-start" 81 onTriggered: application.invokeApplication() 82 } 83 84 actions { 85 main: appbutton.isActive ? appbutton.cancelAction : appbutton.action 86 right: invokeAction 87 } 88 89 InstallApplicationButton { 90 id: appbutton 91 Layout.rightMargin: Kirigami.Units.smallSpacing 92 application: appInfo.application 93 visible: false 94 } 95 96 topPadding: 0 97 bottomPadding: 0 98 leftPadding: 0 99 rightPadding: 0 100 101 // Icon, name, caption, screenshots, description and reviews 102 ColumnLayout { 103 spacing: 0 104 Kirigami.FlexColumn { 105 Layout.fillWidth: true 106 maximumWidth: Kirigami.Units.gridUnit * 40 107 RowLayout { 108 Layout.margins: Kirigami.Units.largeSpacing 109 Kirigami.Icon { 110 Layout.preferredHeight: 80 111 Layout.preferredWidth: 80 112 source: appInfo.application.icon 113 Layout.rightMargin: Kirigami.Units.smallSpacing * 2 114 } 115 ColumnLayout { 116 spacing: 0 117 Kirigami.Heading { 118 text: appInfo.application.name 119 lineHeight: 1.0 120 maximumLineCount: 1 121 font.weight: Font.DemiBold 122 elide: Text.ElideRight 123 Layout.fillWidth: true 124 Layout.alignment: Text.AlignBottom 125 } 126 RowLayout { 127 spacing: Kirigami.Units.largeSpacing 128 Rating { 129 rating: appInfo.application.rating ? appInfo.application.rating.sortableRating : 0 130 starSize: summary.font.pointSize 131 } 132 Label { 133 text: appInfo.application.rating ? i18np("%1 rating", "%1 ratings", appInfo.application.rating.ratingCount) : i18n("No ratings yet") 134 opacity: 0.6 135 } 136 } 137 Kirigami.Heading { 138 id: summary 139 level: 4 140 text: appInfo.application.author 141 opacity: 0.7 142 maximumLineCount: 2 143 lineHeight: lineCount > 1 ? 0.75 : 1.2 144 elide: Text.ElideRight 145 Layout.fillWidth: true 146 Layout.alignment: Qt.AlignTop 147 } 148 } 149 } 150 } 151 152 153 ScrollView { 154 visible: screenshots.count > 0 155 Layout.topMargin: Kirigami.Units.gridUnit * 2 156 Layout.fillWidth: true 157 Layout.preferredHeight: Math.min(Kirigami.Units.gridUnit * 20, Window.height * 0.4) 158 Layout.bottomMargin: Kirigami.Units.gridUnit * 2 159 160 ScrollBar.vertical.policy: ScrollBar.AlwaysOff 161 162 ApplicationScreenshots { 163 id: screenshots 164 resource: appInfo.application 165 } 166 } 167 168 Kirigami.FlexColumn { 169 Layout.fillWidth: true 170 Layout.margins: Kirigami.Units.largeSpacing 171 maximumWidth: Kirigami.Units.gridUnit * 40 172 Kirigami.Heading { 173 text: appInfo.application.comment 174 level: 2 175 font.weight: Font.DemiBold 176 Layout.fillWidth: true 177 wrapMode: Text.WordWrap 178 } 179 180 Label { 181 Layout.fillWidth: true 182 wrapMode: Text.WordWrap 183 text: appInfo.application.longDescription 184 onLinkActivated: Qt.openUrlExternally(link); 185 } 186 187 Kirigami.Heading { 188 Layout.topMargin: visible ? Kirigami.Units.largeSpacing : 0 189 text: i18n("What's New") 190 level: 2 191 visible: changelogLabel.text.length > 0 192 } 193 194 Label { 195 id: changelogLabel 196 Layout.topMargin: text.length > 0 ? Kirigami.Units.largeSpacing : 0 197 Layout.fillWidth: true 198 wrapMode: Text.WordWrap 199 200 Component.onCompleted: appInfo.application.fetchChangelog() 201 Connections { 202 target: appInfo.application 203 function onChangelogFetched(changelog) { 204 changelogLabel.text = changelog 205 } 206 } 207 } 208 209 Kirigami.LinkButton { 210 id: addonsButton 211 text: i18n("Addons") 212 visible: addonsView.containsAddons 213 onClicked: if (addonsView.addonsCount === 0) { 214 Navigation.openExtends(application.appstreamId) 215 } else { 216 addonsView.sheetOpen = true 217 } 218 } 219 220 Kirigami.Heading { 221 Layout.fillWidth: true 222 Layout.topMargin: Kirigami.Units.largeSpacing 223 font.weight: Font.DemiBold 224 text: i18n("Reviews") 225 Layout.alignment: Qt.AlignLeft | Qt.AlignBottom 226 level: 2 227 visible: rep.count > 0 228 } 229 230 Repeater { 231 id: rep 232 model: PaginateModel { 233 sourceModel: reviewsSheet.model 234 pageSize: visibleReviews 235 } 236 delegate: ReviewDelegate { 237 Layout.topMargin: Kirigami.Units.largeSpacing 238 Layout.fillWidth: true 239 separator: false 240 compact: true 241 } 242 } 243 244 RowLayout { 245 Layout.topMargin: Kirigami.Units.largeSpacing 246 Layout.bottomMargin: Kirigami.Units.largeSpacing 247 spacing: Kirigami.Units.largeSpacing 248 249 Button { 250 visible: reviewsModel.count > visibleReviews 251 252 text: i18np("Show %1 Review…", "Show All %1 Reviews…", reviewsModel.count) 253 icon.name: "view-visible" 254 255 onClicked: { 256 reviewsSheet.open() 257 } 258 } 259 260 Button { 261 visible: appbutton.isStateAvailable && reviewsModel.backend && reviewsModel.backend.isResourceSupported(appInfo.application) 262 enabled: appInfo.application.isInstalled 263 264 text: appInfo.application.isInstalled ? i18n("Write a Review") : i18n("Install to Write a Review") 265 icon.name: "document-edit" 266 267 onClicked: { 268 reviewsSheet.openReviewDialog() 269 } 270 } 271 } 272 273 Repeater { 274 model: application.objects 275 delegate: Loader { 276 property QtObject resource: appInfo.application 277 source: modelData 278 } 279 } 280 281 282 // Details/metadata 283 Kirigami.Separator { 284 Layout.fillWidth: true 285 Layout.bottomMargin: Kirigami.Units.largeSpacing 286 } 287 288 Kirigami.FormLayout { 289 Layout.fillWidth: true 290 291 // Category row 292 Label { 293 Kirigami.FormData.label: i18n("Category:") 294 visible: text.length > 0 295 Layout.fillWidth: true 296 elide: Text.ElideRight 297 text: appInfo.application.categoryDisplay 298 } 299 300 // Version row 301 Label { 302 Kirigami.FormData.label: i18n("Version:") 303 visible: text.length > 0 304 Layout.fillWidth: true 305 elide: Text.ElideRight 306 text: appInfo.application.versionString 307 } 308 309 // Author row 310 Label { 311 Kirigami.FormData.label: i18n("Author:") 312 Layout.fillWidth: true 313 elide: Text.ElideRight 314 visible: text.length>0 315 text: appInfo.application.author 316 } 317 318 // Size row 319 Label { 320 Kirigami.FormData.label: i18n("Size:") 321 Layout.fillWidth: true 322 Layout.alignment: Text.AlignTop 323 elide: Text.ElideRight 324 text: appInfo.application.sizeDescription 325 } 326 327 // Source row 328 Label { 329 Kirigami.FormData.label: i18n("Source:") 330 Layout.fillWidth: true 331 horizontalAlignment: Text.AlignLeft 332 text: appInfo.application.displayOrigin 333 elide: Text.ElideRight 334 } 335 336 // License row 337 RowLayout { 338 Kirigami.FormData.label: i18n("License:") 339 visible: appInfo.application.licenses.length>0 340 Layout.fillWidth: true 341 Repeater { 342 model: appInfo.application.licenses 343 delegate: Kirigami.UrlButton { 344 id: licenseButton 345 horizontalAlignment: Text.AlignLeft 346 ToolTip.text: i18n("See full license terms") 347 ToolTip.visible: licenseButton.mouseArea.containsMouse 348 text: modelData.name 349 url: modelData.url 350 enabled: url !== "" 351 } 352 } 353 } 354 355 // "User Guide" row 356 Kirigami.UrlButton { 357 visible: application.helpURL != "" 358 Kirigami.FormData.label: i18n ("Documentation:") 359 text: i18n("Read the user guide") 360 url: application.helpURL 361 Layout.fillWidth: true 362 horizontalAlignment: Text.AlignLeft 363 } 364 365 // Homepage row 366 Kirigami.UrlButton { 367 visible: application.homepage != "" 368 Kirigami.FormData.label: i18n("Get involved:") 369 text: i18n("Visit the app's website") 370 url: application.homepage 371 Layout.fillWidth: true 372 horizontalAlignment: Text.AlignLeft 373 } 374 375 // Donate row 376 Kirigami.UrlButton { 377 visible: application.donationURL != "" 378 text: i18n("Make a donation") 379 url: application.donationURL 380 Layout.fillWidth: true 381 horizontalAlignment: Text.AlignLeft 382 } 383 384 // "Report a Problem" row 385 Kirigami.UrlButton { 386 visible: application.bugURL != "" 387 text: i18n("Report a problem") 388 url: application.bugURL 389 Layout.fillWidth: true 390 horizontalAlignment: Text.AlignLeft 391 } 392 } 393 } 394 } 395 396 readonly property var addons: AddonsView { 397 id: addonsView 398 application: appInfo.application 399 } 400} 401