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.i18n import i18nCatalog
from UM.Version import Version
from UM.Message import Message
from cura import ApplicationMetadata
from cura import UltimakerCloudAuthentication
@ -23,6 +24,7 @@ from cura.Machines.ContainerTree import ContainerTree
from .AuthorsModel import AuthorsModel
from .PackagesModel import PackagesModel
from .SubscribedPackagesModel import SubscribedPackagesModel
if TYPE_CHECKING:
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.
self._server_response_data = {
"authors": [],
"packages": [],
"updates": [],
"authors": [],
"packages": [],
"updates": [],
"subscribed_packages": [],
} # type: Dict[str, List[Any]]
# Models:
self._models = {
"authors": AuthorsModel(self),
"packages": PackagesModel(self),
"updates": PackagesModel(self),
} # type: Dict[str, Union[AuthorsModel, PackagesModel]]
"authors": AuthorsModel(self),
"packages": PackagesModel(self),
"updates": PackagesModel(self),
"subscribed_packages": SubscribedPackagesModel(self),
} # type: Dict[str, Union[AuthorsModel, PackagesModel, SubscribedPackagesModel]]
self._plugins_showcase_model = PackagesModel(self)
self._plugins_available_model = PackagesModel(self)
@ -161,7 +165,7 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, int)
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)
for header_name, header_value in self._request_headers:
@ -197,6 +201,11 @@ class Toolbox(QObject, Extension):
cloud_api_version = self._cloud_api_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.
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)),
"packages": QUrl("{base_url}/packages".format(base_url = self._api_url)),
"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._fetchUserSubscribedPackages)
# 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:
# Request the latest and greatest!
self._fetchPackageUpdates()
self._fetchUserSubscribedPackages()
def _prepareNetworkManager(self):
if self._network_manager is not None:
@ -237,6 +249,11 @@ class Toolbox(QObject, Extension):
# Gather installed packages:
self._updateInstalledModels()
def _fetchUserSubscribedPackages(self):
if self._application.getCuraAPI().account.isLoggedIn:
self._prepareNetworkManager()
self._makeRequestByType("subscribed_packages")
# Displays the toolbox
@pyqtSlot()
def launch(self) -> None:
@ -540,9 +557,7 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, result = bool)
def isEnabled(self, package_id: str) -> bool:
if package_id in self._plugin_registry.getActivePlugins():
return True
return False
return package_id in self._plugin_registry.getActivePlugins()
# Check for plugins that were installed with the old plugin browser
def isOldPlugin(self, plugin_id: str) -> bool:
@ -561,10 +576,11 @@ class Toolbox(QObject, Extension):
# Make API Calls
# --------------------------------------------------------------------------
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])
for header_name, header_value in self._request_headers:
request.setRawHeader(header_name, header_value)
self._updateRequestHeader()
if self._network_manager:
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.
packages = set([pkg["package_id"] for pkg in self._server_response_data[response_type]])
self._package_manager.setPackagesWithUpdate(packages)
elif response_type == "subscribed_packages":
self._checkCompatibilities(json_data["data"])
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())
self.setViewPage("errored")
self.resetDownload()
elif reply.operation() == QNetworkAccessManager.PutOperation:
# Ignore any operation that is not a get operation
pass
def _checkCompatibilities(self, json_data) -> None:
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
def _notifyPackageManager(self):
@ -772,39 +819,43 @@ class Toolbox(QObject, Extension):
# Exposed Models:
# --------------------------------------------------------------------------
@pyqtProperty(QObject, constant=True)
@pyqtProperty(QObject, constant = True)
def authorsModel(self) -> AuthorsModel:
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:
return cast(PackagesModel, self._models["packages"])
@pyqtProperty(QObject, constant=True)
@pyqtProperty(QObject, constant = True)
def pluginsShowcaseModel(self) -> PackagesModel:
return self._plugins_showcase_model
@pyqtProperty(QObject, constant=True)
@pyqtProperty(QObject, constant = True)
def pluginsAvailableModel(self) -> PackagesModel:
return self._plugins_available_model
@pyqtProperty(QObject, constant=True)
@pyqtProperty(QObject, constant = True)
def pluginsInstalledModel(self) -> PackagesModel:
return self._plugins_installed_model
@pyqtProperty(QObject, constant=True)
@pyqtProperty(QObject, constant = True)
def materialsShowcaseModel(self) -> AuthorsModel:
return self._materials_showcase_model
@pyqtProperty(QObject, constant=True)
@pyqtProperty(QObject, constant = True)
def materialsAvailableModel(self) -> AuthorsModel:
return self._materials_available_model
@pyqtProperty(QObject, constant=True)
@pyqtProperty(QObject, constant = True)
def materialsInstalledModel(self) -> PackagesModel:
return self._materials_installed_model
@pyqtProperty(QObject, constant=True)
@pyqtProperty(QObject, constant = True)
def materialsGenericModel(self) -> PackagesModel:
return self._materials_generic_model