Merge pull request #6845 from Ultimaker/CURA-7038

CURA-7038/Show list of packages to be synced
This commit is contained in:
Remco Burema 2019-12-31 14:52:34 +01:00 committed by GitHub
commit 08ea791051
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 264 additions and 25 deletions

View File

@ -0,0 +1,142 @@
// Copyright (c) 2020 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import UM 1.1 as UM
import Cura 1.6 as Cura
UM.Dialog{
visible: true
title: catalog.i18nc("@title", "Changes from your account")
width: UM.Theme.getSize("popup_dialog").width
height: UM.Theme.getSize("popup_dialog").height
minimumWidth: width
maximumWidth: minimumWidth
minimumHeight: height
maximumHeight: minimumHeight
margin: 0
Rectangle
{
id: root
anchors.fill: parent
color: UM.Theme.getColor("main_background")
UM.I18nCatalog
{
id: catalog
name: "cura"
}
ScrollView
{
width: parent.width
height: parent.height - nextButton.height - nextButton.anchors.margins * 2 // We want some leftover space for the button at the bottom
clip: true
Column
{
anchors.fill: parent
anchors.margins: UM.Theme.getSize("default_margin").width
// Compatible packages
Label
{
font: UM.Theme.getFont("default")
text: catalog.i18nc("@label", "The following packages will be added:")
color: UM.Theme.getColor("text")
height: contentHeight + UM.Theme.getSize("default_margin").height
}
Repeater
{
model: toolbox.subscribedPackagesModel
Component
{
Item
{
width: parent.width
property var lineHeight: 60
visible: model.is_compatible == "True" ? true : false
height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the compatible packages here
Image
{
id: packageIcon
source: model.icon_url || "../../images/logobot.svg"
height: lineHeight
width: height
mipmap: true
fillMode: Image.PreserveAspectFit
}
Label
{
text: model.name
font: UM.Theme.getFont("medium_bold")
anchors.left: packageIcon.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: packageIcon.verticalCenter
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
}
}
// Incompatible packages
Label
{
font: UM.Theme.getFont("default")
text: catalog.i18nc("@label", "The following packages can not be installed because of incompatible Cura version:")
color: UM.Theme.getColor("text")
height: contentHeight + UM.Theme.getSize("default_margin").height
}
Repeater
{
model: toolbox.subscribedPackagesModel
Component
{
Item
{
width: parent.width
property var lineHeight: 60
visible: model.is_compatible == "True" ? false : true
height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the incompatible packages here
Image
{
id: packageIcon
source: model.icon_url || "../../images/logobot.svg"
height: lineHeight
width: height
mipmap: true
fillMode: Image.PreserveAspectFit
}
Label
{
text: model.name
font: UM.Theme.getFont("medium_bold")
anchors.left: packageIcon.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: packageIcon.verticalCenter
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
}
}
}
} // End of ScrollView
Cura.ActionButton
{
id: nextButton
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").height
text: catalog.i18nc("@button", "Next")
}
}
}

View File

@ -0,0 +1,46 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt
from UM.Qt.ListModel import ListModel
from cura import ApplicationMetadata
class SubscribedPackagesModel(ListModel):
def __init__(self, parent = None):
super().__init__(parent)
self._metadata = None
self._discrepancies = None
self._sdk_version = ApplicationMetadata.CuraSDKVersion
self.addRoleName(Qt.UserRole + 1, "name")
self.addRoleName(Qt.UserRole + 2, "icon_url")
self.addRoleName(Qt.UserRole + 3, "is_compatible")
def setMetadata(self, data):
if self._metadata != data:
self._metadata = data
def addValue(self, discrepancy):
if self._discrepancies != discrepancy:
self._discrepancies = discrepancy
def update(self):
items = []
for item in self._metadata:
if item["package_id"] not in self._discrepancies:
continue
package = {"name": item["display_name"], "sdk_versions": item["sdk_versions"]}
if self._sdk_version not in item["sdk_versions"]:
package.update({"is_compatible": "False"})
else:
package.update({"is_compatible": "True"})
try:
package.update({"icon_url": item["icon_url"]})
except KeyError: # There is no 'icon_url" in the response payload for this package
package.update({"icon_url": ""})
items.append(package)
self.setItems(items)

View File

@ -15,6 +15,7 @@ from UM.PluginRegistry import PluginRegistry
from UM.Extension import Extension from UM.Extension import Extension
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Version import Version from UM.Version import Version
from UM.Message import Message
from cura import ApplicationMetadata from cura import ApplicationMetadata
from cura import UltimakerCloudAuthentication from cura import UltimakerCloudAuthentication
@ -23,6 +24,7 @@ from cura.Machines.ContainerTree import ContainerTree
from .AuthorsModel import AuthorsModel from .AuthorsModel import AuthorsModel
from .PackagesModel import PackagesModel from .PackagesModel import PackagesModel
from .SubscribedPackagesModel import SubscribedPackagesModel
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
@ -58,17 +60,19 @@ class Toolbox(QObject, Extension):
# The responses as given by the server parsed to a list. # The responses as given by the server parsed to a list.
self._server_response_data = { self._server_response_data = {
"authors": [], "authors": [],
"packages": [], "packages": [],
"updates": [], "updates": [],
"subscribed_packages": [],
} # type: Dict[str, List[Any]] } # type: Dict[str, List[Any]]
# Models: # Models:
self._models = { self._models = {
"authors": AuthorsModel(self), "authors": AuthorsModel(self),
"packages": PackagesModel(self), "packages": PackagesModel(self),
"updates": PackagesModel(self), "updates": PackagesModel(self),
} # type: Dict[str, Union[AuthorsModel, PackagesModel]] "subscribed_packages": SubscribedPackagesModel(self),
} # type: Dict[str, Union[AuthorsModel, PackagesModel, SubscribedPackagesModel]]
self._plugins_showcase_model = PackagesModel(self) self._plugins_showcase_model = PackagesModel(self)
self._plugins_available_model = PackagesModel(self) self._plugins_available_model = PackagesModel(self)
@ -161,7 +165,7 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, int) @pyqtSlot(str, int)
def ratePackage(self, package_id: str, rating: int) -> None: def ratePackage(self, package_id: str, rating: int) -> None:
url = QUrl("{base_url}/packages/{package_id}/ratings".format(base_url=self._api_url, package_id = package_id)) url = QUrl("{base_url}/packages/{package_id}/ratings".format(base_url = self._api_url, package_id = package_id))
self._rate_request = QNetworkRequest(url) self._rate_request = QNetworkRequest(url)
for header_name, header_value in self._request_headers: for header_name, header_value in self._request_headers:
@ -197,6 +201,11 @@ class Toolbox(QObject, Extension):
cloud_api_version = self._cloud_api_version, cloud_api_version = self._cloud_api_version,
sdk_version = self._sdk_version sdk_version = self._sdk_version
) )
# https://api.ultimaker.com/cura-packages/v1/user/packages
self._api_url_user_packages = "{cloud_api_root}/cura-packages/v{cloud_api_version}/user/packages".format(
cloud_api_root = self._cloud_api_root,
cloud_api_version = self._cloud_api_version,
)
# We need to construct a query like installed_packages=ID:VERSION&installed_packages=ID:VERSION, etc. # We need to construct a query like installed_packages=ID:VERSION&installed_packages=ID:VERSION, etc.
installed_package_ids_with_versions = [":".join(items) for items in installed_package_ids_with_versions = [":".join(items) for items in
@ -207,15 +216,18 @@ class Toolbox(QObject, Extension):
"authors": QUrl("{base_url}/authors".format(base_url = self._api_url)), "authors": QUrl("{base_url}/authors".format(base_url = self._api_url)),
"packages": QUrl("{base_url}/packages".format(base_url = self._api_url)), "packages": QUrl("{base_url}/packages".format(base_url = self._api_url)),
"updates": QUrl("{base_url}/packages/package-updates?installed_packages={query}".format( "updates": QUrl("{base_url}/packages/package-updates?installed_packages={query}".format(
base_url = self._api_url, query = installed_packages_query)) base_url = self._api_url, query = installed_packages_query)),
"subscribed_packages": QUrl(self._api_url_user_packages)
} }
self._application.getCuraAPI().account.loginStateChanged.connect(self._restart) self._application.getCuraAPI().account.loginStateChanged.connect(self._restart)
self._application.getCuraAPI().account.loginStateChanged.connect(self._fetchUserSubscribedPackages)
# On boot we check which packages have updates. # On boot we check which packages have updates.
if CuraApplication.getInstance().getPreferences().getValue("info/automatic_update_check") and len(installed_package_ids_with_versions) > 0: if CuraApplication.getInstance().getPreferences().getValue("info/automatic_update_check") and len(installed_package_ids_with_versions) > 0:
# Request the latest and greatest! # Request the latest and greatest!
self._fetchPackageUpdates() self._fetchPackageUpdates()
self._fetchUserSubscribedPackages()
def _prepareNetworkManager(self): def _prepareNetworkManager(self):
if self._network_manager is not None: if self._network_manager is not None:
@ -237,6 +249,11 @@ class Toolbox(QObject, Extension):
# Gather installed packages: # Gather installed packages:
self._updateInstalledModels() self._updateInstalledModels()
def _fetchUserSubscribedPackages(self):
if self._application.getCuraAPI().account.isLoggedIn:
self._prepareNetworkManager()
self._makeRequestByType("subscribed_packages")
# Displays the toolbox # Displays the toolbox
@pyqtSlot() @pyqtSlot()
def launch(self) -> None: def launch(self) -> None:
@ -540,9 +557,7 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, result = bool) @pyqtSlot(str, result = bool)
def isEnabled(self, package_id: str) -> bool: def isEnabled(self, package_id: str) -> bool:
if package_id in self._plugin_registry.getActivePlugins(): return package_id in self._plugin_registry.getActivePlugins()
return True
return False
# Check for plugins that were installed with the old plugin browser # Check for plugins that were installed with the old plugin browser
def isOldPlugin(self, plugin_id: str) -> bool: def isOldPlugin(self, plugin_id: str) -> bool:
@ -561,10 +576,11 @@ class Toolbox(QObject, Extension):
# Make API Calls # Make API Calls
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
def _makeRequestByType(self, request_type: str) -> None: def _makeRequestByType(self, request_type: str) -> None:
Logger.log("d", "Requesting %s metadata from server.", request_type) Logger.log("d", "Requesting '%s' metadata from server.", request_type)
request = QNetworkRequest(self._request_urls[request_type]) request = QNetworkRequest(self._request_urls[request_type])
for header_name, header_value in self._request_headers: for header_name, header_value in self._request_headers:
request.setRawHeader(header_name, header_value) request.setRawHeader(header_name, header_value)
self._updateRequestHeader()
if self._network_manager: if self._network_manager:
self._network_manager.get(request) self._network_manager.get(request)
@ -661,6 +677,8 @@ class Toolbox(QObject, Extension):
# Tell the package manager that there's a new set of updates available. # Tell the package manager that there's a new set of updates available.
packages = set([pkg["package_id"] for pkg in self._server_response_data[response_type]]) packages = set([pkg["package_id"] for pkg in self._server_response_data[response_type]])
self._package_manager.setPackagesWithUpdate(packages) self._package_manager.setPackagesWithUpdate(packages)
elif response_type == "subscribed_packages":
self._checkCompatibilities(json_data["data"])
self.metadataChanged.emit() self.metadataChanged.emit()
@ -674,9 +692,38 @@ class Toolbox(QObject, Extension):
Logger.log("w", "Unable to connect with the server, we got a response code %s while trying to connect to %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url()) Logger.log("w", "Unable to connect with the server, we got a response code %s while trying to connect to %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
self.setViewPage("errored") self.setViewPage("errored")
self.resetDownload() self.resetDownload()
elif reply.operation() == QNetworkAccessManager.PutOperation:
# Ignore any operation that is not a get operation def _checkCompatibilities(self, json_data) -> None:
pass user_subscribed_packages = [plugin["package_id"] for plugin in json_data]
user_installed_packages = self._package_manager.getUserInstalledPackages()
# We check if there are packages installed in Cloud Marketplace but not in Cura marketplace (discrepancy)
package_discrepancy = list(set(user_subscribed_packages).difference(user_installed_packages))
if package_discrepancy:
self._models["subscribed_packages"].addValue(package_discrepancy)
self._models["subscribed_packages"].update()
Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages")
sync_message = Message(i18n_catalog.i18nc(
"@info:generic",
"\nDo you want to sync material and software packages with your account?"),
lifetime=0,
title=i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
sync_message.addAction("sync",
name=i18n_catalog.i18nc("@action:button", "Sync"),
icon="",
description="Sync your Cloud subscribed packages to your local environment.",
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
sync_message.actionTriggered.connect(self._onSyncButtonClicked)
sync_message.show()
def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None:
sync_message.hide()
compatibility_dialog_path = "resources/qml/dialogs/CompatibilityDialog.qml"
plugin_path_prefix = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
if plugin_path_prefix:
path = os.path.join(plugin_path_prefix, compatibility_dialog_path)
self.compatibility_dialog_view = self._application.getInstance().createQmlComponent(path, {"toolbox": self})
# This function goes through all known remote versions of a package and notifies the package manager of this change # This function goes through all known remote versions of a package and notifies the package manager of this change
def _notifyPackageManager(self): def _notifyPackageManager(self):
@ -772,39 +819,43 @@ class Toolbox(QObject, Extension):
# Exposed Models: # Exposed Models:
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@pyqtProperty(QObject, constant=True) @pyqtProperty(QObject, constant = True)
def authorsModel(self) -> AuthorsModel: def authorsModel(self) -> AuthorsModel:
return cast(AuthorsModel, self._models["authors"]) return cast(AuthorsModel, self._models["authors"])
@pyqtProperty(QObject, constant=True) @pyqtProperty(QObject, constant = True)
def subscribedPackagesModel(self) -> SubscribedPackagesModel:
return cast(SubscribedPackagesModel, self._models["subscribed_packages"])
@pyqtProperty(QObject, constant = True)
def packagesModel(self) -> PackagesModel: def packagesModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["packages"]) return cast(PackagesModel, self._models["packages"])
@pyqtProperty(QObject, constant=True) @pyqtProperty(QObject, constant = True)
def pluginsShowcaseModel(self) -> PackagesModel: def pluginsShowcaseModel(self) -> PackagesModel:
return self._plugins_showcase_model return self._plugins_showcase_model
@pyqtProperty(QObject, constant=True) @pyqtProperty(QObject, constant = True)
def pluginsAvailableModel(self) -> PackagesModel: def pluginsAvailableModel(self) -> PackagesModel:
return self._plugins_available_model return self._plugins_available_model
@pyqtProperty(QObject, constant=True) @pyqtProperty(QObject, constant = True)
def pluginsInstalledModel(self) -> PackagesModel: def pluginsInstalledModel(self) -> PackagesModel:
return self._plugins_installed_model return self._plugins_installed_model
@pyqtProperty(QObject, constant=True) @pyqtProperty(QObject, constant = True)
def materialsShowcaseModel(self) -> AuthorsModel: def materialsShowcaseModel(self) -> AuthorsModel:
return self._materials_showcase_model return self._materials_showcase_model
@pyqtProperty(QObject, constant=True) @pyqtProperty(QObject, constant = True)
def materialsAvailableModel(self) -> AuthorsModel: def materialsAvailableModel(self) -> AuthorsModel:
return self._materials_available_model return self._materials_available_model
@pyqtProperty(QObject, constant=True) @pyqtProperty(QObject, constant = True)
def materialsInstalledModel(self) -> PackagesModel: def materialsInstalledModel(self) -> PackagesModel:
return self._materials_installed_model return self._materials_installed_model
@pyqtProperty(QObject, constant=True) @pyqtProperty(QObject, constant = True)
def materialsGenericModel(self) -> PackagesModel: def materialsGenericModel(self) -> PackagesModel:
return self._materials_generic_model return self._materials_generic_model