diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index e57a402fd6..0e73cac3b7 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -4,10 +4,12 @@ from PyQt5.QtCore import pyqtProperty, QObject from typing import Any, Dict, Optional -from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. +from UM.Util import parseBool +from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. catalog = i18nCatalog("cura") + class PackageModel(QObject): """ Represents a package, containing all the relevant information to be displayed about a package. @@ -25,17 +27,65 @@ class PackageModel(QObject): """ super().__init__(parent) self._package_id = package_data.get("package_id", "UnknownPackageId") + + self._icon_url = package_data.get("icon_url", "") self._display_name = package_data.get("display_name", catalog.i18nc("@label:property", "Unknown Package")) + self._is_verified = "verified" in package_data.get("tags", []) + self._package_version = package_data.get("package_version", "") # Display purpose, no need for 'UM.Version'. + self._package_info_url = package_data.get("website", "") # Not to be confused with 'download_url'. + self._download_count = package_data.get("download_count", 0) + self._description = package_data.get("description", "") + + self._download_url = package_data.get("download_url", "") # Not used yet, will be. + self._release_notes = package_data.get("release_notes", "") # Not used yet, propose to add to description? + + author_data = package_data.get("author", {}) + self._author_name = author_data.get("display_name", catalog.i18nc("@label:property", "Unknown Author")) + self._author_info_url = author_data.get("website", "") + self._section_title = section_title + # Note that there's a lot more info in the package_data than just these specified here. @pyqtProperty(str, constant = True) def packageId(self) -> str: return self._package_id + @pyqtProperty(str, constant=True) + def iconUrl(self): + return self._icon_url + @pyqtProperty(str, constant = True) def displayName(self) -> str: return self._display_name + @pyqtProperty(bool, constant=True) + def isVerified(self): + return self._is_verified + + @pyqtProperty(str, constant=True) + def packageVersion(self): + return self._package_version + + @pyqtProperty(str, constant=True) + def packageInfoUrl(self): + return self._package_info_url + + @pyqtProperty(int, constant=True) + def downloadCount(self): + return self._download_count + + @pyqtProperty(str, constant=True) + def description(self): + return self._description + + @pyqtProperty(str, constant=True) + def authorName(self): + return self._author_name + + @pyqtProperty(str, constant=True) + def authorInfoUrl(self): + return self._author_info_url + @pyqtProperty(str, constant = True) def sectionTitle(self) -> Optional[str]: return self._section_title diff --git a/plugins/Marketplace/resources/images/placeholder.svg b/plugins/Marketplace/resources/images/placeholder.svg new file mode 100644 index 0000000000..cc674a4b38 --- /dev/null +++ b/plugins/Marketplace/resources/images/placeholder.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml new file mode 100644 index 0000000000..35eb94c422 --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -0,0 +1,237 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +Rectangle +{ + property var packageData + + width: parent ? parent.width : 0 + height: childrenRect.height + UM.Theme.getSize("default_margin").height * 2 + + color: UM.Theme.getColor("main_background") + radius: UM.Theme.getSize("default_radius").width + + RowLayout + { + width: parent.width - UM.Theme.getSize("default_margin").width * 2 + height: childrenRect.height + x: UM.Theme.getSize("default_margin").width + y: UM.Theme.getSize("default_margin").height + + spacing: UM.Theme.getSize("default_margin").width + + Image //Separate column for icon on the left. + { + Layout.preferredWidth: UM.Theme.getSize("card_icon").width + Layout.preferredHeight: UM.Theme.getSize("card_icon").height + Layout.alignment: Qt.AlignTop + + source: packageData.iconUrl != "" ? packageData.iconUrl : "../images/placeholder.svg" + } + + Column + { + Layout.fillWidth: true + Layout.preferredHeight: childrenRect.height + Layout.alignment: Qt.AlignTop + + spacing: UM.Theme.getSize("default_margin").height + + RowLayout //Title row. + { + width: parent.width + + spacing: UM.Theme.getSize("default_margin").width + + Label + { + Layout.alignment: Qt.AlignTop + + text: packageData.displayName + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + } + + Row //Row inside row, but the non-layout version skips invisible elements. + { + spacing: parent.spacing + Layout.alignment: Qt.AlignTop + + UM.RecolorImage + { + width: UM.Theme.getSize("section_icon").width + height: UM.Theme.getSize("section_icon").height + + color: UM.Theme.getColor("primary") + visible: packageData.isVerified + source: UM.Theme.getIcon("CheckCircle") + + // TODO: on hover + } + + Rectangle + { // placeholder for 'certified material' icon+link whenever we implement the materials part of this card + width: UM.Theme.getSize("section_icon").width + height: UM.Theme.getSize("section_icon").height + + visible: false + // TODO: on hover + } + } + + Label + { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + + text: packageData.packageVersion + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + } + + UM.RecolorImage + { + Layout.preferredWidth: UM.Theme.getSize("section_icon").width + Layout.preferredHeight: UM.Theme.getSize("section_icon").height + Layout.alignment: Qt.AlignTop + + color: UM.Theme.getColor("icon") + source: UM.Theme.getIcon("LinkExternal") + + // TODO: on clicked url + } + } + + Item + { + width: parent.width + height: descriptionLabel.height + + Label + { + id: descriptionLabel + width: parent.width + property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision. + + text: packageData.description + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Text.ElideRight + + onLineLaidOut: + { + if(truncated && line.isLast) + { + let max_line_width = parent.width - readMoreButton.width - fontMetrics.advanceWidth("… ") - UM.Theme.getSize("default_margin").width; + if(line.implicitWidth > max_line_width) + { + line.width = max_line_width; + } + else + { + line.width = line.implicitWidth - fontMetrics.advanceWidth("…"); //Truncate the ellipsis. We're adding this ourselves. + } + descriptionLabel.lastLineWidth = line.implicitWidth; + } + } + } + + Cura.TertiaryButton + { + id: readMoreButton + anchors.right: parent.right + anchors.bottom: parent.bottom + height: fontMetrics.height //Height of a single line. + + text: catalog.i18nc("@info", "Read more") + iconSource: UM.Theme.getIcon("LinkExternal") + + visible: descriptionLabel.truncated + enabled: visible + leftPadding: UM.Theme.getSize("default_margin").width + rightPadding: UM.Theme.getSize("wide_margin").width + textFont: descriptionLabel.font + isIconOnRightSide: true + } + + Label + { + anchors.left: parent.left + anchors.leftMargin: descriptionLabel.lastLineWidth + anchors.bottom: readMoreButton.bottom + + text: "… " + font: descriptionLabel.font + color: descriptionLabel.color + visible: descriptionLabel.truncated + } + } + + RowLayout //Author and action buttons. + { + width: parent.width + + spacing: UM.Theme.getSize("default_margin").width + + Label + { + id: authorBy + Layout.alignment: Qt.AlignTop + + text: catalog.i18nc("@label", "By") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + } + + Cura.TertiaryButton + { + Layout.fillWidth: true + Layout.preferredHeight: authorBy.height + Layout.alignment: Qt.AlignTop + + text: packageData.authorName + textFont: UM.Theme.getFont("default_bold") + leftPadding: 0 + rightPadding: 0 + iconSource: UM.Theme.getIcon("LinkExternal") + isIconOnRightSide: true + + // TODO on clicked (is link) -> MouseArea? + } + + Cura.SecondaryButton + { + text: catalog.i18nc("@button", "Disable") + // not functional right now + } + + Cura.SecondaryButton + { + text: catalog.i18nc("@button", "Uninstall") + // not functional right now + } + + Cura.PrimaryButton + { + text: catalog.i18nc("@button", "Update") + // not functional right now + } + } + } + } + + FontMetrics + { + id: fontMetrics + font: UM.Theme.getFont("medium") + } +} diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml index c9ddf88d16..95064c4469 100644 --- a/plugins/Marketplace/resources/qml/Packages.qml +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -46,24 +46,9 @@ ScrollView } } - delegate: Rectangle + delegate: PackageCard { - width: packagesListview.width - height: UM.Theme.getSize("card").height - - color: UM.Theme.getColor("main_background") - radius: UM.Theme.getSize("default_radius").width - - Label - { - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: Math.round((parent.height - height) / 2) - - text: model.package.displayName - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("text") - } + packageData: model.package } //Wrapper item to add spacing between content and footer. diff --git a/resources/themes/cura-light/icons/default/Download.svg b/resources/themes/cura-light/icons/default/Download.svg new file mode 100644 index 0000000000..cbe0da2a99 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Download.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index abd4844f47..07ed1a899d 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -553,7 +553,9 @@ "standard_list_lineheight": [1.5, 1.5], "standard_arrow": [1.0, 1.0], + "card": [25.0, 8.75], + "card_icon": [6.0, 6.0], "button": [4, 4], "button_icon": [2.5, 2.5],