Merge remote-tracking branch 'origin/marketplace_redesign' into CURA-8587_disable_update_install_and_uninstall

# Conflicts:
#	plugins/Marketplace/LocalPackageList.py
#	plugins/Marketplace/PackageModel.py
#	plugins/Marketplace/RemotePackageList.py
#	plugins/Marketplace/resources/qml/Marketplace.qml
#	plugins/Marketplace/resources/qml/PackageCard.qml
#	plugins/Marketplace/resources/qml/Packages.qml
This commit is contained in:
Jelle Spijker 2021-12-03 13:36:56 +01:00
commit 4fef2de71c
No known key found for this signature in database
GPG key ID: 6662DC033BE6B99A
4 changed files with 739 additions and 277 deletions

View file

@ -1,11 +1,14 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
from typing import Any, Dict, Optional
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
import re
from typing import Any, Dict, List, Optional
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with.
from UM.Logger import Logger
from UM.i18n import i18nCatalog # To translate placeholder names if data is not present.
catalog = i18nCatalog("cura")
@ -39,10 +42,20 @@ class PackageModel(QObject):
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._formatted_description = self._format(self._description)
self._download_url = package_data.get("download_url", "") # Not used yet, will be.
self._download_url = package_data.get("download_url", "")
self._release_notes = package_data.get("release_notes", "") # Not used yet, propose to add to description?
subdata = package_data.get("data", {})
self._technical_data_sheet = self._findLink(subdata, "technical_data_sheet")
self._safety_data_sheet = self._findLink(subdata, "safety_data_sheet")
self._where_to_buy = self._findLink(subdata, "where_to_buy")
self._compatible_printers = self._getCompatiblePrinters(subdata)
self._compatible_support_materials = self._getCompatibleSupportMaterials(subdata)
self._is_compatible_material_station = self._isCompatibleMaterialStation(subdata)
self._is_compatible_air_manager = self._isCompatibleAirManager(subdata)
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", "")
@ -53,6 +66,111 @@ class PackageModel(QObject):
self._section_title = section_title
# Note that there's a lot more info in the package_data than just these specified here.
def _findLink(self, subdata: Dict[str, Any], link_type: str) -> str:
"""
Searches the package data for a link of a certain type.
The links are not in a fixed path in the package data. We need to iterate over the available links to find them.
:param subdata: The "data" element in the package data, which should contain links.
:param link_type: The type of link to find.
:return: A URL of where the link leads, or an empty string if there is no link of that type in the package data.
"""
links = subdata.get("links", [])
for link in links:
if link.get("type", "") == link_type:
return link.get("url", "")
else:
return "" # No link with the correct type was found.
def _format(self, text: str) -> str:
"""
Formats a user-readable block of text for display.
:return: A block of rich text with formatting embedded.
"""
# Turn all in-line hyperlinks into actual links.
url_regex = re.compile(r"(((http|https)://)[a-zA-Z0-9@:%.\-_+~#?&/=]{2,256}\.[a-z]{2,12}(/[a-zA-Z0-9@:%.\-_+~#?&/=]*)?)")
text = re.sub(url_regex, r'<a href="\1">\1</a>', text)
# Turn newlines into <br> so that they get displayed as newlines when rendering as rich text.
text = text.replace("\n", "<br>")
return text
def _getCompatiblePrinters(self, subdata: Dict[str, Any]) -> List[str]:
"""
Gets the list of printers that this package provides material compatibility with.
Any printer is listed, even if it's only for a single nozzle on a single material in the package.
:param subdata: The "data" element in the package data, which should contain this compatibility information.
:return: A list of printer names that this package provides material compatibility with.
"""
result = set()
for material in subdata.get("materials", []):
for compatibility in material.get("compatibility", []):
printer_name = compatibility.get("machine_name")
if printer_name is None:
continue # Missing printer name information. Skip this one.
for subcompatibility in compatibility.get("compatibilities", []):
if subcompatibility.get("hardware_compatible", False):
result.add(printer_name)
break
return list(sorted(result))
def _getCompatibleSupportMaterials(self, subdata: Dict[str, Any]) -> List[str]:
"""
Gets the list of support materials that the materials in this package are compatible with.
Since the materials are individually encoded as keys in the API response, only PVA and Breakaway are currently
supported.
:param subdata: The "data" element in the package data, which should contain this compatibility information.
:return: A list of support materials that the materials in this package are compatible with.
"""
result = set()
container_registry = CuraContainerRegistry.getInstance()
try:
pva_name = container_registry.findContainersMetadata(id = "ultimaker_pva")[0].get("name", "Ultimaker PVA")
except IndexError:
pva_name = "Ultimaker PVA"
try:
breakaway_name = container_registry.findContainersMetadata(id = "ultimaker_bam")[0].get("name", "Ultimaker Breakaway")
except IndexError:
breakaway_name = "Ultimaker Breakaway"
for material in subdata.get("materials", []):
if material.get("pva_compatible", False):
result.add(pva_name)
if material.get("breakaway_compatible", False):
result.add(breakaway_name)
return list(sorted(result))
def _isCompatibleMaterialStation(self, subdata: Dict[str, Any]) -> bool:
"""
Finds out if this package provides any material that is compatible with the material station.
:param subdata: The "data" element in the package data, which should contain this compatibility information.
:return: Whether this package provides any material that is compatible with the material station.
"""
for material in subdata.get("materials", []):
for compatibility in material.get("compatibility", []):
if compatibility.get("material_station_optimized", False):
return True
return False
def _isCompatibleAirManager(self, subdata: Dict[str, Any]) -> bool:
"""
Finds out if this package provides any material that is compatible with the air manager.
:param subdata: The "data" element in the package data, which should contain this compatibility information.
:return: Whether this package provides any material that is compatible with the air manager.
"""
for material in subdata.get("materials", []):
for compatibility in material.get("compatibility", []):
if compatibility.get("air_manager_optimized", False):
return True
return False
@pyqtProperty(str, constant = True)
def packageId(self) -> str:
return self._package_id
@ -89,6 +207,10 @@ class PackageModel(QObject):
def description(self):
return self._description
@pyqtProperty(str, constant = True)
def formattedDescription(self) -> str:
return self._formatted_description
@pyqtProperty(str, constant=True)
def authorName(self):
return self._author_name
@ -105,6 +227,34 @@ class PackageModel(QObject):
def sectionTitle(self) -> Optional[str]:
return self._section_title
@pyqtProperty(str, constant = True)
def technicalDataSheet(self) -> str:
return self._technical_data_sheet
@pyqtProperty(str, constant = True)
def safetyDataSheet(self) -> str:
return self._safety_data_sheet
@pyqtProperty(str, constant = True)
def whereToBuy(self) -> str:
return self._where_to_buy
@pyqtProperty("QStringList", constant = True)
def compatiblePrinters(self) -> List[str]:
return self._compatible_printers
@pyqtProperty("QStringList", constant = True)
def compatibleSupportMaterials(self) -> List[str]:
return self._compatible_support_materials
@pyqtProperty(bool, constant = True)
def isCompatibleMaterialStation(self) -> bool:
return self._is_compatible_material_station
@pyqtProperty(bool, constant = True)
def isCompatibleAirManager(self) -> bool:
return self._is_compatible_air_manager
isInstalledChanged = pyqtSignal()
@pyqtProperty(bool, notify = isInstalledChanged)

View file

@ -86,6 +86,15 @@ Window
}
}
OnboardBanner
{
visible: content.item && content.item.bannerVisible
text: content.item && content.item.bannerText
icon: content.item && content.item.bannerIcon
onRemove: content.item && content.item.onRemoveBanner
readMoreUrl: content.item && content.item.bannerReadMoreUrl
}
// Search & Top-Level Tabs
Item
{
@ -167,6 +176,25 @@ Window
}
}
FontMetrics
{
id: fontMetrics
font: UM.Theme.getFont("default")
}
Cura.TertiaryButton
{
text: catalog.i18nc("@info", "Search in the browser")
iconSource: UM.Theme.getIcon("LinkExternal")
visible: pageSelectionTabBar.currentItem.hasSearch
isIconOnRightSide: true
height: fontMetrics.height
textFont: fontMetrics.font
textColor: UM.Theme.getColor("text")
onClicked: content.item && Qt.openUrlExternally(content.item.searchInBrowserUrl)
}
// Page contents.
Rectangle
{

View file

@ -13,7 +13,7 @@ Rectangle
property var packageData
property bool expanded: false
height: UM.Theme.getSize("card").height
height: childrenRect.height
color: UM.Theme.getColor("main_background")
radius: UM.Theme.getSize("default_radius").width
@ -25,9 +25,19 @@ Rectangle
when: !expanded
PropertyChanges
{
target: descriptionArea
target: shortDescription
visible: true
}
PropertyChanges
{
target: downloadCount
visible: false
}
PropertyChanges
{
target: extendedDescription
visible: false
}
},
State
{
@ -35,13 +45,33 @@ Rectangle
when: expanded
PropertyChanges
{
target: descriptionArea
target: shortDescription
visible: false
}
PropertyChanges
{
target: downloadCount
visible: true
}
PropertyChanges
{
target: extendedDescription
visible: true
}
}
]
// Separate column for icon on the left.
Column
{
width: parent.width
spacing: 0
Item
{
width: parent.width
height: UM.Theme.getSize("card").height
Image
{
id: packageItem
@ -57,19 +87,25 @@ Rectangle
source: packageData.iconUrl != "" ? packageData.iconUrl : "../images/placeholder.svg"
}
ColumnLayout
{
anchors
{
left: packageItem.right
leftMargin: UM.Theme.getSize("default_margin").width
right: parent.right
rightMargin: UM.Theme.getSize("thick_margin").width
top: parent.top
topMargin: UM.Theme.getSize("narrow_margin").height
}
height: packageItem.height + packageItem.anchors.margins * 2
// Title row.
RowLayout
{
id: titleBar
anchors
{
left: packageItem.right
right: parent.right
top: parent.top
topMargin: UM.Theme.getSize("narrow_margin").height
leftMargin: UM.Theme.getSize("default_margin").width
rightMargin:UM.Theme.getSize("thick_margin").width
}
Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height
Label
{
@ -84,7 +120,6 @@ Rectangle
Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width
Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").height
enabled: packageData.isCheckedByUltimaker
visible: packageData.isCheckedByUltimaker
@ -100,7 +135,7 @@ Rectangle
}
}
visible: parent.hovered
targetPoint: Qt.point(0, Math.round(parent.y + parent.height / 2))
targetPoint: Qt.point(0, Math.round(parent.y + parent.height / 4))
}
Rectangle
@ -182,22 +217,14 @@ Rectangle
}
onClicked: Qt.openUrlExternally(packageData.authorInfoUrl)
}
}
// Description area
Item
{
id: descriptionArea
height: childrenRect.height > descriptionLabel.height ? childrenRect.height : descriptionLabel.height
anchors
{
top: titleBar.bottom
left: packageItem.right
right: parent.right
rightMargin: UM.Theme.getSize("default_margin").width
leftMargin: UM.Theme.getSize("default_margin").width
}
id: shortDescription
Layout.preferredWidth: parent.width
Layout.fillHeight: true
Label
{
id: descriptionLabel
@ -205,11 +232,13 @@ Rectangle
property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision.
text: packageData.description
textFormat: Text.PlainText //Must be plain text, or we won't get onLineLaidOut signals. Don't auto-detect!
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
maximumLineCount: 2
wrapMode: Text.Wrap
elide: Text.ElideRight
visible: text !== ""
onLineLaidOut:
{
@ -233,24 +262,24 @@ Rectangle
id: tripleDotLabel
anchors.left: parent.left
anchors.leftMargin: descriptionLabel.lastLineWidth
anchors.bottom: readMoreButton.bottom
anchors.bottom: descriptionLabel.bottom
text: "… "
font: descriptionLabel.font
color: descriptionLabel.color
visible: descriptionLabel.truncated
visible: descriptionLabel.truncated && descriptionLabel.text !== ""
}
Cura.TertiaryButton
{
id: readMoreButton
anchors.left: tripleDotLabel.right
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
visible: descriptionLabel.truncated && descriptionLabel.text !== ""
enabled: visible
leftPadding: UM.Theme.getSize("default_margin").width
rightPadding: UM.Theme.getSize("wide_margin").width
@ -261,24 +290,47 @@ Rectangle
}
}
Row
{
id: downloadCount
Layout.preferredWidth: parent.width
Layout.fillHeight: true
UM.RecolorImage
{
id: downloadsIcon
width: UM.Theme.getSize("card_tiny_icon").width
height: UM.Theme.getSize("card_tiny_icon").height
visible: packageData.installationStatus !== "bundled" //Don't show download count for packages that are bundled. It'll usually be 0.
source: UM.Theme.getIcon("Download")
color: UM.Theme.getColor("text")
}
Label
{
anchors.verticalCenter: downloadsIcon.verticalCenter
visible: packageData.installationStatus !== "bundled" //Don't show download count for packages that are bundled. It'll usually be 0.
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
text: packageData.downloadCount
}
}
// Author and action buttons.
RowLayout
{
id: authorAndActionButton
width: parent.width
anchors
{
bottom: parent.bottom
left: packageItem.right
right: parent.right
margins: UM.Theme.getSize("default_margin").height
}
Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height
spacing: UM.Theme.getSize("narrow_margin").width
Label
{
id: authorBy
Layout.alignment: Qt.AlignVCenter
Layout.alignment: Qt.AlignTop
text: catalog.i18nc("@label", "By")
font: UM.Theme.getFont("default")
@ -289,7 +341,7 @@ Rectangle
{
Layout.fillWidth: true
Layout.preferredHeight: authorBy.height
Layout.alignment: Qt.AlignVCenter
Layout.alignment: Qt.AlignTop
text: packageData.authorName
textFont: UM.Theme.getFont("default_bold")
@ -304,26 +356,252 @@ Rectangle
Cura.SecondaryButton
{
id: enableManageButton
id: disableButton
Layout.alignment: Qt.AlignTop
text: packageData.enableManageButtonText
visible: packageData.enableManageButtonVisible
text: catalog.i18nc("@button", "Disable")
visible: false // not functional right now, also only when unfolding and required
}
Cura.SecondaryButton
{
id: installManageButton
id: uninstallButton
Layout.alignment: Qt.AlignTop
text: packageData.installManageButtonText
visible: packageData.installManageButtonVisible
text: catalog.i18nc("@button", "Uninstall")
visible: false // not functional right now, also only when unfolding and required
}
Cura.PrimaryButton
{
id: updateManageButton
id: installButton
Layout.alignment: Qt.AlignTop
text: catalog.i18nc("@button", "Update") // OR Download, if new!
visible: packageData.updateManageButtonVisible
visible: false // not functional right now, also only when unfolding and required
}
}
}
}
Column
{
id: extendedDescription
width: parent.width
padding: UM.Theme.getSize("default_margin").width
topPadding: 0
spacing: UM.Theme.getSize("default_margin").height
Label
{
width: parent.width - parent.padding * 2
text: catalog.i18nc("@header", "Description")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
{
width: parent.width - parent.padding * 2
text: packageData.formattedDescription
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
wrapMode: Text.Wrap
textFormat: Text.RichText
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
}
Column //Separate column to have no spacing between compatible printers.
{
id: compatiblePrinterColumn
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible printers")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Repeater
{
model: packageData.compatiblePrinters
Label
{
width: compatiblePrinterColumn.width
text: modelData
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Label
{
width: parent.width
visible: packageData.compatiblePrinters.length == 0
text: "(" + catalog.i18nc("@info", "No compatibility information") + ")"
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Column
{
id: compatibleSupportMaterialColumn
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible support materials")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Repeater
{
model: packageData.compatibleSupportMaterials
Label
{
width: compatibleSupportMaterialColumn.width
text: modelData
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Label
{
width: parent.width
visible: packageData.compatibleSupportMaterials.length == 0
text: "(" + catalog.i18nc("@info No materials", "None") + ")"
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Column
{
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible with Material Station")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
{
width: parent.width
text: packageData.isCompatibleMaterialStation ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Column
{
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Optimized for Air Manager")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
{
width: parent.width
text: packageData.isCompatibleAirManager ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Row
{
id: externalButtonRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: UM.Theme.getSize("narrow_margin").width
Cura.SecondaryButton
{
text: packageData.packageType === "plugin" ? catalog.i18nc("@button", "Visit plug-in website") : catalog.i18nc("@button", "Website")
iconSource: UM.Theme.getIcon("Globe")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.packageInfoUrl)
}
Cura.SecondaryButton
{
visible: packageData.packageType === "material"
text: catalog.i18nc("@button", "Buy spool")
iconSource: UM.Theme.getIcon("ShoppingCart")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.whereToBuy)
}
Cura.SecondaryButton
{
visible: packageData.packageType === "material"
text: catalog.i18nc("@button", "Safety datasheet")
iconSource: UM.Theme.getIcon("Warning")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.safetyDataSheet)
}
Cura.SecondaryButton
{
visible: packageData.packageType === "material"
text: catalog.i18nc("@button", "Technical datasheet")
iconSource: UM.Theme.getIcon("DocumentFilled")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.technicalDataSheet)
}
}
}
}

View file

@ -13,6 +13,12 @@ ListView
property string pageTitle
property var selectedPackage
property string searchInBrowserUrl
property bool bannerVisible
property var bannerIcon
property string bannerText
property string bannerReadMoreUrl
property var onRemoveBanner
clip: true