mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-06 21:44:01 -06:00
Merge pull request #11027 from Ultimaker/CURA-8587_disable_update_install_and_uninstall
Cura 8587 disable update install and uninstall
This commit is contained in:
commit
6321b2b892
25 changed files with 1454 additions and 711 deletions
|
@ -1,13 +1,15 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import List, Tuple, TYPE_CHECKING, Optional
|
||||
from typing import Any, cast, Dict, List, Set, Tuple, TYPE_CHECKING, Optional
|
||||
|
||||
from cura.CuraApplication import CuraApplication #To find some resource types.
|
||||
from cura.CuraApplication import CuraApplication # To find some resource types.
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
from UM.PackageManager import PackageManager #The class we're extending.
|
||||
from UM.Resources import Resources #To find storage paths for some resource types.
|
||||
from UM.PackageManager import PackageManager # The class we're extending.
|
||||
from UM.Resources import Resources # To find storage paths for some resource types.
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
|
@ -17,6 +19,31 @@ if TYPE_CHECKING:
|
|||
class CuraPackageManager(PackageManager):
|
||||
def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(application, parent)
|
||||
self._local_packages: Optional[List[Dict[str, Any]]] = None
|
||||
self._local_packages_ids: Optional[Set[str]] = None
|
||||
self.installedPackagesChanged.connect(self._updateLocalPackages)
|
||||
|
||||
def _updateLocalPackages(self) -> None:
|
||||
self._local_packages = self.getAllLocalPackages()
|
||||
self._local_packages_ids = set(pkg["package_id"] for pkg in self._local_packages)
|
||||
|
||||
@property
|
||||
def local_packages(self) -> List[Dict[str, Any]]:
|
||||
"""locally installed packages, lazy execution"""
|
||||
if self._local_packages is None:
|
||||
self._updateLocalPackages()
|
||||
# _updateLocalPackages always results in a list of packages, not None.
|
||||
# It's guaranteed to be a list now.
|
||||
return cast(List[Dict[str, Any]], self._local_packages)
|
||||
|
||||
@property
|
||||
def local_packages_ids(self) -> Set[str]:
|
||||
"""locally installed packages, lazy execution"""
|
||||
if self._local_packages_ids is None:
|
||||
self._updateLocalPackages()
|
||||
# _updateLocalPackages always results in a list of packages, not None.
|
||||
# It's guaranteed to be a list now.
|
||||
return cast(Set[str], self._local_packages_ids)
|
||||
|
||||
def initialize(self) -> None:
|
||||
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
|
||||
|
@ -47,3 +74,12 @@ class CuraPackageManager(PackageManager):
|
|||
machine_with_qualities.append((global_stack, str(extruder_nr), container_id))
|
||||
|
||||
return machine_with_materials, machine_with_qualities
|
||||
|
||||
def getAllLocalPackages(self) -> List[Dict[str, Any]]:
|
||||
""" Returns an unordered list of all the package_info of installed, to be installed, or bundled packages"""
|
||||
packages: List[Dict[str, Any]] = []
|
||||
|
||||
for packages_to_add in self.getAllInstalledPackagesInfo().values():
|
||||
packages.extend(packages_to_add)
|
||||
|
||||
return packages
|
||||
|
|
12
plugins/Marketplace/Constants.py
Normal file
12
plugins/Marketplace/Constants.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from cura.UltimakerCloud import UltimakerCloudConstants
|
||||
from cura.ApplicationMetadata import CuraSDKVersion
|
||||
|
||||
ROOT_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}"
|
||||
ROOT_CURA_URL = f"{ROOT_URL}/cura/v{CuraSDKVersion}" # Root of all Marketplace API requests.
|
||||
ROOT_USER_URL = f"{ROOT_URL}/user"
|
||||
PACKAGES_URL = f"{ROOT_CURA_URL}/packages" # URL to use for requesting the list of packages.
|
||||
PACKAGE_UPDATES_URL = f"{PACKAGES_URL}/package-updates" # URL to use for requesting the list of packages that can be updated.
|
||||
USER_PACKAGES_URL = f"{ROOT_USER_URL}/packages"
|
|
@ -1,40 +1,66 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
from operator import attrgetter
|
||||
|
||||
from typing import Any, Dict, Generator, List, Optional, TYPE_CHECKING
|
||||
from PyQt5.QtCore import pyqtSlot, QObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtCore import QObject
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
||||
from UM.Logger import Logger
|
||||
|
||||
from .PackageList import PackageList
|
||||
from .PackageModel import PackageModel # The contents of this list.
|
||||
from .PackageModel import PackageModel
|
||||
from .Constants import PACKAGE_UPDATES_URL
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class LocalPackageList(PackageList):
|
||||
PACKAGE_SECTION_HEADER = {
|
||||
PACKAGE_CATEGORIES = {
|
||||
"installed":
|
||||
{
|
||||
"plugin": catalog.i18nc("@label:property", "Installed Plugins"),
|
||||
"material": catalog.i18nc("@label:property", "Installed Materials")
|
||||
"plugin": catalog.i18nc("@label", "Installed Plugins"),
|
||||
"material": catalog.i18nc("@label", "Installed Materials")
|
||||
},
|
||||
"bundled":
|
||||
{
|
||||
"plugin": catalog.i18nc("@label:property", "Bundled Plugins"),
|
||||
"material": catalog.i18nc("@label:property", "Bundled Materials")
|
||||
"plugin": catalog.i18nc("@label", "Bundled Plugins"),
|
||||
"material": catalog.i18nc("@label", "Bundled Materials")
|
||||
}
|
||||
} # The section headers to be used for the different package categories
|
||||
|
||||
def __init__(self, parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._manager = CuraApplication.getInstance().getPackageManager()
|
||||
self._has_footer = False
|
||||
self._ongoing_requests["check_updates"] = None
|
||||
self._package_manager.packagesWithUpdateChanged.connect(self._sortSectionsOnUpdate)
|
||||
self._package_manager.packageUninstalled.connect(self._removePackageModel)
|
||||
|
||||
def _sortSectionsOnUpdate(self) -> None:
|
||||
section_order = dict(zip([i for k, v in self.PACKAGE_CATEGORIES.items() for i in self.PACKAGE_CATEGORIES[k].values()], ["a", "b", "c", "d"]))
|
||||
self.sort(lambda model: (section_order[model.sectionTitle], model.canUpdate, model.displayName.lower()), key = "package")
|
||||
|
||||
def _removePackageModel(self, package_id: str) -> None:
|
||||
"""
|
||||
Cleanup function to remove the package model from the list. Note that this is only done if the package can't
|
||||
be updated, it is in the to remove list and isn't in the to be installed list
|
||||
"""
|
||||
package = self.getPackageModel(package_id)
|
||||
if not package.canUpdate and \
|
||||
package_id in self._package_manager.getToRemovePackageIDs() and \
|
||||
package_id not in self._package_manager.getPackagesToInstall():
|
||||
index = self.find("package", package_id)
|
||||
if index < 0:
|
||||
Logger.error(f"Could not find card in Listview corresponding with {package_id}")
|
||||
self.updatePackages()
|
||||
return
|
||||
self.removeItem(index)
|
||||
|
||||
@pyqtSlot()
|
||||
def updatePackages(self) -> None:
|
||||
|
@ -44,50 +70,52 @@ class LocalPackageList(PackageList):
|
|||
"""
|
||||
self.setErrorMessage("") # Clear any previous errors.
|
||||
self.setIsLoading(True)
|
||||
self._getLocalPackages()
|
||||
|
||||
# Obtain and sort the local packages
|
||||
self.setItems([{"package": p} for p in [self._makePackageModel(p) for p in self._package_manager.local_packages]])
|
||||
self._sortSectionsOnUpdate()
|
||||
self.checkForUpdates(self._package_manager.local_packages)
|
||||
|
||||
self.setIsLoading(False)
|
||||
self.setHasMore(False) # All packages should have been loaded at this time
|
||||
|
||||
def _getLocalPackages(self) -> None:
|
||||
""" Obtain the local packages.
|
||||
|
||||
The list is sorted per category as in the order of the PACKAGE_SECTION_HEADER dictionary, whereas the packages
|
||||
for the sections are sorted alphabetically on the display name. These sorted sections are then added to the items
|
||||
"""
|
||||
package_info = list(self._allPackageInfo())
|
||||
sorted_sections: List[Dict[str, PackageModel]] = []
|
||||
for section in self._getSections():
|
||||
packages = filter(lambda p: p.sectionTitle == section, package_info)
|
||||
sorted_sections.extend([{"package": p} for p in sorted(packages, key = lambda p: p.displayName)])
|
||||
self.setItems(sorted_sections)
|
||||
|
||||
def _getSections(self) -> Generator[str, None, None]:
|
||||
""" Flatten and order the PACKAGE_SECTION_HEADER such that it can be used in obtaining the packages in the
|
||||
correct order"""
|
||||
for package_type in self.PACKAGE_SECTION_HEADER.values():
|
||||
for section in package_type.values():
|
||||
yield section
|
||||
|
||||
def _allPackageInfo(self) -> Generator[PackageModel, None, None]:
|
||||
""" A generator which returns a unordered list of all the PackageModels"""
|
||||
|
||||
# Get all the installed packages, add a section_title depending on package_type and user installed
|
||||
for packages in self._manager.getAllInstalledPackagesInfo().values():
|
||||
for package_info in packages:
|
||||
yield self._makePackageModel(package_info)
|
||||
|
||||
# Get all to be removed package_info's. These packages are still used in the current session so the user might
|
||||
# still want to interact with these.
|
||||
for package_data in self._manager.getPackagesToRemove().values():
|
||||
yield self._makePackageModel(package_data["package_info"])
|
||||
|
||||
# Get all to be installed package_info's. Since the user might want to interact with these
|
||||
for package_data in self._manager.getPackagesToInstall().values():
|
||||
yield self._makePackageModel(package_data["package_info"])
|
||||
|
||||
def _makePackageModel(self, package_info: Dict[str, Any]) -> PackageModel:
|
||||
""" Create a PackageModel from the package_info and determine its section_title"""
|
||||
bundled_or_installed = "installed" if self._manager.isUserInstalledPackage(package_info["package_id"]) else "bundled"
|
||||
|
||||
package_id = package_info["package_id"]
|
||||
bundled_or_installed = "bundled" if self._package_manager.isBundledPackage(package_id) else "installed"
|
||||
package_type = package_info["package_type"]
|
||||
section_title = self.PACKAGE_SECTION_HEADER[bundled_or_installed][package_type]
|
||||
return PackageModel(package_info, installation_status = bundled_or_installed, section_title = section_title, parent = self)
|
||||
section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type]
|
||||
package = PackageModel(package_info, section_title = section_title, parent = self)
|
||||
self._connectManageButtonSignals(package)
|
||||
return package
|
||||
|
||||
def checkForUpdates(self, packages: List[Dict[str, Any]]) -> None:
|
||||
installed_packages = "&".join([f"installed_packages={package['package_id']}:{package['package_version']}" for package in packages])
|
||||
request_url = f"{PACKAGE_UPDATES_URL}?installed_packages={installed_packages[:-1]}"
|
||||
|
||||
self._ongoing_requests["check_updates"] = HttpRequestManager.getInstance().get(
|
||||
request_url,
|
||||
scope = self._scope,
|
||||
callback = self._parseResponse
|
||||
)
|
||||
|
||||
def _parseResponse(self, reply: "QNetworkReply") -> None:
|
||||
"""
|
||||
Parse the response from the package list API request which can update.
|
||||
|
||||
:param reply: A reply containing information about a number of packages.
|
||||
"""
|
||||
response_data = HttpRequestManager.readJSON(reply)
|
||||
if "data" not in response_data:
|
||||
Logger.error(
|
||||
f"Could not interpret the server's response. Missing 'data' from response data. Keys in response: {response_data.keys()}")
|
||||
return
|
||||
if len(response_data["data"]) == 0:
|
||||
return
|
||||
|
||||
packages = response_data["data"]
|
||||
|
||||
self._package_manager.setPackagesWithUpdate({p['package_id'] for p in packages})
|
||||
|
||||
self._ongoing_requests["check_updates"] = None
|
||||
|
|
|
@ -6,22 +6,18 @@ from PyQt5.QtCore import pyqtSlot
|
|||
from PyQt5.QtQml import qmlRegisterType
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from cura.ApplicationMetadata import CuraSDKVersion
|
||||
from cura.CuraApplication import CuraApplication # Creating QML objects and managing packages.
|
||||
from cura.UltimakerCloud import UltimakerCloudConstants
|
||||
|
||||
from UM.Extension import Extension # We are implementing the main object of an extension here.
|
||||
from UM.PluginRegistry import PluginRegistry # To find out where we are stored (the proper way).
|
||||
|
||||
from .RemotePackageList import RemotePackageList # To register this type with QML.
|
||||
from .LocalPackageList import LocalPackageList # To register this type with QML.
|
||||
from .RestartManager import RestartManager # To register this type with QML.
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtCore import QObject
|
||||
|
||||
ROOT_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}/cura/v{CuraSDKVersion}" # Root of all Marketplace API requests.
|
||||
PACKAGES_URL = f"{ROOT_URL}/packages" # URL to use for requesting the list of packages.
|
||||
|
||||
|
||||
class Marketplace(Extension):
|
||||
"""
|
||||
|
@ -31,9 +27,11 @@ class Marketplace(Extension):
|
|||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._window: Optional["QObject"] = None # If the window has been loaded yet, it'll be cached in here.
|
||||
self._plugin_registry: Optional[PluginRegistry] = None
|
||||
|
||||
qmlRegisterType(RemotePackageList, "Marketplace", 1, 0, "RemotePackageList")
|
||||
qmlRegisterType(LocalPackageList, "Marketplace", 1, 0, "LocalPackageList")
|
||||
qmlRegisterType(RestartManager, "Marketplace", 1, 0, "RestartManager")
|
||||
|
||||
@pyqtSlot()
|
||||
def show(self) -> None:
|
||||
|
@ -43,6 +41,7 @@ class Marketplace(Extension):
|
|||
If the window hadn't been loaded yet into Qt, it will be created lazily.
|
||||
"""
|
||||
if self._window is None:
|
||||
self._plugin_registry = PluginRegistry.getInstance()
|
||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
if plugin_path is None:
|
||||
plugin_path = os.path.dirname(__file__)
|
||||
|
|
|
@ -1,14 +1,29 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import tempfile
|
||||
import json
|
||||
import os.path
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from typing import cast, Dict, Optional, Set, TYPE_CHECKING
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestData, HttpRequestManager
|
||||
from UM.Logger import Logger
|
||||
from UM import PluginRegistry
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.CuraPackageManager import CuraPackageManager
|
||||
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization.
|
||||
|
||||
from .PackageModel import PackageModel
|
||||
from .Constants import USER_PACKAGES_URL, PACKAGES_URL
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtCore import QObject
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -18,26 +33,51 @@ class PackageList(ListModel):
|
|||
such as Packages obtained from Remote or Local source
|
||||
"""
|
||||
PackageRole = Qt.UserRole + 1
|
||||
DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB
|
||||
|
||||
def __init__(self, parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
|
||||
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
|
||||
self._account = CuraApplication.getInstance().getCuraAPI().account
|
||||
self._error_message = ""
|
||||
self.addRoleName(self.PackageRole, "package")
|
||||
self._is_loading = False
|
||||
self._has_more = False
|
||||
self._has_footer = True
|
||||
self._to_install: Dict[str, str] = {}
|
||||
|
||||
self._ongoing_requests: Dict[str, Optional[HttpRequestData]] = {"download_package": None}
|
||||
self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
|
||||
self._license_dialogs: Dict[str, QObject] = {}
|
||||
|
||||
def __del__(self) -> None:
|
||||
""" When this object is deleted it will loop through all registered API requests and aborts them """
|
||||
|
||||
try:
|
||||
self.isLoadingChanged.disconnect()
|
||||
self.hasMoreChanged.disconnect()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
self.cleanUpAPIRequest()
|
||||
|
||||
def abortRequest(self, request_id: str) -> None:
|
||||
"""Aborts a single request"""
|
||||
if request_id in self._ongoing_requests and self._ongoing_requests[request_id]:
|
||||
HttpRequestManager.getInstance().abortRequest(self._ongoing_requests[request_id])
|
||||
self._ongoing_requests[request_id] = None
|
||||
|
||||
@pyqtSlot()
|
||||
def cleanUpAPIRequest(self) -> None:
|
||||
for request_id in self._ongoing_requests:
|
||||
self.abortRequest(request_id)
|
||||
|
||||
@pyqtSlot()
|
||||
def updatePackages(self) -> None:
|
||||
""" A Qt slot which will update the List from a source. Actual implementation should be done in the child class"""
|
||||
pass
|
||||
|
||||
@pyqtSlot()
|
||||
def abortUpdating(self) -> None:
|
||||
""" A Qt slot which allows the update process to be aborted. Override this for child classes with async/callback
|
||||
updatePackges methods"""
|
||||
pass
|
||||
|
||||
def reset(self) -> None:
|
||||
""" Resets and clears the list"""
|
||||
self.clear()
|
||||
|
@ -91,3 +131,170 @@ class PackageList(ListModel):
|
|||
""" Indicating if the PackageList should have a Footer visible. For paginated PackageLists
|
||||
:return: ``True`` if a Footer should be displayed in the ListView, e.q.: paginated lists, ``False`` Otherwise"""
|
||||
return self._has_footer
|
||||
|
||||
def getPackageModel(self, package_id: str) -> PackageModel:
|
||||
index = self.find("package", package_id)
|
||||
return self.getItem(index)["package"]
|
||||
|
||||
def _openLicenseDialog(self, package_id: str, license_content: str) -> None:
|
||||
plugin_path = self._plugin_registry.getPluginPath("Marketplace")
|
||||
if plugin_path is None:
|
||||
plugin_path = os.path.dirname(__file__)
|
||||
|
||||
# create a QML component for the license dialog
|
||||
license_dialog_component_path = os.path.join(plugin_path, "resources", "qml", "LicenseDialog.qml")
|
||||
dialog = CuraApplication.getInstance().createQmlComponent(license_dialog_component_path, {
|
||||
"licenseContent": license_content,
|
||||
"packageId": package_id,
|
||||
"handler": self
|
||||
})
|
||||
dialog.show()
|
||||
# place dialog in class such that it does not get remove by garbage collector
|
||||
self._license_dialogs[package_id] = dialog
|
||||
|
||||
@pyqtSlot(str)
|
||||
def onLicenseAccepted(self, package_id: str) -> None:
|
||||
# close dialog
|
||||
dialog = self._license_dialogs.pop(package_id)
|
||||
if dialog is not None:
|
||||
dialog.deleteLater()
|
||||
# install relevant package
|
||||
self._install(package_id)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def onLicenseDeclined(self, package_id: str) -> None:
|
||||
# close dialog
|
||||
dialog = self._license_dialogs.pop(package_id)
|
||||
if dialog is not None:
|
||||
dialog.deleteLater()
|
||||
# reset package card
|
||||
self._package_manager.packageInstallingFailed.emit(package_id)
|
||||
|
||||
def _requestInstall(self, package_id: str, update: bool = False) -> None:
|
||||
package_path = self._to_install[package_id]
|
||||
license_content = self._package_manager.getPackageLicense(package_path)
|
||||
|
||||
if not update and license_content is not None and license_content != "":
|
||||
# If installation is not and update, and the packages contains a license then
|
||||
# open dialog, prompting the using to accept the plugin license
|
||||
self._openLicenseDialog(package_id, license_content)
|
||||
else:
|
||||
# Otherwise continue the installation
|
||||
self._install(package_id, update)
|
||||
|
||||
def _install(self, package_id: str, update: bool = False) -> None:
|
||||
package_path = self._to_install.pop(package_id)
|
||||
to_be_installed = self._package_manager.installPackage(package_path) is not None
|
||||
if not to_be_installed:
|
||||
Logger.warning(f"Could not install {package_id}")
|
||||
return
|
||||
package = self.getPackageModel(package_id)
|
||||
self.subscribeUserToPackage(package_id, str(package.sdk_version))
|
||||
|
||||
def download(self, package_id: str, url: str, update: bool = False) -> None:
|
||||
"""Initiate the download request
|
||||
|
||||
:param package_id: the package identification string
|
||||
:param url: the URL from which the package needs to be obtained
|
||||
:param update: A flag if this is download request is an update process
|
||||
"""
|
||||
|
||||
if url == "":
|
||||
url = f"{PACKAGES_URL}/{package_id}/download"
|
||||
|
||||
def downloadFinished(reply: "QNetworkReply") -> None:
|
||||
self._downloadFinished(package_id, reply, update)
|
||||
|
||||
def downloadError(reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None:
|
||||
self._downloadError(package_id, update, reply, error)
|
||||
|
||||
self._ongoing_requests["download_package"] = HttpRequestManager.getInstance().get(
|
||||
url,
|
||||
scope = self._scope,
|
||||
callback = downloadFinished,
|
||||
error_callback = downloadError
|
||||
)
|
||||
|
||||
def _downloadFinished(self, package_id: str, reply: "QNetworkReply", update: bool = False) -> None:
|
||||
with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file:
|
||||
try:
|
||||
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
|
||||
while bytes_read:
|
||||
temp_file.write(bytes_read)
|
||||
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
|
||||
except IOError as e:
|
||||
Logger.error(f"Failed to write downloaded package to temp file {e}")
|
||||
temp_file.close()
|
||||
self._downloadError(package_id, update)
|
||||
except RuntimeError:
|
||||
# Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling
|
||||
# between de-/constructing Remote or Local PackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object
|
||||
# was deleted when it was still parsing the response
|
||||
temp_file.close()
|
||||
return
|
||||
temp_file.close()
|
||||
self._to_install[package_id] = temp_file.name
|
||||
self._ongoing_requests["download_package"] = None
|
||||
self._requestInstall(package_id, update)
|
||||
|
||||
|
||||
def _downloadError(self, package_id: str, update: bool = False, reply: Optional["QNetworkReply"] = None, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
||||
if reply:
|
||||
reply_string = bytes(reply.readAll()).decode()
|
||||
Logger.error(f"Failed to download package: {package_id} due to {reply_string}")
|
||||
self._package_manager.packageInstallingFailed.emit(package_id)
|
||||
|
||||
def subscribeUserToPackage(self, package_id: str, sdk_version: str) -> None:
|
||||
"""Subscribe the user (if logged in) to the package for a given SDK
|
||||
|
||||
:param package_id: the package identification string
|
||||
:param sdk_version: the SDK version
|
||||
"""
|
||||
if self._account.isLoggedIn:
|
||||
HttpRequestManager.getInstance().put(
|
||||
url = USER_PACKAGES_URL,
|
||||
data = json.dumps({"data": {"package_id": package_id, "sdk_version": sdk_version}}).encode(),
|
||||
scope = self._scope
|
||||
)
|
||||
|
||||
def unsunscribeUserFromPackage(self, package_id: str) -> None:
|
||||
"""Unsubscribe the user (if logged in) from the package
|
||||
|
||||
:param package_id: the package identification string
|
||||
"""
|
||||
if self._account.isLoggedIn:
|
||||
HttpRequestManager.getInstance().delete(url = f"{USER_PACKAGES_URL}/{package_id}", scope = self._scope)
|
||||
|
||||
# --- Handle the manage package buttons ---
|
||||
|
||||
def _connectManageButtonSignals(self, package: PackageModel) -> None:
|
||||
package.installPackageTriggered.connect(self.installPackage)
|
||||
package.uninstallPackageTriggered.connect(self.uninstallPackage)
|
||||
package.updatePackageTriggered.connect(self.updatePackage)
|
||||
|
||||
def installPackage(self, package_id: str, url: str) -> None:
|
||||
"""Install a package from the Marketplace
|
||||
|
||||
:param package_id: the package identification string
|
||||
"""
|
||||
if not self._package_manager.reinstallPackage(package_id):
|
||||
self.download(package_id, url, False)
|
||||
else:
|
||||
package = self.getPackageModel(package_id)
|
||||
self.subscribeUserToPackage(package_id, str(package.sdk_version))
|
||||
|
||||
def uninstallPackage(self, package_id: str) -> None:
|
||||
"""Uninstall a package from the Marketplace
|
||||
|
||||
:param package_id: the package identification string
|
||||
"""
|
||||
self._package_manager.removePackage(package_id)
|
||||
self.unsunscribeUserFromPackage(package_id)
|
||||
|
||||
def updatePackage(self, package_id: str, url: str) -> None:
|
||||
"""Update a package from the Marketplace
|
||||
|
||||
:param package_id: the package identification string
|
||||
"""
|
||||
self._package_manager.removePackage(package_id, force_add = not self._package_manager.isBundledPackage(package_id))
|
||||
self.download(package_id, url, True)
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, QObject
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional
|
||||
from enum import Enum
|
||||
from typing import Any, cast, Dict, List, Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtQml import QQmlEngine
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.CuraPackageManager import CuraPackageManager
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with.
|
||||
from UM.i18n import i18nCatalog # To translate placeholder names if data is not present.
|
||||
from UM.Logger import Logger
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -14,26 +21,28 @@ catalog = i18nCatalog("cura")
|
|||
class PackageModel(QObject):
|
||||
"""
|
||||
Represents a package, containing all the relevant information to be displayed about a package.
|
||||
|
||||
Effectively this behaves like a glorified named tuple, but as a QObject so that its properties can be obtained from
|
||||
QML. The model can also be constructed directly from a response received by the API.
|
||||
"""
|
||||
|
||||
def __init__(self, package_data: Dict[str, Any], installation_status: str, section_title: Optional[str] = None, parent: Optional[QObject] = None) -> None:
|
||||
def __init__(self, package_data: Dict[str, Any], section_title: Optional[str] = None, parent: Optional[QObject] = None) -> None:
|
||||
"""
|
||||
Constructs a new model for a single package.
|
||||
:param package_data: The data received from the Marketplace API about the package to create.
|
||||
:param installation_status: Whether the package is `not_installed`, `installed` or `bundled`.
|
||||
:param section_title: If the packages are to be categorized per section provide the section_title
|
||||
:param parent: The parent QML object that controls the lifetime of this model (normally a PackageList).
|
||||
"""
|
||||
super().__init__(parent)
|
||||
QQmlEngine.setObjectOwnership(self, QQmlEngine.CppOwnership)
|
||||
self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
|
||||
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
|
||||
|
||||
self._package_id = package_data.get("package_id", "UnknownPackageId")
|
||||
self._package_type = package_data.get("package_type", "")
|
||||
self._is_bundled = package_data.get("is_bundled", False)
|
||||
self._icon_url = package_data.get("icon_url", "")
|
||||
self._display_name = package_data.get("display_name", catalog.i18nc("@label:property", "Unknown Package"))
|
||||
tags = package_data.get("tags", [])
|
||||
self._is_checked_by_ultimaker = (self._package_type == "plugin" and "verified" in tags) or (self._package_type == "material" and "certified" in tags)
|
||||
self._is_checked_by_ultimaker = (self._package_type == "plugin" and "verified" in tags) or (
|
||||
self._package_type == "material" and "certified" in 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)
|
||||
|
@ -58,10 +67,40 @@ class PackageModel(QObject):
|
|||
if not self._icon_url or self._icon_url == "":
|
||||
self._icon_url = author_data.get("icon_url", "")
|
||||
|
||||
self._installation_status = installation_status
|
||||
self._can_update = False
|
||||
self._section_title = section_title
|
||||
self.sdk_version = package_data.get("sdk_version_semver", "")
|
||||
# Note that there's a lot more info in the package_data than just these specified here.
|
||||
|
||||
self.enablePackageTriggered.connect(self._plugin_registry.enablePlugin)
|
||||
self.disablePackageTriggered.connect(self._plugin_registry.disablePlugin)
|
||||
|
||||
self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.stateManageButtonChanged)
|
||||
self._package_manager.packageInstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id))
|
||||
self._package_manager.packageUninstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id))
|
||||
self._package_manager.packageInstallingFailed.connect(lambda pkg_id: self._packageInstalled(pkg_id))
|
||||
self._package_manager.packagesWithUpdateChanged.connect(self._processUpdatedPackages)
|
||||
|
||||
self._is_busy = False
|
||||
|
||||
@pyqtSlot()
|
||||
def _processUpdatedPackages(self):
|
||||
self.setCanUpdate(self._package_manager.checkIfPackageCanUpdate(self._package_id))
|
||||
|
||||
def __del__(self):
|
||||
self._package_manager.packagesWithUpdateChanged.disconnect(self._processUpdatedPackages)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, PackageModel):
|
||||
return other == self
|
||||
elif isinstance(other, str):
|
||||
return other == self._package_id
|
||||
else:
|
||||
return False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self._package_id} : {self._package_version} : {self._section_title}>"
|
||||
|
||||
def _findLink(self, subdata: Dict[str, Any], link_type: str) -> str:
|
||||
"""
|
||||
Searches the package data for a link of a certain type.
|
||||
|
@ -175,8 +214,8 @@ class PackageModel(QObject):
|
|||
def packageType(self) -> str:
|
||||
return self._package_type
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def iconUrl(self):
|
||||
@pyqtProperty(str, constant = True)
|
||||
def iconUrl(self) -> str:
|
||||
return self._icon_url
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
|
@ -187,37 +226,33 @@ class PackageModel(QObject):
|
|||
def isCheckedByUltimaker(self):
|
||||
return self._is_checked_by_ultimaker
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def packageVersion(self):
|
||||
@pyqtProperty(str, constant = True)
|
||||
def packageVersion(self) -> str:
|
||||
return self._package_version
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def packageInfoUrl(self):
|
||||
@pyqtProperty(str, constant = True)
|
||||
def packageInfoUrl(self) -> str:
|
||||
return self._package_info_url
|
||||
|
||||
@pyqtProperty(int, constant=True)
|
||||
def downloadCount(self):
|
||||
@pyqtProperty(int, constant = True)
|
||||
def downloadCount(self) -> str:
|
||||
return self._download_count
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def description(self):
|
||||
@pyqtProperty(str, constant = True)
|
||||
def description(self) -> str:
|
||||
return self._description
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def formattedDescription(self) -> str:
|
||||
return self._formatted_description
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def authorName(self):
|
||||
@pyqtProperty(str, constant = True)
|
||||
def authorName(self) -> str:
|
||||
return self._author_name
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def authorInfoUrl(self):
|
||||
return self._author_info_url
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def installationStatus(self) -> str:
|
||||
return self._installation_status
|
||||
def authorInfoUrl(self) -> str:
|
||||
return self._author_info_url
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def sectionTitle(self) -> Optional[str]:
|
||||
|
@ -250,3 +285,99 @@ class PackageModel(QObject):
|
|||
@pyqtProperty(bool, constant = True)
|
||||
def isCompatibleAirManager(self) -> bool:
|
||||
return self._is_compatible_air_manager
|
||||
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def isBundled(self) -> bool:
|
||||
return self._is_bundled
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def downloadURL(self) -> str:
|
||||
return self._download_url
|
||||
|
||||
# --- manage buttons signals ---
|
||||
|
||||
stateManageButtonChanged = pyqtSignal()
|
||||
|
||||
installPackageTriggered = pyqtSignal(str, str)
|
||||
|
||||
uninstallPackageTriggered = pyqtSignal(str)
|
||||
|
||||
updatePackageTriggered = pyqtSignal(str, str)
|
||||
|
||||
enablePackageTriggered = pyqtSignal(str)
|
||||
|
||||
disablePackageTriggered = pyqtSignal(str)
|
||||
|
||||
busyChanged = pyqtSignal()
|
||||
|
||||
@pyqtSlot()
|
||||
def install(self):
|
||||
self.setBusy(True)
|
||||
self.installPackageTriggered.emit(self.packageId, self.downloadURL)
|
||||
|
||||
@pyqtSlot()
|
||||
def update(self):
|
||||
self.setBusy(True)
|
||||
self.updatePackageTriggered.emit(self.packageId, self.downloadURL)
|
||||
|
||||
@pyqtSlot()
|
||||
def uninstall(self):
|
||||
self.uninstallPackageTriggered.emit(self.packageId)
|
||||
|
||||
@pyqtProperty(bool, notify= busyChanged)
|
||||
def busy(self):
|
||||
"""
|
||||
Property indicating that some kind of upgrade is active.
|
||||
"""
|
||||
return self._is_busy
|
||||
|
||||
@pyqtSlot()
|
||||
def enable(self):
|
||||
self.enablePackageTriggered.emit(self.packageId)
|
||||
|
||||
@pyqtSlot()
|
||||
def disable(self):
|
||||
self.disablePackageTriggered.emit(self.packageId)
|
||||
|
||||
def setBusy(self, value: bool):
|
||||
if self._is_busy != value:
|
||||
self._is_busy = value
|
||||
try:
|
||||
self.busyChanged.emit()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def _packageInstalled(self, package_id: str) -> None:
|
||||
if self._package_id != package_id:
|
||||
return
|
||||
self.setBusy(False)
|
||||
try:
|
||||
self.stateManageButtonChanged.emit()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
@pyqtProperty(bool, notify = stateManageButtonChanged)
|
||||
def isInstalled(self) -> bool:
|
||||
return self._package_id in self._package_manager.getAllInstalledPackageIDs()
|
||||
|
||||
@pyqtProperty(bool, notify = stateManageButtonChanged)
|
||||
def isToBeInstalled(self) -> bool:
|
||||
return self._package_id in self._package_manager.getPackagesToInstall()
|
||||
|
||||
@pyqtProperty(bool, notify = stateManageButtonChanged)
|
||||
def isActive(self) -> bool:
|
||||
return not self._package_id in self._plugin_registry.getDisabledPlugins()
|
||||
|
||||
@pyqtProperty(bool, notify = stateManageButtonChanged)
|
||||
def canDowngrade(self) -> bool:
|
||||
"""Flag if the installed package can be downgraded to a bundled version"""
|
||||
return self._package_manager.canDowngrade(self._package_id)
|
||||
|
||||
def setCanUpdate(self, value: bool) -> None:
|
||||
self._can_update = value
|
||||
self.stateManageButtonChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, fset = setCanUpdate, notify = stateManageButtonChanged)
|
||||
def canUpdate(self) -> bool:
|
||||
"""Flag indicating if the package can be updated"""
|
||||
return self._can_update
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization.
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager, HttpRequestData # To request the package list from the API.
|
||||
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope # To request JSON responses from the API.
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API.
|
||||
|
||||
from . import Marketplace # To get the list of packages. Imported this way to prevent circular imports.
|
||||
from .Constants import PACKAGES_URL # To get the list of packages. Imported this way to prevent circular imports.
|
||||
from .PackageList import PackageList
|
||||
from .PackageModel import PackageModel # The contents of this list.
|
||||
|
||||
|
@ -28,23 +25,14 @@ class RemotePackageList(PackageList):
|
|||
def __init__(self, parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._ongoing_request: Optional[HttpRequestData] = None
|
||||
self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
|
||||
|
||||
self._package_type_filter = ""
|
||||
self._requested_search_string = ""
|
||||
self._current_search_string = ""
|
||||
self._request_url = self._initialRequestUrl()
|
||||
self._ongoing_requests["get_packages"] = None
|
||||
self.isLoadingChanged.connect(self._onLoadingChanged)
|
||||
self.isLoadingChanged.emit()
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""
|
||||
When deleting this object, abort the request so that we don't get a callback from it later on a deleted C++
|
||||
object.
|
||||
"""
|
||||
self.abortUpdating()
|
||||
|
||||
@pyqtSlot()
|
||||
def updatePackages(self) -> None:
|
||||
"""
|
||||
|
@ -55,18 +43,13 @@ class RemotePackageList(PackageList):
|
|||
self.setErrorMessage("") # Clear any previous errors.
|
||||
self.setIsLoading(True)
|
||||
|
||||
self._ongoing_request = HttpRequestManager.getInstance().get(
|
||||
self._ongoing_requests["get_packages"] = HttpRequestManager.getInstance().get(
|
||||
self._request_url,
|
||||
scope = self._scope,
|
||||
callback = self._parseResponse,
|
||||
error_callback = self._onError
|
||||
)
|
||||
|
||||
@pyqtSlot()
|
||||
def abortUpdating(self) -> None:
|
||||
HttpRequestManager.getInstance().abortRequest(self._ongoing_request)
|
||||
self._ongoing_request = None
|
||||
|
||||
def reset(self) -> None:
|
||||
self.clear()
|
||||
self._request_url = self._initialRequestUrl()
|
||||
|
@ -113,7 +96,7 @@ class RemotePackageList(PackageList):
|
|||
Get the URL to request the first paginated page with.
|
||||
:return: A URL to request.
|
||||
"""
|
||||
request_url = f"{Marketplace.PACKAGES_URL}?limit={self.ITEMS_PER_PAGE}"
|
||||
request_url = f"{PACKAGES_URL}?limit={self.ITEMS_PER_PAGE}"
|
||||
if self._package_type_filter != "":
|
||||
request_url += f"&package_type={self._package_type_filter}"
|
||||
if self._current_search_string != "":
|
||||
|
@ -134,18 +117,21 @@ class RemotePackageList(PackageList):
|
|||
return
|
||||
|
||||
for package_data in response_data["data"]:
|
||||
installation_status = "installed" if CuraApplication.getInstance().getPackageManager().isUserInstalledPackage(package_data["package_id"]) else "not_installed"
|
||||
package_id = package_data["package_id"]
|
||||
if package_id in self._package_manager.local_packages_ids:
|
||||
continue # We should only show packages which are not already installed
|
||||
try:
|
||||
package = PackageModel(package_data, installation_status, parent = self)
|
||||
package = PackageModel(package_data, parent = self)
|
||||
self._connectManageButtonSignals(package)
|
||||
self.appendItem({"package": package}) # Add it to this list model.
|
||||
except RuntimeError:
|
||||
# Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling
|
||||
# between de-/constructing RemotePackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object
|
||||
# was deleted when it was still parsing the response
|
||||
return
|
||||
continue
|
||||
|
||||
self._request_url = response_data["links"].get("next", "") # Use empty string to signify that there is no next page.
|
||||
self._ongoing_request = None
|
||||
self._ongoing_requests["get_packages"] = None
|
||||
self.setIsLoading(False)
|
||||
self.setHasMore(self._request_url != "")
|
||||
|
||||
|
@ -157,9 +143,9 @@ class RemotePackageList(PackageList):
|
|||
"""
|
||||
if error == QNetworkReply.NetworkError.OperationCanceledError:
|
||||
Logger.debug("Cancelled request for packages.")
|
||||
self._ongoing_request = None
|
||||
self._ongoing_requests["get_packages"] = None
|
||||
return # Don't show an error about this to the user.
|
||||
Logger.error("Could not reach Marketplace server.")
|
||||
self.setErrorMessage(catalog.i18nc("@info:error", "Could not reach Marketplace."))
|
||||
self._ongoing_request = None
|
||||
self._ongoing_requests["get_packages"] = None
|
||||
self.setIsLoading(False)
|
||||
|
|
36
plugins/Marketplace/RestartManager.py
Normal file
36
plugins/Marketplace/RestartManager.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.CuraPackageManager import CuraPackageManager
|
||||
|
||||
|
||||
class RestartManager(QObject):
|
||||
def __init__(self, parent: Optional[QObject] = None) -> None:
|
||||
super().__init__(parent = parent)
|
||||
self._manager: "CuraPackageManager" = CuraApplication.getInstance().getPackageManager()
|
||||
self._plugin_registry: "PluginRegistry" = CuraApplication.getInstance().getPluginRegistry()
|
||||
|
||||
self._manager.installedPackagesChanged.connect(self.checkIfRestartNeeded)
|
||||
self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded)
|
||||
|
||||
self._restart_needed = False
|
||||
|
||||
def checkIfRestartNeeded(self) -> None:
|
||||
if self._manager.hasPackagesToRemoveOrInstall or len(self._plugin_registry.getCurrentSessionActivationChangedPlugins()) > 0:
|
||||
self._restart_needed = True
|
||||
else:
|
||||
self._restart_needed = False
|
||||
self.showRestartNotificationChanged.emit()
|
||||
|
||||
showRestartNotificationChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = showRestartNotificationChanged)
|
||||
def showRestartNotification(self) -> bool:
|
||||
return self._restart_needed
|
91
plugins/Marketplace/resources/qml/LicenseDialog.qml
Normal file
91
plugins/Marketplace/resources/qml/LicenseDialog.qml
Normal file
|
@ -0,0 +1,91 @@
|
|||
//Copyright (c) 2021 Ultimaker B.V.
|
||||
//Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Dialogs 1.1
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.6 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
UM.Dialog
|
||||
{
|
||||
id: licenseDialog
|
||||
title: catalog.i18nc("@button", "Plugin license agreement")
|
||||
minimumWidth: UM.Theme.getSize("license_window_minimum").width
|
||||
minimumHeight: UM.Theme.getSize("license_window_minimum").height
|
||||
width: minimumWidth
|
||||
height: minimumHeight
|
||||
backgroundColor: UM.Theme.getColor("main_background")
|
||||
|
||||
property variant catalog: UM.I18nCatalog { name: "cura" }
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
anchors.fill: parent
|
||||
spacing: UM.Theme.getSize("thick_margin").height
|
||||
|
||||
Row
|
||||
{
|
||||
Layout.fillWidth: true
|
||||
height: childrenRect.height
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
leftPadding: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: icon
|
||||
width: UM.Theme.getSize("marketplace_large_icon").width
|
||||
height: UM.Theme.getSize("marketplace_large_icon").height
|
||||
color: UM.Theme.getColor("text")
|
||||
source: UM.Theme.getIcon("Certificate", "high")
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@text", "Please read and agree with the plugin licence.")
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("large")
|
||||
anchors.verticalCenter: icon.verticalCenter
|
||||
height: UM.Theme.getSize("marketplace_large_icon").height
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
wrapMode: Text.Wrap
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
Cura.ScrollableTextArea
|
||||
{
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
|
||||
textArea.text: licenseContent
|
||||
textArea.readOnly: true
|
||||
}
|
||||
|
||||
}
|
||||
rightButtons:
|
||||
[
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
text: catalog.i18nc("@button", "Accept")
|
||||
onClicked: handler.onLicenseAccepted(packageId)
|
||||
}
|
||||
]
|
||||
|
||||
leftButtons:
|
||||
[
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
text: catalog.i18nc("@button", "Decline")
|
||||
onClicked: handler.onLicenseDeclined(packageId)
|
||||
}
|
||||
]
|
||||
|
||||
onAccepted: handler.onLicenseAccepted(packageId)
|
||||
onRejected: handler.onLicenseDeclined(packageId)
|
||||
onClosing: handler.onLicenseDeclined(packageId)
|
||||
}
|
114
plugins/Marketplace/resources/qml/ManageButton.qml
Normal file
114
plugins/Marketplace/resources/qml/ManageButton.qml
Normal file
|
@ -0,0 +1,114 @@
|
|||
// 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
|
||||
|
||||
Item
|
||||
{
|
||||
id: manageButton
|
||||
property bool button_style: true
|
||||
property string text
|
||||
property bool busy: false
|
||||
property bool confirmed: false
|
||||
|
||||
implicitWidth: childrenRect.width
|
||||
implicitHeight: childrenRect.height
|
||||
|
||||
signal clicked()
|
||||
|
||||
property Component primaryButton: Component
|
||||
{
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
text: manageButton.text
|
||||
onClicked: manageButton.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
property Component secondaryButton: Component
|
||||
{
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
text: manageButton.text
|
||||
onClicked: manageButton.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
property Component busyButton: Component
|
||||
{
|
||||
Item
|
||||
{
|
||||
height: UM.Theme.getSize("action_button").height
|
||||
width: childrenRect.width
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: busyIndicator
|
||||
visible: parent.visible
|
||||
height: UM.Theme.getSize("action_button").height - 2 * UM.Theme.getSize("narrow_margin").height
|
||||
width: height
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
source: UM.Theme.getIcon("Spinner")
|
||||
color: UM.Theme.getColor("primary")
|
||||
|
||||
RotationAnimator
|
||||
{
|
||||
target: busyIndicator
|
||||
running: parent.visible
|
||||
from: 0
|
||||
to: 360
|
||||
loops: Animation.Infinite
|
||||
duration: 2500
|
||||
}
|
||||
}
|
||||
Label
|
||||
{
|
||||
visible: parent.visible
|
||||
anchors.left: busyIndicator.right
|
||||
anchors.leftMargin: UM.Theme.getSize("narrow_margin").width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: manageButton.text
|
||||
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("primary")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Component confirmButton: Component
|
||||
{
|
||||
Item
|
||||
{
|
||||
height: UM.Theme.getSize("action_button").height
|
||||
width: childrenRect.width
|
||||
|
||||
Label
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: manageButton.text
|
||||
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("primary")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader
|
||||
{
|
||||
|
||||
sourceComponent:
|
||||
{
|
||||
if (busy) { return manageButton.busyButton; }
|
||||
else if (confirmed) { return manageButton.confirmButton; }
|
||||
else if (manageButton.button_style) { return manageButton.primaryButton; }
|
||||
else { return manageButton.secondaryButton; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ Packages
|
|||
bannerVisible = false;
|
||||
}
|
||||
searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-plugins-browser"
|
||||
packagesManageableInListView: true
|
||||
|
||||
model: Marketplace.LocalPackageList
|
||||
{
|
||||
|
|
|
@ -8,11 +8,13 @@ import QtQuick.Window 2.2
|
|||
|
||||
import UM 1.2 as UM
|
||||
import Cura 1.6 as Cura
|
||||
import Marketplace 1.0 as Marketplace
|
||||
|
||||
Window
|
||||
{
|
||||
id: marketplaceDialog
|
||||
property variant catalog: UM.I18nCatalog { name: "cura" }
|
||||
property variant restartManager: Marketplace.RestartManager { }
|
||||
|
||||
signal searchStringChanged(string new_search)
|
||||
|
||||
|
@ -106,9 +108,8 @@ Window
|
|||
height: UM.Theme.getSize("button_icon").height + UM.Theme.getSize("default_margin").height
|
||||
spacing: UM.Theme.getSize("thin_margin").width
|
||||
|
||||
Rectangle
|
||||
Item
|
||||
{
|
||||
color: "transparent"
|
||||
Layout.preferredHeight: parent.height
|
||||
Layout.preferredWidth: searchBar.visible ? UM.Theme.getSize("thin_margin").width : 0
|
||||
Layout.fillWidth: ! searchBar.visible
|
||||
|
@ -228,4 +229,56 @@ Window
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width
|
||||
color: UM.Theme.getColor("primary")
|
||||
visible: restartManager.showRestartNotification
|
||||
anchors
|
||||
{
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
RowLayout
|
||||
{
|
||||
anchors
|
||||
{
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
margins: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: bannerIcon
|
||||
source: UM.Theme.getIcon("Plugin")
|
||||
|
||||
color: UM.Theme.getColor("primary_button_text")
|
||||
implicitWidth: UM.Theme.getSize("banner_icon_size").width
|
||||
implicitHeight: UM.Theme.getSize("banner_icon_size").height
|
||||
}
|
||||
Text
|
||||
{
|
||||
color: UM.Theme.getColor("primary_button_text")
|
||||
text: catalog.i18nc("@button", "In order to use the package you will need to restart Cura")
|
||||
font: UM.Theme.getFont("default")
|
||||
renderType: Text.NativeRendering
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: quitButton
|
||||
text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName)
|
||||
onClicked:
|
||||
{
|
||||
marketplaceDialog.hide();
|
||||
CuraApplication.closeApplication();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ Packages
|
|||
bannerVisible = false;
|
||||
}
|
||||
searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/materials?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-materials-browser"
|
||||
packagesManageableInListView: false
|
||||
|
||||
model: Marketplace.RemotePackageList
|
||||
{
|
||||
|
|
|
@ -10,597 +10,85 @@ import Cura 1.6 as Cura
|
|||
|
||||
Rectangle
|
||||
{
|
||||
property var packageData
|
||||
property bool expanded: false
|
||||
property alias packageData: packageCardHeader.packageData
|
||||
property alias manageableInListView: packageCardHeader.showManageButtons
|
||||
|
||||
height: childrenRect.height
|
||||
color: UM.Theme.getColor("main_background")
|
||||
radius: UM.Theme.getSize("default_radius").width
|
||||
|
||||
states:
|
||||
[
|
||||
State
|
||||
{
|
||||
name: "Folded"
|
||||
when: !expanded
|
||||
PropertyChanges
|
||||
{
|
||||
target: shortDescription
|
||||
visible: true
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
target: downloadCount
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
target: extendedDescription
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
State
|
||||
{
|
||||
name: "Expanded"
|
||||
when: expanded
|
||||
PropertyChanges
|
||||
{
|
||||
target: shortDescription
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
target: downloadCount
|
||||
visible: true
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
target: extendedDescription
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Column
|
||||
PackageCardHeader
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
spacing: 0
|
||||
id: packageCardHeader
|
||||
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("card").height
|
||||
id: shortDescription
|
||||
|
||||
Image
|
||||
{
|
||||
id: packageItem
|
||||
anchors
|
||||
{
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
margins: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
width: UM.Theme.getSize("card_icon").width
|
||||
height: width
|
||||
|
||||
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
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
|
||||
Label
|
||||
{
|
||||
text: packageData.displayName
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
verticalAlignment: Text.AlignTop
|
||||
}
|
||||
|
||||
Control
|
||||
{
|
||||
Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width
|
||||
Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").height
|
||||
|
||||
enabled: packageData.isCheckedByUltimaker
|
||||
visible: packageData.isCheckedByUltimaker
|
||||
|
||||
Cura.ToolTip
|
||||
{
|
||||
tooltipText:
|
||||
{
|
||||
switch(packageData.packageType)
|
||||
{
|
||||
case "plugin": return catalog.i18nc("@info", "Ultimaker Verified Plug-in");
|
||||
case "material": return catalog.i18nc("@info", "Ultimaker Certified Material");
|
||||
default: return catalog.i18nc("@info", "Ultimaker Verified Package");
|
||||
}
|
||||
}
|
||||
visible: parent.hovered
|
||||
targetPoint: Qt.point(0, Math.round(parent.y + parent.height / 4))
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
anchors.fill: parent
|
||||
color: UM.Theme.getColor("action_button_hovered")
|
||||
radius: width
|
||||
UM.RecolorImage
|
||||
{
|
||||
anchors.fill: parent
|
||||
color: UM.Theme.getColor("primary")
|
||||
source: packageData.packageType == "plugin" ? UM.Theme.getIcon("CheckCircle") : UM.Theme.getIcon("Certified")
|
||||
}
|
||||
}
|
||||
|
||||
//NOTE: Can we link to something here? (Probably a static link explaining what verified is):
|
||||
// onClicked: Qt.openUrlExternally( XXXXXX )
|
||||
}
|
||||
|
||||
Control
|
||||
{
|
||||
Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width
|
||||
Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").height
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
enabled: false // remove!
|
||||
visible: false // replace packageInfo.XXXXXX
|
||||
// TODO: waiting for materials card implementation
|
||||
|
||||
Cura.ToolTip
|
||||
{
|
||||
tooltipText: "" // TODO
|
||||
visible: parent.hovered
|
||||
}
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
anchors.fill: parent
|
||||
|
||||
color: UM.Theme.getColor("primary")
|
||||
source: UM.Theme.getIcon("CheckCircle") // TODO
|
||||
}
|
||||
|
||||
// onClicked: Qt.openUrlExternally( XXXXXX ) // TODO
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: packageVersionLabel
|
||||
text: packageData.packageVersion
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
id: externalLinkButton
|
||||
|
||||
// For some reason if i set padding, they don't match up. If i set all of them explicitly, it does work?
|
||||
leftPadding: UM.Theme.getSize("narrow_margin").width
|
||||
rightPadding: UM.Theme.getSize("narrow_margin").width
|
||||
topPadding: UM.Theme.getSize("narrow_margin").width
|
||||
bottomPadding: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
|
||||
Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
|
||||
contentItem: UM.RecolorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("LinkExternal")
|
||||
color: UM.Theme.getColor("icon")
|
||||
implicitWidth: UM.Theme.getSize("card_tiny_icon").width
|
||||
implicitHeight: UM.Theme.getSize("card_tiny_icon").height
|
||||
}
|
||||
|
||||
background: Rectangle
|
||||
{
|
||||
color: externalLinkButton.hovered ? UM.Theme.getColor("action_button_hovered"): "transparent"
|
||||
radius: externalLinkButton.width / 2
|
||||
}
|
||||
onClicked: Qt.openUrlExternally(packageData.authorInfoUrl)
|
||||
}
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
id: shortDescription
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.fillHeight: true
|
||||
|
||||
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
|
||||
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:
|
||||
{
|
||||
if(truncated && line.isLast)
|
||||
{
|
||||
let max_line_width = parent.width - readMoreButton.width - fontMetrics.advanceWidth("… ") - 2 * 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Label
|
||||
{
|
||||
id: tripleDotLabel
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: descriptionLabel.lastLineWidth
|
||||
anchors.bottom: descriptionLabel.bottom
|
||||
|
||||
text: "… "
|
||||
font: descriptionLabel.font
|
||||
color: descriptionLabel.color
|
||||
visible: descriptionLabel.truncated && descriptionLabel.text !== ""
|
||||
}
|
||||
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 && descriptionLabel.text !== ""
|
||||
enabled: visible
|
||||
leftPadding: UM.Theme.getSize("default_margin").width
|
||||
rightPadding: UM.Theme.getSize("wide_margin").width
|
||||
textFont: descriptionLabel.font
|
||||
isIconOnRightSide: true
|
||||
|
||||
onClicked: Qt.openUrlExternally(packageData.packageInfoUrl)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
|
||||
spacing: UM.Theme.getSize("narrow_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")
|
||||
textColor: UM.Theme.getColor("text") // override normal link color
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||
isIconOnRightSide: true
|
||||
|
||||
onClicked: Qt.openUrlExternally(packageData.authorInfoUrl)
|
||||
}
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: disableButton
|
||||
Layout.alignment: Qt.AlignTop
|
||||
text: catalog.i18nc("@button", "Disable")
|
||||
visible: false // not functional right now, also only when unfolding and required
|
||||
}
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: uninstallButton
|
||||
Layout.alignment: Qt.AlignTop
|
||||
text: catalog.i18nc("@button", "Uninstall")
|
||||
visible: false // not functional right now, also only when unfolding and required
|
||||
}
|
||||
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: installButton
|
||||
Layout.alignment: Qt.AlignTop
|
||||
text: catalog.i18nc("@button", "Update") // OR Download, if new!
|
||||
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
|
||||
anchors.fill: parent
|
||||
|
||||
Label
|
||||
{
|
||||
width: parent.width - parent.padding * 2
|
||||
id: descriptionLabel
|
||||
width: parent.width
|
||||
property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision.
|
||||
|
||||
text: catalog.i18nc("@header", "Description")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
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")
|
||||
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")
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: Text.RichText
|
||||
elide: Text.ElideRight
|
||||
visible: text !== ""
|
||||
|
||||
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
|
||||
onLineLaidOut:
|
||||
{
|
||||
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
|
||||
if(truncated && line.isLast)
|
||||
{
|
||||
width: compatiblePrinterColumn.width
|
||||
|
||||
text: modelData
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
let max_line_width = parent.width - readMoreButton.width - fontMetrics.advanceWidth("… ") - 2 * 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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
Label
|
||||
{
|
||||
id: compatibleSupportMaterialColumn
|
||||
width: parent.width - parent.padding * 2
|
||||
id: tripleDotLabel
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: descriptionLabel.lastLineWidth
|
||||
anchors.bottom: descriptionLabel.bottom
|
||||
|
||||
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
|
||||
}
|
||||
text: "… "
|
||||
font: descriptionLabel.font
|
||||
color: descriptionLabel.color
|
||||
visible: descriptionLabel.truncated && descriptionLabel.text !== ""
|
||||
}
|
||||
|
||||
Column
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
width: parent.width - parent.padding * 2
|
||||
id: readMoreButton
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: descriptionLabel.bottom
|
||||
height: fontMetrics.height //Height of a single line.
|
||||
|
||||
visible: packageData.packageType === "material"
|
||||
spacing: 0
|
||||
text: catalog.i18nc("@info", "Read more")
|
||||
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||
|
||||
Label
|
||||
{
|
||||
width: parent.width
|
||||
visible: descriptionLabel.truncated && descriptionLabel.text !== ""
|
||||
enabled: visible
|
||||
leftPadding: UM.Theme.getSize("default_margin").width
|
||||
rightPadding: UM.Theme.getSize("wide_margin").width
|
||||
textFont: descriptionLabel.font
|
||||
isIconOnRightSide: true
|
||||
|
||||
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)
|
||||
}
|
||||
onClicked: Qt.openUrlExternally(packageData.packageInfoUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
213
plugins/Marketplace/resources/qml/PackageCardHeader.qml
Normal file
213
plugins/Marketplace/resources/qml/PackageCardHeader.qml
Normal file
|
@ -0,0 +1,213 @@
|
|||
// 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
|
||||
|
||||
// As both the PackageCard and Package contain similar components; a package icon, title, author bar. These components
|
||||
// are combined into the reusable "PackageCardHeader" component
|
||||
Item
|
||||
{
|
||||
default property alias contents: contentItem.children;
|
||||
|
||||
property var packageData
|
||||
property bool showManageButtons: false
|
||||
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("card").height
|
||||
|
||||
// card icon
|
||||
Image
|
||||
{
|
||||
id: packageItem
|
||||
anchors
|
||||
{
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
margins: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
width: UM.Theme.getSize("card_icon").width
|
||||
height: width
|
||||
|
||||
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("default_margin").width
|
||||
top: parent.top
|
||||
topMargin: UM.Theme.getSize("narrow_margin").height
|
||||
}
|
||||
height: packageItem.height + packageItem.anchors.margins * 2
|
||||
|
||||
// Title row.
|
||||
RowLayout
|
||||
{
|
||||
id: titleBar
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
|
||||
Label
|
||||
{
|
||||
text: packageData.displayName
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
verticalAlignment: Text.AlignTop
|
||||
}
|
||||
VerifiedIcon
|
||||
{
|
||||
enabled: packageData.isCheckedByUltimaker
|
||||
visible: packageData.isCheckedByUltimaker
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: packageVersionLabel
|
||||
text: packageData.packageVersion
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
id: externalLinkButton
|
||||
|
||||
// For some reason if i set padding, they don't match up. If i set all of them explicitly, it does work?
|
||||
leftPadding: UM.Theme.getSize("narrow_margin").width
|
||||
rightPadding: UM.Theme.getSize("narrow_margin").width
|
||||
topPadding: UM.Theme.getSize("narrow_margin").width
|
||||
bottomPadding: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
|
||||
Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
|
||||
contentItem: UM.RecolorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("LinkExternal")
|
||||
color: UM.Theme.getColor("icon")
|
||||
implicitWidth: UM.Theme.getSize("card_tiny_icon").width
|
||||
implicitHeight: UM.Theme.getSize("card_tiny_icon").height
|
||||
}
|
||||
|
||||
background: Rectangle
|
||||
{
|
||||
color: externalLinkButton.hovered ? UM.Theme.getColor("action_button_hovered"): "transparent"
|
||||
radius: externalLinkButton.width / 2
|
||||
}
|
||||
onClicked: Qt.openUrlExternally(packageData.authorInfoUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// When a package Card companent is created and children are provided to it they are rendered here
|
||||
Item {
|
||||
id: contentItem
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: parent.width
|
||||
}
|
||||
|
||||
// Author and action buttons.
|
||||
RowLayout
|
||||
{
|
||||
id: authorAndActionButton
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
|
||||
spacing: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
// label "By"
|
||||
Label
|
||||
{
|
||||
id: authorBy
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
text: catalog.i18nc("@label", "By")
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
// clickable author name
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: authorBy.height
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
text: packageData.authorName
|
||||
textFont: UM.Theme.getFont("default_bold")
|
||||
textColor: UM.Theme.getColor("text") // override normal link color
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||
isIconOnRightSide: true
|
||||
|
||||
onClicked: Qt.openUrlExternally(packageData.authorInfoUrl)
|
||||
}
|
||||
|
||||
ManageButton
|
||||
{
|
||||
id: enableManageButton
|
||||
visible: showManageButtons && packageData.isInstalled && !packageData.isToBeInstalled && packageData.packageType != "material"
|
||||
enabled: !packageData.busy
|
||||
|
||||
button_style: !packageData.isActive
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
text: button_style ? catalog.i18nc("@button", "Enable") : catalog.i18nc("@button", "Disable")
|
||||
|
||||
onClicked: packageData.isActive ? packageData.disable(): packageData.enable()
|
||||
}
|
||||
|
||||
ManageButton
|
||||
{
|
||||
id: installManageButton
|
||||
visible: showManageButtons && (packageData.canDowngrade || !packageData.isBundled)
|
||||
enabled: !packageData.busy
|
||||
busy: packageData.busy
|
||||
button_style: !(packageData.isInstalled || packageData.isToBeInstalled)
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
text:
|
||||
{
|
||||
if (packageData.canDowngrade)
|
||||
{
|
||||
if (busy) { return catalog.i18nc("@button", "Downgrading..."); }
|
||||
else { return catalog.i18nc("@button", "Downgrade"); }
|
||||
}
|
||||
if (!(packageData.isInstalled || packageData.isToBeInstalled))
|
||||
{
|
||||
if (busy) { return catalog.i18nc("@button", "Installing..."); }
|
||||
else { return catalog.i18nc("@button", "Install"); }
|
||||
}
|
||||
else
|
||||
{
|
||||
return catalog.i18nc("@button", "Uninstall");
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: packageData.isInstalled || packageData.isToBeInstalled ? packageData.uninstall(): packageData.install()
|
||||
}
|
||||
|
||||
ManageButton
|
||||
{
|
||||
id: updateManageButton
|
||||
visible: showManageButtons && packageData.canUpdate
|
||||
enabled: !packageData.busy
|
||||
busy: packageData.busy
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
text: busy ? catalog.i18nc("@button", "Updating..."): catalog.i18nc("@button", "Update")
|
||||
|
||||
onClicked: packageData.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -74,11 +74,11 @@ Item
|
|||
|
||||
clip: true //Need to clip, not for the bottom (which is off the window) but for the top (which would overlap the header).
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
contentHeight: expandedPackageCard.height + UM.Theme.getSize("default_margin").height * 2
|
||||
contentHeight: packagePage.height + UM.Theme.getSize("default_margin").height * 2
|
||||
|
||||
PackageCard
|
||||
PackagePage
|
||||
{
|
||||
id: expandedPackageCard
|
||||
id: packagePage
|
||||
anchors
|
||||
{
|
||||
left: parent.left
|
||||
|
@ -90,7 +90,6 @@ Item
|
|||
}
|
||||
|
||||
packageData: detailPage.packageData
|
||||
expanded: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
295
plugins/Marketplace/resources/qml/PackagePage.qml
Normal file
295
plugins/Marketplace/resources/qml/PackagePage.qml
Normal file
|
@ -0,0 +1,295 @@
|
|||
// 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
|
||||
{
|
||||
id: root
|
||||
property alias packageData: packageCardHeader.packageData
|
||||
|
||||
height: childrenRect.height
|
||||
color: UM.Theme.getColor("main_background")
|
||||
radius: UM.Theme.getSize("default_radius").width
|
||||
|
||||
Column
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
spacing: 0
|
||||
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("card").height
|
||||
|
||||
PackageCardHeader
|
||||
{
|
||||
id: packageCardHeader
|
||||
showManageButtons: true
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
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
|
||||
|
||||
source: UM.Theme.getIcon("Download")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
anchors.verticalCenter: downloadsIcon.verticalCenter
|
||||
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("default")
|
||||
text: packageData.downloadCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FontMetrics
|
||||
{
|
||||
id: fontMetrics
|
||||
font: UM.Theme.getFont("default")
|
||||
}
|
||||
}
|
|
@ -19,11 +19,12 @@ ListView
|
|||
property string bannerText
|
||||
property string bannerReadMoreUrl
|
||||
property var onRemoveBanner
|
||||
property bool packagesManageableInListView
|
||||
|
||||
clip: true
|
||||
|
||||
Component.onCompleted: model.updatePackages()
|
||||
Component.onDestruction: model.abortUpdating()
|
||||
Component.onDestruction: model.cleanUpAPIRequest()
|
||||
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
|
||||
|
@ -35,15 +36,13 @@ ListView
|
|||
|
||||
color: UM.Theme.getColor("detail_background")
|
||||
|
||||
required property string section
|
||||
|
||||
Label
|
||||
{
|
||||
id: sectionHeaderText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
|
||||
text: parent.section
|
||||
text: section
|
||||
font: UM.Theme.getFont("large")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
@ -82,6 +81,7 @@ ListView
|
|||
|
||||
PackageCard
|
||||
{
|
||||
manageableInListView: packages.packagesManageableInListView
|
||||
packageData: model.package
|
||||
width: parent.width - UM.Theme.getSize("default_margin").width - UM.Theme.getSize("narrow_margin").width
|
||||
color: cardMouseArea.containsMouse ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("main_background")
|
||||
|
@ -230,4 +230,3 @@ ListView
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ Packages
|
|||
bannerVisible = false;
|
||||
}
|
||||
searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-plugins-browser"
|
||||
packagesManageableInListView: false
|
||||
|
||||
model: Marketplace.RemotePackageList
|
||||
{
|
||||
|
|
45
plugins/Marketplace/resources/qml/VerifiedIcon.qml
Normal file
45
plugins/Marketplace/resources/qml/VerifiedIcon.qml
Normal file
|
@ -0,0 +1,45 @@
|
|||
// 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
|
||||
Control
|
||||
{
|
||||
implicitWidth: UM.Theme.getSize("card_tiny_icon").width
|
||||
implicitHeight: UM.Theme.getSize("card_tiny_icon").height
|
||||
|
||||
Cura.ToolTip
|
||||
{
|
||||
tooltipText:
|
||||
{
|
||||
switch(packageData.packageType)
|
||||
{
|
||||
case "plugin": return catalog.i18nc("@info", "Ultimaker Verified Plug-in");
|
||||
case "material": return catalog.i18nc("@info", "Ultimaker Certified Material");
|
||||
default: return catalog.i18nc("@info", "Ultimaker Verified Package");
|
||||
}
|
||||
}
|
||||
visible: parent.hovered
|
||||
targetPoint: Qt.point(0, Math.round(parent.y + parent.height / 4))
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
anchors.fill: parent
|
||||
color: UM.Theme.getColor("action_button_hovered")
|
||||
radius: width
|
||||
UM.RecolorImage
|
||||
{
|
||||
anchors.fill: parent
|
||||
color: UM.Theme.getColor("primary")
|
||||
source: packageData.packageType == "plugin" ? UM.Theme.getIcon("CheckCircle") : UM.Theme.getIcon("Certified")
|
||||
}
|
||||
}
|
||||
|
||||
//NOTE: Can we link to something here? (Probably a static link explaining what verified is):
|
||||
// onClicked: Qt.openUrlExternally( XXXXXX )
|
||||
}
|
|
@ -38,7 +38,7 @@ class CloudApiClient:
|
|||
def _subscribe(self, package_id: str) -> None:
|
||||
"""You probably don't want to use this directly. All installed packages will be automatically subscribed."""
|
||||
|
||||
Logger.debug("Subscribing to {}", package_id)
|
||||
Logger.debug("Subscribing to using the Old Toolbox {}", package_id)
|
||||
data = "{\"data\": {\"package_id\": \"%s\", \"sdk_version\": \"%s\"}}" % (package_id, CloudApiModel.sdk_version)
|
||||
HttpRequestManager.getInstance().put(
|
||||
url = CloudApiModel.api_url_user_packages,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Toolbox is released under the terms of the LGPLv3 or higher.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import json
|
||||
import os
|
||||
|
@ -634,8 +634,8 @@ class Toolbox(QObject, Extension):
|
|||
self._models[request_type].setFilter({"tags": "generic"})
|
||||
elif request_type == "updates":
|
||||
# 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[request_type]])
|
||||
self._package_manager.setPackagesWithUpdate(packages)
|
||||
packages = self._server_response_data[request_type]
|
||||
self._package_manager.setPackagesWithUpdate({p['package_id'] for p in packages})
|
||||
|
||||
self.metadataChanged.emit()
|
||||
|
||||
|
|
3
resources/themes/cura-light/icons/default/Spinner.svg
Normal file
3
resources/themes/cura-light/icons/default/Spinner.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 12C22 14.6522 20.9464 17.1957 19.0711 19.0711C17.1957 20.9464 14.6522 22 12 22C9.34784 22 6.8043 20.9464 4.92893 19.0711C3.05357 17.1957 2 14.6522 2 12H4C4 13.5823 4.46919 15.129 5.34824 16.4446C6.22729 17.7602 7.47672 18.7855 8.93853 19.391C10.4003 19.9965 12.0089 20.155 13.5607 19.8463C15.1126 19.5376 16.538 18.7757 17.6569 17.6569C18.7757 16.538 19.5376 15.1126 19.8463 13.5607C20.155 12.0089 19.9965 10.4003 19.391 8.93853C18.7855 7.47672 17.7602 6.22729 16.4446 5.34824C15.129 4.46919 13.5823 4 12 4V2C14.6508 2.00436 17.1918 3.05933 19.0662 4.93375C20.9407 6.80817 21.9956 9.34917 22 12Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 703 B |
3
resources/themes/cura-light/icons/high/Certificate.svg
Normal file
3
resources/themes/cura-light/icons/high/Certificate.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M39.0176 13.0001H9.01764V11.0001H39.0176V13.0001ZM24.0176 16.0001H9.01764V18.0001H24.0176V16.0001ZM20.0176 21.0001H9.01764V23.0001H20.0176V21.0001ZM12.0176 26.0001H9.01764V28.0001H12.0176V26.0001ZM43.9576 5.06006V36.9401H35.6976V43.3901L31.6976 42.0501L27.6976 43.3901V36.9401H4.07764V5.06006H43.9576ZM33.6976 33.1401L31.7276 33.6801L29.6976 33.1501V40.6101L31.6976 39.9501L33.6976 40.6101V33.1401ZM36.2876 28.9501L36.9776 26.3001L36.2576 23.6601L34.3176 21.7301L31.6676 21.0401L29.0276 21.7601L27.0976 23.7001L26.4076 26.3501L27.1276 28.9901L29.0676 30.9201L31.7176 31.6101L34.3576 30.8901L36.2876 28.9501ZM42.0776 6.94006H5.95764V35.0601H27.6976V32.3701L25.3376 30.0301L24.3376 26.3601L25.3076 22.6701L27.9876 19.9701L31.6576 18.9601L35.3476 19.9301L38.0476 22.6201L39.0576 26.2901L38.0876 29.9801L35.6976 32.3801V35.0601H42.0776V6.94006Z" fill="#000E1A"/>
|
||||
</svg>
|
After Width: | Height: | Size: 959 B |
|
@ -179,7 +179,7 @@
|
|||
"lining": [192, 193, 194, 255],
|
||||
"viewport_overlay": [246, 246, 246, 255],
|
||||
|
||||
"primary": [50, 130, 255, 255],
|
||||
"primary": [25, 110, 240, 255],
|
||||
"primary_shadow": [64, 47, 205, 255],
|
||||
"primary_hover": [48, 182, 231, 255],
|
||||
"primary_text": [255, 255, 255, 255],
|
||||
|
@ -554,7 +554,7 @@
|
|||
"standard_list_lineheight": [1.5, 1.5],
|
||||
"standard_arrow": [1.0, 1.0],
|
||||
|
||||
"card": [25.0, 8.75],
|
||||
"card": [25.0, 10],
|
||||
"card_icon": [6.0, 6.0],
|
||||
"card_tiny_icon": [1.5, 1.5],
|
||||
|
||||
|
@ -686,6 +686,8 @@
|
|||
"welcome_wizard_content_image_big": [18, 15],
|
||||
"welcome_wizard_cloud_content_image": [4, 4],
|
||||
|
||||
"banner_icon_size": [2.0, 2.0]
|
||||
"banner_icon_size": [2.0, 2.0],
|
||||
|
||||
"marketplace_large_icon": [4.0, 4.0]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue