Fix merge conflict

This commit is contained in:
ChrisTerBeke 2018-12-13 11:48:35 +01:00
commit 36f5150d94
73 changed files with 1758 additions and 474 deletions

1
.gitignore vendored
View file

@ -42,7 +42,6 @@ plugins/cura-siemensnx-plugin
plugins/CuraBlenderPlugin
plugins/CuraCloudPlugin
plugins/CuraDrivePlugin
plugins/CuraDrive
plugins/CuraLiveScriptingPlugin
plugins/CuraOpenSCADPlugin
plugins/CuraPrintProfileCreator

View file

@ -6,6 +6,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
from UM.i18n import i18nCatalog
from UM.Message import Message
from cura import CuraConstants
from cura.OAuth2.AuthorizationService import AuthorizationService
from cura.OAuth2.Models import OAuth2Settings
@ -37,7 +38,7 @@ class Account(QObject):
self._logged_in = False
self._callback_port = 32118
self._oauth_root = "https://account-staging.ultimaker.com"
self._oauth_root = CuraConstants.CuraCloudAccountAPIRoot
self._oauth_settings = OAuth2Settings(
OAUTH_SERVER_URL= self._oauth_root,

View file

@ -115,6 +115,8 @@ from cura.ObjectsModel import ObjectsModel
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
from cura import CuraConstants
from UM.FlameProfiler import pyqtSlot
from UM.Decorators import override
@ -127,15 +129,6 @@ if TYPE_CHECKING:
numpy.seterr(all = "ignore")
try:
from cura.CuraVersion import CuraAppDisplayName, CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion # type: ignore
except ImportError:
CuraAppDisplayName = "Ultimaker Cura"
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
CuraBuildType = ""
CuraDebugMode = False
CuraSDKVersion = "5.0.0"
class CuraApplication(QtApplication):
# SettingVersion represents the set of settings available in the machine/extruder definitions.
@ -162,11 +155,11 @@ class CuraApplication(QtApplication):
def __init__(self, *args, **kwargs):
super().__init__(name = "cura",
app_display_name = CuraAppDisplayName,
version = CuraVersion,
api_version = CuraSDKVersion,
buildtype = CuraBuildType,
is_debug_mode = CuraDebugMode,
app_display_name = CuraConstants.CuraAppDisplayName,
version = CuraConstants.CuraVersion,
api_version = CuraConstants.CuraSDKVersion,
buildtype = CuraConstants.CuraBuildType,
is_debug_mode = CuraConstants.CuraDebugMode,
tray_icon_name = "cura-icon-32.png",
**kwargs)
@ -936,7 +929,7 @@ class CuraApplication(QtApplication):
engine.rootContext().setContextProperty("CuraApplication", self)
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
engine.rootContext().setContextProperty("CuraSDKVersion", CuraSDKVersion)
engine.rootContext().setContextProperty("CuraSDKVersion", CuraConstants.CuraSDKVersion)
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")

60
cura/CuraConstants.py Normal file
View file

@ -0,0 +1,60 @@
#
# This file contains all constant values in Cura
#
# -------------
# Cura Versions
# -------------
DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
DEFAULT_CURA_VERSION = "master"
DEFAULT_CURA_BUILD_TYPE = ""
DEFAULT_CURA_DEBUG_MODE = False
DEFAULT_CURA_SDK_VERSION = "5.0.0"
try:
from cura.CuraVersion import CuraAppDisplayName # type: ignore
except ImportError:
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
try:
from cura.CuraVersion import CuraVersion # type: ignore
except ImportError:
CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value]
try:
from cura.CuraVersion import CuraBuildType # type: ignore
except ImportError:
CuraBuildType = DEFAULT_CURA_BUILD_TYPE
try:
from cura.CuraVersion import CuraDebugMode # type: ignore
except ImportError:
CuraDebugMode = DEFAULT_CURA_DEBUG_MODE
try:
from cura.CuraVersion import CuraSDKVersion # type: ignore
except ImportError:
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
# ---------
# Cloud API
# ---------
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
DEFAULT_CLOUD_API_VERSION = "1" # type: str
DEFAULT_CLOUD_ACCOUNT_API_ROOT = "https://account.ultimaker.com" # type: str
try:
from cura.CuraVersion import CuraCloudAPIRoot # type: ignore
except ImportError:
CuraCloudAPIRoot = DEFAULT_CLOUD_API_ROOT
try:
from cura.CuraVersion import CuraCloudAPIVersion # type: ignore
except ImportError:
CuraCloudAPIVersion = DEFAULT_CLOUD_API_VERSION
try:
from cura.CuraVersion import CuraCloudAccountAPIRoot # type: ignore
except ImportError:
CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT

View file

@ -81,9 +81,14 @@ class AuthorizationHelpers:
# \param access_token: The encoded JWT token.
# \return: Dict containing some profile data.
def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
"Authorization": "Bearer {}".format(access_token)
})
try:
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
"Authorization": "Bearer {}".format(access_token)
})
except ConnectionError:
# Connection was suddenly dropped. Nothing we can do about that.
Logger.logException("e", "Something failed while attempting to parse the JWT token")
return None
if token_request.status_code not in (200, 201):
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
return None

View file

@ -0,0 +1,14 @@
# Copyright (c) 2017 Ultimaker B.V.
import os
is_testing = os.getenv('ENV_NAME', "development") == "testing"
# Only load the whole plugin when not running tests as __init__.py is automatically loaded by PyTest
if not is_testing:
from .src.DrivePluginExtension import DrivePluginExtension
def getMetaData():
return {}
def register(app):
return {"extension": DrivePluginExtension(app)}

View file

@ -0,0 +1,8 @@
{
"name": "Cura Backups",
"author": "Ultimaker B.V.",
"description": "Backup and restore your configuration.",
"version": "1.2.0",
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -0,0 +1,185 @@
# Copyright (c) 2017 Ultimaker B.V.
import base64
import hashlib
from datetime import datetime
from tempfile import NamedTemporaryFile
from typing import Any, Optional, List, Dict
import requests
from UM.Logger import Logger
from UM.Message import Message
from UM.Signal import Signal
from .UploadBackupJob import UploadBackupJob
from .Settings import Settings
class DriveApiService:
"""
The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling.
"""
GET_BACKUPS_URL = "{}/backups".format(Settings.DRIVE_API_URL)
PUT_BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
DELETE_BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
# Emit signal when restoring backup started or finished.
onRestoringStateChanged = Signal()
# Emit signal when creating backup started or finished.
onCreatingStateChanged = Signal()
def __init__(self, cura_api) -> None:
"""Create a new instance of the Drive API service and set the cura_api object."""
self._cura_api = cura_api
def getBackups(self) -> List[Dict[str, Any]]:
"""Get all backups from the API."""
access_token = self._cura_api.account.accessToken
if not access_token:
Logger.log("w", "Could not get access token.")
return []
backup_list_request = requests.get(self.GET_BACKUPS_URL, headers={
"Authorization": "Bearer {}".format(access_token)
})
if backup_list_request.status_code > 299:
Logger.log("w", "Could not get backups list from remote: %s", backup_list_request.text)
Message(Settings.translatable_messages["get_backups_error"], title = Settings.MESSAGE_TITLE,
lifetime = 10).show()
return []
return backup_list_request.json()["data"]
def createBackup(self) -> None:
"""Create a backup and upload it to CuraDrive cloud storage."""
self.onCreatingStateChanged.emit(is_creating=True)
# Create the backup.
backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup()
if not backup_zip_file or not backup_meta_data:
self.onCreatingStateChanged.emit(is_creating=False, error_message="Could not create backup.")
return
# Create an upload entry for the backup.
timestamp = datetime.now().isoformat()
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
backup_upload_url = self._requestBackupUpload(backup_meta_data, len(backup_zip_file))
if not backup_upload_url:
self.onCreatingStateChanged.emit(is_creating=False, error_message="Could not upload backup.")
return
# Upload the backup to storage.
upload_backup_job = UploadBackupJob(backup_upload_url, backup_zip_file)
upload_backup_job.finished.connect(self._onUploadFinished)
upload_backup_job.start()
def _onUploadFinished(self, job: "UploadBackupJob") -> None:
"""
Callback handler for the upload job.
:param job: The executed job.
"""
if job.backup_upload_error_message != "":
# If the job contains an error message we pass it along so the UI can display it.
self.onCreatingStateChanged.emit(is_creating=False, error_message=job.backup_upload_error_message)
else:
self.onCreatingStateChanged.emit(is_creating=False)
def restoreBackup(self, backup: Dict[str, Any]) -> None:
"""
Restore a previously exported backup from cloud storage.
:param backup: A dict containing an entry from the API list response.
"""
self.onRestoringStateChanged.emit(is_restoring=True)
download_url = backup.get("download_url")
if not download_url:
# If there is no download URL, we can't restore the backup.
return self._emitRestoreError()
download_package = requests.get(download_url, stream=True)
if download_package.status_code != 200:
# Something went wrong when attempting to download the backup.
Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
return self._emitRestoreError()
# We store the file in a temporary path fist to ensure integrity.
temporary_backup_file = NamedTemporaryFile(delete=False)
with open(temporary_backup_file.name, "wb") as write_backup:
for chunk in download_package:
write_backup.write(chunk)
if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash", "")):
# Don't restore the backup if the MD5 hashes do not match.
# This can happen if the download was interrupted.
Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
return self._emitRestoreError()
# Tell Cura to place the backup back in the user data folder.
with open(temporary_backup_file.name, "rb") as read_backup:
self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("data"))
self.onRestoringStateChanged.emit(is_restoring=False)
def _emitRestoreError(self, error_message: str = Settings.translatable_messages["backup_restore_error_message"]):
"""Helper method for emitting a signal when restoring failed."""
self.onRestoringStateChanged.emit(
is_restoring=False,
error_message=error_message
)
@staticmethod
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
"""
Verify the MD5 hash of a file.
:param file_path: Full path to the file.
:param known_hash: The known MD5 hash of the file.
:return: Success or not.
"""
with open(file_path, "rb") as read_backup:
local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars=b"_-").decode("utf-8")
return known_hash == local_md5_hash
def deleteBackup(self, backup_id: str) -> bool:
"""
Delete a backup from the server by ID.
:param backup_id: The ID of the backup to delete.
:return: Success bool.
"""
access_token = self._cura_api.account.accessToken
if not access_token:
Logger.log("w", "Could not get access token.")
return False
delete_backup = requests.delete("{}/{}".format(self.DELETE_BACKUP_URL, backup_id), headers = {
"Authorization": "Bearer {}".format(access_token)
})
if delete_backup.status_code > 299:
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
return False
return True
def _requestBackupUpload(self, backup_metadata: Dict[str, Any], backup_size: int) -> Optional[str]:
"""
Request a backup upload slot from the API.
:param backup_metadata: A dict containing some meta data about the backup.
:param backup_size: The size of the backup file in bytes.
:return: The upload URL for the actual backup file if successful, otherwise None.
"""
access_token = self._cura_api.account.accessToken
if not access_token:
Logger.log("w", "Could not get access token.")
return None
backup_upload_request = requests.put(self.PUT_BACKUP_URL, json={
"data": {
"backup_size": backup_size,
"metadata": backup_metadata
}
}, headers={
"Authorization": "Bearer {}".format(access_token)
})
if backup_upload_request.status_code > 299:
Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text)
return None
return backup_upload_request.json()["data"]["upload_url"]

View file

@ -0,0 +1,201 @@
# Copyright (c) 2017 Ultimaker B.V.
import os
from datetime import datetime
from typing import Optional
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
from UM.Extension import Extension
from UM.Message import Message
from .Settings import Settings
from .DriveApiService import DriveApiService
from .models.BackupListModel import BackupListModel
class DrivePluginExtension(QObject, Extension):
"""
The DivePluginExtension provides functionality to backup and restore your Cura configuration to Ultimaker's cloud.
"""
# Signal emitted when the list of backups changed.
backupsChanged = pyqtSignal()
# Signal emitted when restoring has started. Needed to prevent parallel restoring.
restoringStateChanged = pyqtSignal()
# Signal emitted when creating has started. Needed to prevent parallel creation of backups.
creatingStateChanged = pyqtSignal()
# Signal emitted when preferences changed (like auto-backup).
preferencesChanged = pyqtSignal()
DATE_FORMAT = "%d/%m/%Y %H:%M:%S"
def __init__(self, application):
super(DrivePluginExtension, self).__init__()
# Re-usable instance of application.
self._application = application
# Local data caching for the UI.
self._drive_window = None # type: Optional[QObject]
self._backups_list_model = BackupListModel()
self._is_restoring_backup = False
self._is_creating_backup = False
# Initialize services.
self._preferences = self._application.getPreferences()
self._cura_api = self._application.getCuraAPI()
self._drive_api_service = DriveApiService(self._cura_api)
# Attach signals.
self._cura_api.account.loginStateChanged.connect(self._onLoginStateChanged)
self._drive_api_service.onRestoringStateChanged.connect(self._onRestoringStateChanged)
self._drive_api_service.onCreatingStateChanged.connect(self._onCreatingStateChanged)
# Register preferences.
self._preferences.addPreference(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, False)
self._preferences.addPreference(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, datetime.now()
.strftime(self.DATE_FORMAT))
# Register menu items.
self._updateMenuItems()
# Make auto-backup on boot if required.
self._application.engineCreatedSignal.connect(self._autoBackup)
def showDriveWindow(self) -> None:
"""Show the Drive UI popup window."""
if not self._drive_window:
self._drive_window = self.createDriveWindow()
self.refreshBackups()
if self._drive_window:
self._drive_window.show()
def createDriveWindow(self) -> Optional["QObject"]:
"""
Create an instance of the Drive UI popup window.
:return: The popup window object.
"""
path = os.path.join(os.path.dirname(__file__), "qml", "main.qml")
return self._application.createQmlComponent(path, {"CuraDrive": self})
def _updateMenuItems(self) -> None:
"""Update the menu items."""
self.addMenuItem(Settings.translatable_messages["extension_menu_entry"], self.showDriveWindow)
def _autoBackup(self) -> None:
"""Automatically make a backup on boot if enabled."""
if self._preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._lastBackupTooLongAgo():
self.createBackup()
def _lastBackupTooLongAgo(self) -> bool:
"""Check if the last backup was longer than 1 day ago."""
current_date = datetime.now()
last_backup_date = self._getLastBackupDate()
date_diff = current_date - last_backup_date
return date_diff.days > 1
def _getLastBackupDate(self) -> "datetime":
"""Get the last backup date as datetime object."""
last_backup_date = self._preferences.getValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY)
return datetime.strptime(last_backup_date, self.DATE_FORMAT)
def _storeBackupDate(self) -> None:
"""Store the current date as last backup date."""
backup_date = datetime.now().strftime(self.DATE_FORMAT)
self._preferences.setValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, backup_date)
def _onLoginStateChanged(self, logged_in: bool = False) -> None:
"""Callback handler for changes in the login state."""
if logged_in:
self.refreshBackups()
def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: str = None) -> None:
"""Callback handler for changes in the restoring state."""
self._is_restoring_backup = is_restoring
self.restoringStateChanged.emit()
if error_message:
Message(error_message, title = Settings.MESSAGE_TITLE, lifetime = 5).show()
def _onCreatingStateChanged(self, is_creating: bool = False, error_message: str = None) -> None:
"""Callback handler for changes in the creation state."""
self._is_creating_backup = is_creating
self.creatingStateChanged.emit()
if error_message:
Message(error_message, title = Settings.MESSAGE_TITLE, lifetime = 5).show()
else:
self._storeBackupDate()
if not is_creating:
# We've finished creating a new backup, to the list has to be updated.
self.refreshBackups()
@pyqtSlot(bool, name = "toggleAutoBackup")
def toggleAutoBackup(self, enabled: bool) -> None:
"""Enable or disable the auto-backup feature."""
self._preferences.setValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, enabled)
self.preferencesChanged.emit()
@pyqtProperty(bool, notify = preferencesChanged)
def autoBackupEnabled(self) -> bool:
"""Check if auto-backup is enabled or not."""
return bool(self._preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY))
@pyqtProperty(QObject, notify = backupsChanged)
def backups(self) -> BackupListModel:
"""
Get a list of the backups.
:return: The backups as Qt List Model.
"""
return self._backups_list_model
@pyqtSlot(name = "refreshBackups")
def refreshBackups(self) -> None:
"""
Forcefully refresh the backups list.
"""
self._backups_list_model.loadBackups(self._drive_api_service.getBackups())
self.backupsChanged.emit()
@pyqtProperty(bool, notify = restoringStateChanged)
def isRestoringBackup(self) -> bool:
"""
Get the current restoring state.
:return: Boolean if we are restoring or not.
"""
return self._is_restoring_backup
@pyqtProperty(bool, notify = creatingStateChanged)
def isCreatingBackup(self) -> bool:
"""
Get the current creating state.
:return: Boolean if we are creating or not.
"""
return self._is_creating_backup
@pyqtSlot(str, name = "restoreBackup")
def restoreBackup(self, backup_id: str) -> None:
"""
Download and restore a backup by ID.
:param backup_id: The ID of the backup.
"""
index = self._backups_list_model.find("backup_id", backup_id)
backup = self._backups_list_model.getItem(index)
self._drive_api_service.restoreBackup(backup)
@pyqtSlot(name = "createBackup")
def createBackup(self) -> None:
"""
Create a new backup.
"""
self._drive_api_service.createBackup()
@pyqtSlot(str, name = "deleteBackup")
def deleteBackup(self, backup_id: str) -> None:
"""
Delete a backup by ID.
:param backup_id: The ID of the backup.
"""
self._drive_api_service.deleteBackup(backup_id)
self.refreshBackups()

View file

@ -0,0 +1,37 @@
# Copyright (c) 2018 Ultimaker B.V.
from UM import i18nCatalog
from cura import CuraConstants
class Settings:
"""
Keeps the application settings.
"""
DRIVE_API_VERSION = 1
DRIVE_API_URL = "{}/cura-drive/v{}".format(CuraConstants.CuraCloudAPIRoot, str(DRIVE_API_VERSION))
AUTO_BACKUP_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled"
AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date"
I18N_CATALOG_ID = "cura"
I18N_CATALOG = i18nCatalog(I18N_CATALOG_ID)
MESSAGE_TITLE = I18N_CATALOG.i18nc("@info:title", "Backups"),
# Translatable messages for the entire plugin.
translatable_messages = {
# Menu items.
"extension_menu_entry": I18N_CATALOG.i18nc("@item:inmenu", "Manage backups"),
# Notification messages.
"backup_failed": I18N_CATALOG.i18nc("@info:backup_status", "There was an error while creating your backup."),
"uploading_backup": I18N_CATALOG.i18nc("@info:backup_status", "Uploading your backup..."),
"uploading_backup_success": I18N_CATALOG.i18nc("@info:backup_status", "Your backup has finished uploading."),
"uploading_backup_error": I18N_CATALOG.i18nc("@info:backup_status",
"There was an error while uploading your backup."),
"get_backups_error": I18N_CATALOG.i18nc("@info:backup_status", "There was an error listing your backups."),
"backup_restore_error_message": I18N_CATALOG.i18nc("@info:backup_status",
"There was an error trying to restore your backup.")
}

View file

@ -0,0 +1,39 @@
# Copyright (c) 2018 Ultimaker B.V.
import requests
from UM.Job import Job
from UM.Logger import Logger
from UM.Message import Message
from .Settings import Settings
class UploadBackupJob(Job):
"""
This job is responsible for uploading the backup file to cloud storage.
As it can take longer than some other tasks, we schedule this using a Cura Job.
"""
def __init__(self, signed_upload_url: str, backup_zip: bytes) -> None:
super().__init__()
self._signed_upload_url = signed_upload_url
self._backup_zip = backup_zip
self._upload_success = False
self.backup_upload_error_message = ""
def run(self) -> None:
Message(Settings.translatable_messages["uploading_backup"], title = Settings.MESSAGE_TITLE,
lifetime = 10).show()
backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip)
if backup_upload.status_code not in (200, 201):
self.backup_upload_error_message = backup_upload.text
Logger.log("w", "Could not upload backup file: %s", backup_upload.text)
Message(Settings.translatable_messages["uploading_backup_error"], title = Settings.MESSAGE_TITLE,
lifetime = 10).show()
else:
self._upload_success = True
Message(Settings.translatable_messages["uploading_backup_success"], title = Settings.MESSAGE_TITLE,
lifetime = 10).show()
self.finished.emit(self)

View file

View file

@ -0,0 +1,38 @@
# Copyright (c) 2018 Ultimaker B.V.
from typing import Any, List, Dict
from UM.Qt.ListModel import ListModel
from PyQt5.QtCore import Qt
class BackupListModel(ListModel):
"""
The BackupListModel transforms the backups data that came from the server so it can be served to the Qt UI.
"""
def __init__(self, parent = None) -> None:
super().__init__(parent)
self.addRoleName(Qt.UserRole + 1, "backup_id")
self.addRoleName(Qt.UserRole + 2, "download_url")
self.addRoleName(Qt.UserRole + 3, "generated_time")
self.addRoleName(Qt.UserRole + 4, "md5_hash")
self.addRoleName(Qt.UserRole + 5, "data")
def loadBackups(self, data: List[Dict[str, Any]]) -> None:
"""
Populate the model with server data.
:param data:
"""
items = []
for backup in data:
# We do this loop because we only want to append these specific fields.
# Without this, ListModel will break.
items.append({
"backup_id": backup["backup_id"],
"download_url": backup["download_url"],
"generated_time": backup["generated_time"],
"md5_hash": backup["md5_hash"],
"data": backup["metadata"]
})
self.setItems(items)

View file

View file

@ -0,0 +1,67 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import UM 1.1 as UM
Button
{
id: button
property alias cursorShape: mouseArea.cursorShape
property var iconSource: ""
property var busy: false
property var color: UM.Theme.getColor("primary")
property var hoverColor: UM.Theme.getColor("primary_hover")
property var disabledColor: color
property var textColor: UM.Theme.getColor("button_text")
property var textHoverColor: UM.Theme.getColor("button_text_hover")
property var textDisabledColor: textColor
property var textFont: UM.Theme.getFont("action_button")
contentItem: RowLayout
{
Icon
{
id: buttonIcon
iconSource: button.iconSource
width: 16 * screenScaleFactor
color: button.hovered ? button.textHoverColor : button.textColor
visible: button.iconSource != "" && !loader.visible
}
Icon
{
id: loader
iconSource: "../images/loading.gif"
width: 16 * screenScaleFactor
color: button.hovered ? button.textHoverColor : button.textColor
visible: button.busy
animated: true
}
Label
{
id: buttonText
text: button.text
color: button.enabled ? (button.hovered ? button.textHoverColor : button.textColor): button.textDisabledColor
font: button.textFont
visible: button.text != ""
renderType: Text.NativeRendering
}
}
background: Rectangle
{
color: button.enabled ? (button.hovered ? button.hoverColor : button.color) : button.disabledColor
}
MouseArea
{
id: mouseArea
anchors.fill: parent
onPressed: mouse.accepted = false
hoverEnabled: true
cursorShape: button.enabled ? (hovered ? Qt.PointingHandCursor : Qt.ArrowCursor) : Qt.ForbiddenCursor
}
}

View file

@ -0,0 +1,49 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import UM 1.3 as UM
CheckBox
{
id: checkbox
hoverEnabled: true
property var label: ""
indicator: Rectangle {
implicitWidth: 30 * screenScaleFactor
implicitHeight: 30 * screenScaleFactor
x: 0
y: Math.round(parent.height / 2 - height / 2)
color: UM.Theme.getColor("sidebar")
border.color: UM.Theme.getColor("text")
Rectangle {
width: 14 * screenScaleFactor
height: 14 * screenScaleFactor
x: 8 * screenScaleFactor
y: 8 * screenScaleFactor
color: UM.Theme.getColor("primary")
visible: checkbox.checked
}
}
contentItem: Label {
anchors
{
left: checkbox.indicator.right
leftMargin: 5 * screenScaleFactor
}
text: catalog.i18nc("@checkbox:description", "Auto Backup")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
verticalAlignment: Text.AlignVCenter
}
ActionToolTip
{
text: checkbox.label
}
}

View file

@ -0,0 +1,29 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import UM 1.1 as UM
ToolTip
{
id: tooltip
visible: parent.hovered
opacity: 0.9
delay: 500
background: Rectangle
{
color: UM.Theme.getColor("sidebar")
border.color: UM.Theme.getColor("primary")
border.width: 1 * screenScaleFactor
}
contentItem: Label
{
text: tooltip.text
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("very_small")
renderType: Text.NativeRendering
}
}

View file

@ -0,0 +1,31 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import UM 1.1 as UM
ListView
{
id: backupList
width: parent.width
clip: true
delegate: Item
{
width: parent.width
height: childrenRect.height
BackupListItem
{
id: backupListItem
width: parent.width
}
Divider
{
width: parent.width
anchors.top: backupListItem.bottom
}
}
ScrollBar.vertical: RightSideScrollBar {}
}

View file

@ -0,0 +1,42 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import UM 1.3 as UM
import "../components"
RowLayout
{
id: backupListFooter
width: parent.width
property bool showInfoButton: false
ActionButton
{
id: infoButton
text: catalog.i18nc("@button", "Want more?")
iconSource: "../images/info.svg"
onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2")
visible: backupListFooter.showInfoButton
}
ActionButton
{
id: createBackupButton
text: catalog.i18nc("@button", "Backup Now")
iconSource: "../images/backup.svg"
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
onClicked: CuraDrive.createBackup()
busy: CuraDrive.isCreatingBackup
}
ActionCheckBox
{
id: autoBackupEnabled
checked: CuraDrive.autoBackupEnabled
onClicked: CuraDrive.toggleAutoBackup(autoBackupEnabled.checked)
label: catalog.i18nc("@checkbox:description", "Automatically create a backup each day that Cura is started.")
}
}

View file

@ -0,0 +1,112 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import QtQuick.Dialogs 1.1
import UM 1.1 as UM
Item
{
id: backupListItem
width: parent.width
height: showDetails ? dataRow.height + backupDetails.height : dataRow.height
property bool showDetails: false
// Backup details toggle animation.
Behavior on height
{
PropertyAnimation
{
duration: 70
}
}
RowLayout
{
id: dataRow
spacing: UM.Theme.getSize("default_margin").width * 2
width: parent.width
height: 50 * screenScaleFactor
ActionButton
{
color: "transparent"
hoverColor: "transparent"
textColor: UM.Theme.getColor("text")
textHoverColor: UM.Theme.getColor("primary")
iconSource: "../images/info.svg"
onClicked: backupListItem.showDetails = !backupListItem.showDetails
}
Label
{
text: new Date(model["generated_time"]).toLocaleString(UM.Preferences.getValue("general/language"))
color: UM.Theme.getColor("text")
elide: Text.ElideRight
Layout.minimumWidth: 100 * screenScaleFactor
Layout.maximumWidth: 500 * screenScaleFactor
Layout.fillWidth: true
renderType: Text.NativeRendering
}
Label
{
text: model["data"]["description"]
color: UM.Theme.getColor("text")
elide: Text.ElideRight
Layout.minimumWidth: 100 * screenScaleFactor
Layout.maximumWidth: 500 * screenScaleFactor
Layout.fillWidth: true
renderType: Text.NativeRendering
}
ActionButton
{
text: catalog.i18nc("@button", "Restore")
color: "transparent"
hoverColor: "transparent"
textColor: UM.Theme.getColor("text")
textHoverColor: UM.Theme.getColor("text_link")
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
onClicked: confirmRestoreDialog.visible = true
}
ActionButton
{
color: "transparent"
hoverColor: "transparent"
textColor: UM.Theme.getColor("setting_validation_error")
textHoverColor: UM.Theme.getColor("setting_validation_error")
iconSource: "../images/delete.svg"
onClicked: confirmDeleteDialog.visible = true
}
}
BackupListItemDetails
{
id: backupDetails
backupDetailsData: model
width: parent.width
visible: parent.showDetails
anchors.top: dataRow.bottom
}
MessageDialog
{
id: confirmDeleteDialog
title: catalog.i18nc("@dialog:title", "Delete Backup")
text: catalog.i18nc("@dialog:info", "Are you sure you want to delete this backup? This cannot be undone.")
standardButtons: StandardButton.Yes | StandardButton.No
onYes: CuraDrive.deleteBackup(model["backup_id"])
}
MessageDialog
{
id: confirmRestoreDialog
title: catalog.i18nc("@dialog:title", "Restore Backup")
text: catalog.i18nc("@dialog:info", "You will need to restart Cura before your backup is restored. Do you want to close Cura now?")
standardButtons: StandardButton.Yes | StandardButton.No
onYes: CuraDrive.restoreBackup(model["backup_id"])
}
}

View file

@ -0,0 +1,61 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import UM 1.1 as UM
ColumnLayout
{
id: backupDetails
width: parent.width
spacing: 10 * screenScaleFactor
property var backupDetailsData
// Cura version
BackupListItemDetailsRow
{
iconSource: "../images/cura.svg"
label: catalog.i18nc("@backuplist:label", "Cura Version")
value: backupDetailsData["data"]["cura_release"]
}
// Machine count.
BackupListItemDetailsRow
{
iconSource: "../images/printer.svg"
label: catalog.i18nc("@backuplist:label", "Machines")
value: backupDetailsData["data"]["machine_count"]
}
// Meterial count.
BackupListItemDetailsRow
{
iconSource: "../images/material.svg"
label: catalog.i18nc("@backuplist:label", "Materials")
value: backupDetailsData["data"]["material_count"]
}
// Meterial count.
BackupListItemDetailsRow
{
iconSource: "../images/profile.svg"
label: catalog.i18nc("@backuplist:label", "Profiles")
value: backupDetailsData["data"]["profile_count"]
}
// Meterial count.
BackupListItemDetailsRow
{
iconSource: "../images/plugin.svg"
label: catalog.i18nc("@backuplist:label", "Plugins")
value: backupDetailsData["data"]["plugin_count"]
}
// Spacer.
Item
{
width: parent.width
height: 10 * screenScaleFactor
}
}

View file

@ -0,0 +1,52 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import UM 1.3 as UM
RowLayout
{
id: detailsRow
width: parent.width
height: 40 * screenScaleFactor
property var iconSource
property var label
property var value
// Spacing.
Item
{
width: 40 * screenScaleFactor
}
Icon
{
width: 18 * screenScaleFactor
iconSource: detailsRow.iconSource
color: UM.Theme.getColor("text")
}
Label
{
text: detailsRow.label
color: UM.Theme.getColor("text")
elide: Text.ElideRight
Layout.minimumWidth: 50 * screenScaleFactor
Layout.maximumWidth: 100 * screenScaleFactor
Layout.fillWidth: true
renderType: Text.NativeRendering
}
Label
{
text: detailsRow.value
color: UM.Theme.getColor("text")
elide: Text.ElideRight
Layout.minimumWidth: 50 * screenScaleFactor
Layout.maximumWidth: 100 * screenScaleFactor
Layout.fillWidth: true
renderType: Text.NativeRendering
}
}

View file

@ -0,0 +1,11 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import UM 1.3 as UM
Rectangle
{
id: divider
color: UM.Theme.getColor("lining")
height: UM.Theme.getSize("default_lining").height
}

View file

@ -0,0 +1,56 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtGraphicalEffects 1.0
Item
{
id: icon
width: parent.height
height: width
property var color: "transparent"
property var iconSource
property bool animated: false
Image
{
id: iconImage
width: parent.height
height: width
smooth: true
source: icon.iconSource
sourceSize.width: width
sourceSize.height: height
antialiasing: true
visible: !icon.animated
}
AnimatedImage
{
id: animatedIconImage
width: parent.height
height: width
smooth: true
antialiasing: true
source: "../images/loading.gif"
visible: icon.animated
}
ColorOverlay
{
anchors.fill: iconImage
source: iconImage
color: icon.color
antialiasing: true
visible: !icon.animated
}
ColorOverlay
{
anchors.fill: animatedIconImage
source: animatedIconImage
color: icon.color
antialiasing: true
visible: icon.animated
}
}

View file

@ -0,0 +1,13 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
ScrollBar
{
active: true
size: parent.height
anchors.top: parent.top
anchors.right: parent.right
anchors.bottom: parent.bottom
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1024px" height="1183px" viewBox="0 0 1024 1183" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
<title>Polygon 18</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Home" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" fill-opacity="0.2">
<g id="1440px-home" transform="translate(-86.000000, -30.000000)" fill="#D1D9DB">
<polygon id="Polygon-18" points="598 30 1110 325.603338 1110 916.810013 598 1212.41335 86 916.810013 86 325.603338"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 734 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="white">
<path d="M16.5 11.3h-5.2v5.2H8.7v-5.2H3.5V8.7h5.2V3.5h2.6v5.2h5.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 154 B

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<switch>
<g>
<path d="M11.07 3L3 11.071V27h15.931L27 18.93V3H11.07zm10.175 8.235h-6.071c-2.02.013-3.016 1.414-3.016 3.115 0 1.702.996 3.125 3.016 3.136h6.071v3.433h-6.071c-3.996 0-6.419-2.743-6.419-6.568 0-3.826 2.423-6.548 6.419-6.548h6.071v3.432z"/>
</g>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 13" fill="red">
<switch>
<g>
<path d="M13 2.23L8.73 6.5 13 10.77l-2.135 2.134-4.269-4.269-4.27 4.269L.191 10.77l4.27-4.27-4.27-4.27L2.326.096l4.27 4.269L10.865.096z"/>
</g>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 281 B

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<switch>
<g>
<path d="M21.718 10.969V7.758H8.451L7.014 5.222h-5.83v19.436h21.211l6.422-13.69-7.099.001zm-1.098 0H8.958L3.043 23.56h-.761V6.321h4.056l1.437 2.535H20.62v2.113z"/>
</g>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 295 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" fill="white">
<path d="m 8.5727081,5.62578 v 2.97725 q 0,0.16127 -0.1178498,0.27912 Q 8.3370085,9 8.1757405,9 H 5.7939349 V 6.61819 H 4.2060646 V 9 H 1.824259 Q 1.6629909,9 1.5451412,8.88215 1.4272914,8.7643 1.4272914,8.60303 V 5.62578 q 0,-0.006 0.0031,-0.0186 0.0031,-0.0124 0.0031,-0.0186 L 4.9999998,2.64852 8.5665054,5.58856 q 0.0062,0.0124 0.0062,0.0372 z M 9.955892,5.19779 9.5713297,5.65679 q -0.049621,0.0558 -0.130255,0.0682 h -0.018608 q -0.080634,0 -0.130255,-0.0434 L 4.9999998,2.10269 0.70778771,5.6816 Q 0.63335631,5.7312 0.55892486,5.725 0.47829087,5.7126 0.42866987,5.6568 L 0.04410752,5.1978 Q -0.00551343,5.1358 6.8917799e-4,5.05204 0.00689178,4.96834 0.06891799,4.91869 L 4.5286008,1.20331 q 0.1984838,-0.16127 0.471399,-0.16127 0.2729153,0 0.471399,0.16127 L 6.9848377,2.46864 V 1.25913 q 0,-0.0868 0.055824,-0.14266 0.055824,-0.0558 0.1426602,-0.0558 h 1.1909028 q 0.086837,0 0.1426602,0.0558 0.055824,0.0558 0.055824,0.14266 V 3.7898 l 1.3583734,1.12888 q 0.062026,0.0496 0.068229,0.13335 0.0062,0.0837 -0.043418,0.14576 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="15" height="15" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 15C11.641 15 15 11.643 15 7.5 15 3.358 11.641 0 7.5 0 3.358 0 0 3.358 0 7.5 0 11.643 3.358 15 7.5 15ZM8.6 12.369L6.472 12.369 6.472 4.57 8.6 4.57 8.6 12.369ZM7.541 1.514C8.313 1.514 8.697 1.861 8.697 2.553 8.697 2.885 8.6 3.141 8.409 3.325 8.216 3.509 7.926 3.601 7.541 3.601 6.767 3.601 6.382 3.252 6.382 2.553 6.382 1.861 6.767 1.514 7.541 1.514Z"/>
</svg>

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<switch>
<g>
<path d="M2.995 1H5.67v24H2.995zm21.33 0H27v24h-2.675zM8.992 3.284c-.368 0-.669.224-.669.5v18.433c0 .276.3.5.669.5.369 0 .669-.224.669-.5V3.784c0-.276-.299-.5-.669-.5m4.003 0c-.368 0-.669.224-.669.5v18.433c0 .276.3.5.669.5.371 0 .669-.224.669-.5V3.784c0-.276-.298-.5-.669-.5m4.004 0c-.371 0-.669.224-.669.5v24.451c0 .277.298.5.669.5.368 0 .669-.223.669-.5V3.784c0-.276-.301-.5-.669-.5m4.003 0c-.368 0-.669.224-.669.5v18.433c0 .276.3.5.669.5.37 0 .669-.224.669-.5V3.784c-.001-.276-.3-.5-.669-.5"/>
</g>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 628 B

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<switch>
<g>
<path d="M22.32 19.715l-6.96-6.96 6.96-6.958v4.541H27V3H10.999L3 11.001V27h15.998L27 19.001v-3.828h-4.68z"/>
</g>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 49 (51002) - http://www.bohemiancoding.com/sketch -->
<title>icn_singlePrinter</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Visual" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Printer-status-icon" transform="translate(-217.000000, -176.000000)" fill="#000000">
<g id="icn_singlePrinter" transform="translate(217.000000, 176.000000)">
<path d="M2,13 L14,13 L14,15 L2,15 L2,13 L2,13 Z M2,3 L14,3 L14,5 L2,5 L2,3 L2,3 Z M0,14 L2,14 L2,16 L0,16 L0,14 L0,14 Z M14,14 L16,14 L16,16 L14,16 L14,14 L14,14 Z M0,0 L2,0 L2,14 L0,14 L0,0 L0,0 Z M14,0 L16,0 L16,14 L14,14 L14,0 L14,0 Z M6,5 L10,5 L10,6 L6,6 L6,5 L6,5 Z M7,6 L9,6 L9,8 L7,8 L7,6 L7,6 Z M2,0 L14,0 L14,2 L2,2 L2,0 L2,0 Z M3,12 L13,12 L13,13 L3,13 L3,12 L3,12 Z" id="Rectangle-185-Copy-5-Copy-4"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 13">
<path d="M12.96 5.778c-.021-.182-.234-.32-.419-.32-.595 0-1.124-.35-1.346-.89a1.45 1.45 0 0 1 .364-1.608.361.361 0 0 0 .04-.49 6.43 6.43 0 0 0-1.03-1.04.362.362 0 0 0-.494.04c-.388.43-1.084.589-1.621.364A1.444 1.444 0 0 1 7.576.423a.36.36 0 0 0-.32-.38A6.485 6.485 0 0 0 5.795.04a.362.362 0 0 0-.321.372 1.447 1.447 0 0 1-.89 1.387c-.532.217-1.223.06-1.61-.366a.362.362 0 0 0-.49-.041A6.46 6.46 0 0 0 1.43 2.43a.362.362 0 0 0 .04.493 1.44 1.44 0 0 1 .363 1.622c-.225.534-.78.879-1.415.879a.354.354 0 0 0-.375.319A6.51 6.51 0 0 0 .04 7.222c.02.183.24.32.426.32a1.426 1.426 0 0 1 1.338.89c.227.554.08 1.2-.364 1.608a.36.36 0 0 0-.04.49c.303.384.65.734 1.029 1.04.149.12.365.103.495-.04.389-.43 1.084-.589 1.62-.364.561.235.914.802.88 1.41a.36.36 0 0 0 .318.38 6.44 6.44 0 0 0 1.463.005.362.362 0 0 0 .322-.373 1.445 1.445 0 0 1 .889-1.386c.535-.218 1.223-.058 1.61.366.128.14.34.157.49.041a6.476 6.476 0 0 0 1.052-1.04.361.361 0 0 0-.039-.493 1.44 1.44 0 0 1-.364-1.622c.22-.527.755-.88 1.33-.88l.08.001a.362.362 0 0 0 .38-.319c.058-.488.058-.985.003-1.478zM6.51 8.682a2.17 2.17 0 0 1-2.168-2.168A2.17 2.17 0 0 1 6.51 4.346a2.17 2.17 0 0 1 2.168 2.168A2.17 2.17 0 0 1 6.51 8.682z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
<switch>
<g>
<path d="M-.113 31.935c8.904 4.904 17.81 9.802 26.709 14.714 2.507 1.384 4.993 2.807 7.868 4.426C57.24 23.782 85.983 7.847 121.495 5.371c27.659-1.928 53.113 5.077 76.006 20.788 44.611 30.614 63.473 86.919 46.099 137.829-17.883 52.399-66.749 82.265-113.69 81.745v-36.688c25.195-1.141 46.785-10.612 63.364-30.267 11.82-14.013 18.14-30.322 19.349-48.541 2.323-34.992-19.005-68.519-51.909-81.916-32.223-13.12-70.379-4.319-93 22.01l31.263 18.198c-1.545 1.07-2.387 1.747-3.312 2.281a81656.22 81656.22 0 0 1-92.099 53.112c-1.128.65-2.448.967-3.679 1.439V31.935z"/>
</g>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 691 B

View file

@ -0,0 +1,42 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Window 2.2
import UM 1.3 as UM
import Cura 1.1 as Cura
import "components"
import "pages"
Window
{
id: curaDriveDialog
minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width)
minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height)
maximumWidth: minimumWidth * 1.2
maximumHeight: minimumHeight * 1.2
width: minimumWidth
height: minimumHeight
color: UM.Theme.getColor("sidebar")
title: catalog.i18nc("@title:window", "Cura Backups")
// Globally available.
UM.I18nCatalog
{
id: catalog
name: "cura_drive"
}
WelcomePage
{
id: welcomePage
visible: !Cura.API.account.isLoggedIn
}
BackupsPage
{
id: backupsPage
visible: Cura.API.account.isLoggedIn
}
}

View file

@ -0,0 +1,73 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import UM 1.3 as UM
import Cura 1.1 as Cura
import "../components"
Item
{
id: backupsPage
anchors.fill: parent
anchors.margins: UM.Theme.getSize("default_margin").width * 3
ColumnLayout
{
spacing: UM.Theme.getSize("default_margin").height * 2
width: parent.width
anchors.fill: parent
Label
{
id: backupTitle
text: catalog.i18nc("@title", "My Backups")
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
Layout.fillWidth: true
renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@empty_state",
"You don't have any backups currently. Use the 'Backup Now' button to create one.")
width: parent.width
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
wrapMode: Label.WordWrap
visible: backupList.count == 0
Layout.fillWidth: true
Layout.fillHeight: true
renderType: Text.NativeRendering
}
BackupList
{
id: backupList
model: CuraDrive.backups
Layout.fillWidth: true
Layout.fillHeight: true
}
Label
{
text: catalog.i18nc("@backup_limit_info",
"During the preview phase, you'll be limited to 5 visible backups. Remove a backup to see older ones.")
width: parent.width
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
wrapMode: Label.WordWrap
visible: backupList.count > 4
renderType: Text.NativeRendering
}
BackupListFooter
{
id: backupListFooter
showInfoButton: backupList.count > 4
}
}
}

View file

@ -0,0 +1,48 @@
// Copyright (c) 2018 Ultimaker B.V.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Window 2.2
import UM 1.3 as UM
import Cura 1.1 as Cura
import "../components"
Column
{
id: welcomePage
spacing: UM.Theme.getSize("wide_margin").height
width: parent.width
topPadding: 150 * screenScaleFactor
Image
{
id: profileImage
fillMode: Image.PreserveAspectFit
source: "../images/icon.png"
anchors.horizontalCenter: parent.horizontalCenter
width: Math.round(parent.width / 4)
}
Label
{
id: welcomeTextLabel
text: catalog.i18nc("@description", "Backup and synchronize your Cura settings.")
width: Math.round(parent.width / 2)
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Label.WordWrap
renderType: Text.NativeRendering
}
ActionButton
{
id: loginButton
onClicked: Cura.API.account.login()
text: catalog.i18nc("@button", "Sign In")
anchors.horizontalCenter: parent.horizontalCenter
}
}

View file

@ -14,8 +14,8 @@ Window
modality: Qt.ApplicationModal
flags: Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint
width: 720 * screenScaleFactor
height: 640 * screenScaleFactor
width: Math.floor(720 * screenScaleFactor)
height: Math.floor(640 * screenScaleFactor)
minimumWidth: width
maximumWidth: minimumWidth
minimumHeight: height
@ -95,6 +95,7 @@ Window
licenseDialog.show();
}
}
ToolboxLicenseDialog
{
id: licenseDialog

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.3
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@ -59,6 +59,7 @@ Item
wrapMode: Text.WordWrap
width: parent.width
height: UM.Theme.getSize("toolbox_property_label").height
renderType: Text.NativeRendering
}
Label
{
@ -70,6 +71,7 @@ Item
left: title.left
topMargin: UM.Theme.getSize("default_margin").height
}
renderType: Text.NativeRendering
}
Column
{
@ -88,12 +90,14 @@ Item
text: catalog.i18nc("@label", "Website") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Email") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
renderType: Text.NativeRendering
}
}
Column
@ -122,6 +126,7 @@ Item
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
renderType: Text.NativeRendering
}
Label
@ -138,6 +143,7 @@ Item
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
renderType: Text.NativeRendering
}
}
Rectangle

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@ -64,6 +64,7 @@ Item
font: UM.Theme.getFont("default_bold")
horizontalAlignment: Text.AlignRight
width: control.width
renderType: Text.NativeRendering
}
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@ -67,6 +67,7 @@ Item
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
}
TableView
@ -99,6 +100,7 @@ Item
text: styleData.value || ""
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default_bold")
renderType: Text.NativeRendering
}
Rectangle
{
@ -118,6 +120,7 @@ Item
text: styleData.value || ""
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
}
itemDelegate: Item
@ -130,6 +133,7 @@ Item
text: styleData.value || ""
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
}
@ -144,6 +148,7 @@ Item
elide: Text.ElideRight
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
}
@ -232,5 +237,6 @@ Item
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
renderType: Text.NativeRendering
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick 2.10
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
@ -66,6 +66,7 @@ UM.Dialog
anchors.right: parent.right
font: UM.Theme.getFont("default")
wrapMode: Text.WordWrap
renderType: Text.NativeRendering
}
// Buttons

View file

@ -26,10 +26,19 @@ Item
}
height: childrenRect.height + 2 * UM.Theme.getSize("wide_margin").height
spacing: UM.Theme.getSize("default_margin").height
Repeater
{
model: toolbox.packagesModel
delegate: ToolboxDetailTile {}
delegate: Loader
{
// FIXME: When using asynchronous loading, on Mac and Windows, the tile may fail to load complete,
// leaving an empty space below the title part. We turn it off for now to make it work on Mac and
// Windows.
// Can be related to this QT bug: https://bugreports.qt.io/browse/QTBUG-50992
asynchronous: false
source: "ToolboxDetailTile.qml"
}
}
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.3
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@ -65,6 +65,7 @@ Item
wrapMode: Text.WordWrap
width: parent.width
height: UM.Theme.getSize("toolbox_property_label").height
renderType: Text.NativeRendering
}
Column
@ -84,24 +85,28 @@ Item
text: catalog.i18nc("@label", "Version") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Last updated") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Author") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Downloads") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
renderType: Text.NativeRendering
}
}
Column
@ -121,6 +126,7 @@ Item
text: details === null ? "" : (details.version || catalog.i18nc("@label", "Unknown"))
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
Label
{
@ -135,6 +141,7 @@ Item
}
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
Label
{
@ -153,12 +160,14 @@ Item
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
renderType: Text.NativeRendering
}
Label
{
text: details === null ? "" : (details.download_count || catalog.i18nc("@label", "Unknown"))
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
}
Rectangle

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@ -31,6 +31,7 @@ Item
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("medium_bold")
renderType: Text.NativeRendering
}
Label
{
@ -42,6 +43,7 @@ Item
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
}

View file

@ -1,40 +1,69 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
import Cura 1.1 as Cura
Column
{
property bool installed: toolbox.isInstalled(model.id)
property bool canUpdate: toolbox.canUpdate(model.id)
property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
width: UM.Theme.getSize("toolbox_action_button").width
spacing: UM.Theme.getSize("narrow_margin").height
ToolboxProgressButton
Item
{
id: installButton
active: toolbox.isDownloading && toolbox.activePackage == model
complete: installed
readyAction: function()
width: installButton.width
height: installButton.height
ToolboxProgressButton
{
toolbox.activePackage = model
toolbox.startDownload(model.download_url)
id: installButton
active: toolbox.isDownloading && toolbox.activePackage == model
onReadyAction:
{
toolbox.activePackage = model
toolbox.startDownload(model.download_url)
}
onActiveAction: toolbox.cancelDownload()
// Don't allow installing while another download is running
enabled: installed || (!(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired)
opacity: enabled ? 1.0 : 0.5
visible: !updateButton.visible && !installed// Don't show when the update button is visible
}
activeAction: function()
Cura.SecondaryButton
{
toolbox.cancelDownload()
visible: installed
onClicked: toolbox.viewCategory = "installed"
text: catalog.i18nc("@action:button", "Installed")
fixedWidthMode: true
width: installButton.width
height: installButton.height
}
completeAction: function()
}
Label
{
wrapMode: Text.WordWrap
text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Log in</a> is required to install or update")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
visible: loginRequired
width: installButton.width
renderType: Text.NativeRendering
MouseArea
{
toolbox.viewCategory = "installed"
anchors.fill: parent
onClicked: Cura.API.account.login()
}
// Don't allow installing while another download is running
enabled: installed || !(toolbox.isDownloading && toolbox.activePackage != model)
opacity: enabled ? 1.0 : 0.5
visible: !updateButton.visible // Don't show when the update button is visible
}
ToolboxProgressButton
@ -44,20 +73,19 @@ Column
readyLabel: catalog.i18nc("@action:button", "Update")
activeLabel: catalog.i18nc("@action:button", "Updating")
completeLabel: catalog.i18nc("@action:button", "Updated")
readyAction: function()
onReadyAction:
{
toolbox.activePackage = model
toolbox.update(model.id)
}
activeAction: function()
{
toolbox.cancelDownload()
}
onActiveAction: toolbox.cancelDownload()
// Don't allow installing while another download is running
enabled: !(toolbox.isDownloading && toolbox.activePackage != model)
enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired
opacity: enabled ? 1.0 : 0.5
visible: canUpdate
}
Connections
{
target: toolbox

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.3
@ -23,8 +23,9 @@ Column
width: parent.width
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
}
GridLayout
Grid
{
id: grid
width: parent.width - 2 * parent.padding
@ -34,10 +35,12 @@ Column
Repeater
{
model: gridArea.model
delegate: ToolboxDownloadsGridTile
delegate: Loader
{
Layout.preferredWidth: (grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns
Layout.preferredHeight: UM.Theme.getSize("toolbox_thumbnail_small").height
asynchronous: true
width: Math.round((grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns)
height: UM.Theme.getSize("toolbox_thumbnail_small").height
source: "ToolboxDownloadsGridTile.qml"
}
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.3
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.3
@ -62,6 +62,8 @@ Item
{
width: parent.width - thumbnail.width - parent.spacing
spacing: Math.floor(UM.Theme.getSize("narrow_margin").width)
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
Label
{
id: name
@ -70,6 +72,7 @@ Item
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default_bold")
renderType: Text.NativeRendering
}
Label
{
@ -81,6 +84,7 @@ Item
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@ -24,29 +24,33 @@ Rectangle
width: parent.width
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
}
Grid
{
height: childrenRect.height
spacing: UM.Theme.getSize("wide_margin").width
columns: 3
anchors
{
horizontalCenter: parent.horizontalCenter
}
anchors.horizontalCenter: parent.horizontalCenter
Repeater
{
model: {
if ( toolbox.viewCategory == "plugin" )
model:
{
if (toolbox.viewCategory == "plugin")
{
return toolbox.pluginsShowcaseModel
}
if ( toolbox.viewCategory == "material" )
if (toolbox.viewCategory == "material")
{
return toolbox.materialsShowcaseModel
}
}
delegate: ToolboxDownloadsShowcaseTile {}
delegate: Loader
{
asynchronous: true
source: "ToolboxDownloadsShowcaseTile.qml"
}
}
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtGraphicalEffects 1.0
@ -79,6 +79,7 @@ Rectangle
wrapMode: Text.WordWrap
color: UM.Theme.getColor("button_text")
font: UM.Theme.getFont("medium_bold")
renderType: Text.NativeRendering
}
}
MouseArea

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
@ -18,5 +18,6 @@ Rectangle
{
centerIn: parent
}
renderType: Text.NativeRendering
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@ -26,7 +26,7 @@ Item
right: restartButton.right
rightMargin: UM.Theme.getSize("default_margin").width
}
renderType: Text.NativeRendering
}
Button
{
@ -56,6 +56,7 @@ Item
text: control.text
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering
}
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
@ -21,44 +21,40 @@ ScrollView
Column
{
spacing: UM.Theme.getSize("default_margin").height
visible: toolbox.pluginsInstalledModel.items.length > 0
height: childrenRect.height + 4 * UM.Theme.getSize("default_margin").height
anchors
{
right: parent.right
left: parent.left
leftMargin: UM.Theme.getSize("wide_margin").width
topMargin: UM.Theme.getSize("wide_margin").height
bottomMargin: UM.Theme.getSize("wide_margin").height
margins: UM.Theme.getSize("default_margin").width
top: parent.top
}
height: childrenRect.height + 4 * UM.Theme.getSize("default_margin").height
Label
{
visible: toolbox.pluginsInstalledModel.items.length > 0
width: parent.width
width: page.width
text: catalog.i18nc("@title:tab", "Plugins")
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
}
Rectangle
{
visible: toolbox.pluginsInstalledModel.items.length > 0
color: "transparent"
width: parent.width
height: childrenRect.height + 1 * UM.Theme.getSize("default_lining").width
height: childrenRect.height + UM.Theme.getSize("default_margin").width
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width
Column
{
height: childrenRect.height
anchors
{
top: parent.top
right: parent.right
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
rightMargin: UM.Theme.getSize("default_margin").width
topMargin: UM.Theme.getSize("default_lining").width
bottomMargin: UM.Theme.getSize("default_lining").width
margins: UM.Theme.getSize("default_margin").width
}
Repeater
{
@ -70,32 +66,27 @@ ScrollView
}
Label
{
visible: toolbox.materialsInstalledModel.items.length > 0
width: page.width
text: catalog.i18nc("@title:tab", "Materials")
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
}
Rectangle
{
visible: toolbox.materialsInstalledModel.items.length > 0
color: "transparent"
width: parent.width
height: childrenRect.height + 1 * UM.Theme.getSize("default_lining").width
height: childrenRect.height + UM.Theme.getSize("default_margin").width
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width
Column
{
height: Math.max( UM.Theme.getSize("wide_margin").height, childrenRect.height)
anchors
{
top: parent.top
right: parent.right
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
rightMargin: UM.Theme.getSize("default_margin").width
topMargin: UM.Theme.getSize("default_lining").width
bottomMargin: UM.Theme.getSize("default_lining").width
margins: UM.Theme.getSize("default_margin").width
}
Repeater
{

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@ -51,6 +51,7 @@ Item
wrapMode: Text.WordWrap
font: UM.Theme.getFont("default_bold")
color: pluginInfo.color
renderType: Text.NativeRendering
}
Label
{
@ -60,6 +61,7 @@ Item
width: parent.width
wrapMode: Text.WordWrap
color: pluginInfo.color
renderType: Text.NativeRendering
}
}
Column
@ -88,6 +90,7 @@ Item
onLinkActivated: Qt.openUrlExternally("mailto:" + model.author_email + "?Subject=Cura: " + model.name + " Plugin")
color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining")
linkColor: UM.Theme.getColor("text_link")
renderType: Text.NativeRendering
}
Label
@ -98,6 +101,7 @@ Item
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
renderType: Text.NativeRendering
}
}
ToolboxInstalledTileActions

View file

@ -1,15 +1,18 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
import Cura 1.1 as Cura
Column
{
property bool canUpdate: false
property bool canDowngrade: false
property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
width: UM.Theme.getSize("toolbox_action_button").width
spacing: UM.Theme.getSize("narrow_margin").height
@ -21,6 +24,7 @@ Column
font: UM.Theme.getFont("default")
wrapMode: Text.WordWrap
width: parent.width
renderType: Text.NativeRendering
}
ToolboxProgressButton
@ -30,59 +34,49 @@ Column
readyLabel: catalog.i18nc("@action:button", "Update")
activeLabel: catalog.i18nc("@action:button", "Updating")
completeLabel: catalog.i18nc("@action:button", "Updated")
readyAction: function()
onReadyAction:
{
toolbox.activePackage = model
toolbox.update(model.id)
}
activeAction: function()
{
toolbox.cancelDownload()
}
onActiveAction: toolbox.cancelDownload()
// Don't allow installing while another download is running
enabled: !(toolbox.isDownloading && toolbox.activePackage != model)
enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired
opacity: enabled ? 1.0 : 0.5
visible: canUpdate
}
Button
Label
{
wrapMode: Text.WordWrap
text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Log in</a> is required to update")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
visible: loginRequired
width: updateButton.width
renderType: Text.NativeRendering
MouseArea
{
anchors.fill: parent
onClicked: Cura.API.account.login()
}
}
Cura.SecondaryButton
{
id: removeButton
text: canDowngrade ? catalog.i18nc("@action:button", "Downgrade") : catalog.i18nc("@action:button", "Uninstall")
visible: !model.is_bundled && model.is_installed
enabled: !toolbox.isDownloading
style: ButtonStyle
{
background: Rectangle
{
implicitWidth: UM.Theme.getSize("toolbox_action_button").width
implicitHeight: UM.Theme.getSize("toolbox_action_button").height
color: "transparent"
border
{
width: UM.Theme.getSize("default_lining").width
color:
{
if (control.hovered)
{
return UM.Theme.getColor("primary_hover")
}
else
{
return UM.Theme.getColor("lining")
}
}
}
}
label: Label
{
text: control.text
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
font: UM.Theme.getFont("default")
}
}
width: UM.Theme.getSize("toolbox_action_button").width
height: UM.Theme.getSize("toolbox_action_button").height
fixedWidthMode: true
onClicked: toolbox.checkPackageUsageAndUninstall(model.id)
Connections
{

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick 2.10
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
@ -32,6 +32,7 @@ UM.Dialog
anchors.right: parent.right
text: licenseDialog.pluginName + catalog.i18nc("@label", "This plugin contains a license.\nYou need to accept this license to install this plugin.\nDo you agree with the terms below?")
wrapMode: Text.Wrap
renderType: Text.NativeRendering
}
TextArea
{

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
@ -18,5 +18,6 @@ Rectangle
{
centerIn: parent
}
renderType: Text.NativeRendering
}
}

View file

@ -5,6 +5,7 @@ import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
import Cura 1.0 as Cura
Item
@ -18,16 +19,19 @@ Item
property var activeLabel: catalog.i18nc("@action:button", "Cancel")
property var completeLabel: catalog.i18nc("@action:button", "Installed")
property var readyAction: null // Action when button is ready and clicked (likely install)
property var activeAction: null // Action when button is active and clicked (likely cancel)
property var completeAction: null // Action when button is complete and clicked (likely go to installed)
signal readyAction() // Action when button is ready and clicked (likely install)
signal activeAction() // Action when button is active and clicked (likely cancel)
signal completeAction() // Action when button is complete and clicked (likely go to installed)
width: UM.Theme.getSize("toolbox_action_button").width
height: UM.Theme.getSize("toolbox_action_button").height
Button
Cura.PrimaryButton
{
id: button
width: UM.Theme.getSize("toolbox_action_button").width
height: UM.Theme.getSize("toolbox_action_button").height
fixedWidthMode: true
text:
{
if (complete)
@ -47,101 +51,15 @@ Item
{
if (complete)
{
return completeAction()
completeAction()
}
else if (active)
{
return activeAction()
activeAction()
}
else
{
return readyAction()
}
}
style: ButtonStyle
{
background: Rectangle
{
implicitWidth: UM.Theme.getSize("toolbox_action_button").width
implicitHeight: UM.Theme.getSize("toolbox_action_button").height
color:
{
if (base.complete)
{
return "transparent"
}
else
{
if (control.hovered)
{
return UM.Theme.getColor("primary_hover")
}
else
{
return UM.Theme.getColor("primary")
}
}
}
border
{
width:
{
if (base.complete)
{
UM.Theme.getSize("default_lining").width
}
else
{
return 0
}
}
color:
{
if (control.hovered)
{
return UM.Theme.getColor("primary_hover")
}
else
{
return UM.Theme.getColor("lining")
}
}
}
}
label: Label
{
text: control.text
color:
{
if (base.complete)
{
return UM.Theme.getColor("text")
}
else
{
if (control.hovered)
{
return UM.Theme.getColor("button_text_hover")
}
else
{
return UM.Theme.getColor("button_text")
}
}
}
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
font:
{
if (base.complete)
{
return UM.Theme.getFont("default")
}
else
{
return UM.Theme.getFont("default_bold")
}
}
readyAction()
}
}
}

View file

@ -1,51 +1,51 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.1 as UM
Button
{
id: control
property bool active: false
style: ButtonStyle
hoverEnabled: true
background: Item
{
background: Rectangle
implicitWidth: UM.Theme.getSize("toolbox_header_tab").width
implicitHeight: UM.Theme.getSize("toolbox_header_tab").height
Rectangle
{
color: "transparent"
implicitWidth: UM.Theme.getSize("toolbox_header_tab").width
implicitHeight: UM.Theme.getSize("toolbox_header_tab").height
Rectangle
{
visible: control.active
color: UM.Theme.getColor("toolbox_header_highlight_hover")
anchors.bottom: parent.bottom
width: parent.width
height: UM.Theme.getSize("toolbox_header_highlight").height
}
}
label: Label
{
text: control.text
color:
{
if(control.hovered)
{
return UM.Theme.getColor("toolbox_header_button_text_hovered");
}
if(control.active)
{
return UM.Theme.getColor("toolbox_header_button_text_active");
}
else
{
return UM.Theme.getColor("toolbox_header_button_text_inactive");
}
}
font: control.enabled ? (control.active ? UM.Theme.getFont("medium_bold") : UM.Theme.getFont("medium")) : UM.Theme.getFont("default_italic")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
visible: control.active
color: UM.Theme.getColor("primary")
anchors.bottom: parent.bottom
width: parent.width
height: UM.Theme.getSize("toolbox_header_highlight").height
}
}
}
contentItem: Label
{
id: label
text: control.text
color:
{
if(control.hovered)
{
return UM.Theme.getColor("toolbox_header_button_text_hovered");
}
if(control.active)
{
return UM.Theme.getColor("toolbox_header_button_text_active");
}
else
{
return UM.Theme.getColor("toolbox_header_button_text_inactive");
}
}
font: control.enabled ? (control.active ? UM.Theme.getFont("medium_bold") : UM.Theme.getFont("medium")) : UM.Theme.getFont("default_italic")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering
}
}

View file

@ -2,18 +2,19 @@
# Cura is released under the terms of the LGPLv3 or higher.
import re
from typing import Dict
from typing import Dict, List, Optional, Union
from PyQt5.QtCore import Qt, pyqtProperty, pyqtSignal
from UM.Qt.ListModel import ListModel
## Model that holds cura packages. By setting the filter property the instances held by this model can be changed.
class AuthorsModel(ListModel):
def __init__(self, parent = None):
def __init__(self, parent = None) -> None:
super().__init__(parent)
self._metadata = None
self._metadata = None # type: Optional[List[Dict[str, Union[str, List[str], int]]]]
self.addRoleName(Qt.UserRole + 1, "id")
self.addRoleName(Qt.UserRole + 2, "name")
@ -25,39 +26,40 @@ class AuthorsModel(ListModel):
self.addRoleName(Qt.UserRole + 8, "description")
# List of filters for queries. The result is the union of the each list of results.
self._filter = {} # type: Dict[str,str]
self._filter = {} # type: Dict[str, str]
def setMetadata(self, data):
self._metadata = data
self._update()
def setMetadata(self, data: List[Dict[str, Union[str, List[str], int]]]):
if self._metadata != data:
self._metadata = data
self._update()
def _update(self):
items = []
def _update(self) -> None:
items = [] # type: List[Dict[str, Union[str, List[str], int, None]]]
if not self._metadata:
self.setItems([])
self.setItems(items)
return
for author in self._metadata:
items.append({
"id": author["author_id"],
"name": author["display_name"],
"email": author["email"] if "email" in author else None,
"website": author["website"],
"package_count": author["package_count"] if "package_count" in author else 0,
"package_types": author["package_types"] if "package_types" in author else [],
"icon_url": author["icon_url"] if "icon_url" in author else None,
"description": "Material and quality profiles from {author_name}".format(author_name = author["display_name"])
"id": author.get("author_id"),
"name": author.get("display_name"),
"email": author.get("email"),
"website": author.get("website"),
"package_count": author.get("package_count", 0),
"package_types": author.get("package_types", []),
"icon_url": author.get("icon_url"),
"description": "Material and quality profiles from {author_name}".format(author_name = author.get("display_name", ""))
})
# Filter on all the key-word arguments.
for key, value in self._filter.items():
if key is "package_types":
key_filter = lambda item, value = value: value in item["package_types"]
key_filter = lambda item, value = value: value in item["package_types"] # type: ignore
elif "*" in value:
key_filter = lambda item, key = key, value = value: self._matchRegExp(item, key, value)
key_filter = lambda item, key = key, value = value: self._matchRegExp(item, key, value) # type: ignore
else:
key_filter = lambda item, key = key, value = value: self._matchString(item, key, value)
items = filter(key_filter, items)
key_filter = lambda item, key = key, value = value: self._matchString(item, key, value) # type: ignore
items = filter(key_filter, items) # type: ignore
# Execute all filters.
filtered_items = list(items)

View file

@ -33,20 +33,22 @@ class PackagesModel(ListModel):
self.addRoleName(Qt.UserRole + 12, "last_updated")
self.addRoleName(Qt.UserRole + 13, "is_bundled")
self.addRoleName(Qt.UserRole + 14, "is_active")
self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed
self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed
self.addRoleName(Qt.UserRole + 16, "has_configs")
self.addRoleName(Qt.UserRole + 17, "supported_configs")
self.addRoleName(Qt.UserRole + 18, "download_count")
self.addRoleName(Qt.UserRole + 19, "tags")
self.addRoleName(Qt.UserRole + 20, "links")
self.addRoleName(Qt.UserRole + 21, "website")
self.addRoleName(Qt.UserRole + 22, "login_required")
# List of filters for queries. The result is the union of the each list of results.
self._filter = {} # type: Dict[str, str]
def setMetadata(self, data):
self._metadata = data
self._update()
if self._metadata != data:
self._metadata = data
self._update()
def _update(self):
items = []
@ -99,6 +101,7 @@ class PackagesModel(ListModel):
"tags": package["tags"] if "tags" in package else [],
"links": links_dict,
"website": package["website"] if "website" in package else None,
"login_required": "login-required" in package.get("tags", [])
})
# Filter on all the key-word arguments.

View file

@ -18,6 +18,7 @@ from UM.i18n import i18nCatalog
from UM.Version import Version
import cura
from cura import CuraConstants
from cura.CuraApplication import CuraApplication
from .AuthorsModel import AuthorsModel
@ -31,17 +32,17 @@ i18n_catalog = i18nCatalog("cura")
## The Toolbox class is responsible of communicating with the server through the API
class Toolbox(QObject, Extension):
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" #type: str
DEFAULT_CLOUD_API_VERSION = 1 #type: int
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
DEFAULT_CLOUD_API_VERSION = 1 # type: int
def __init__(self, application: CuraApplication) -> None:
super().__init__()
self._application = application # type: CuraApplication
self._sdk_version = None # type: Optional[Union[str, int]]
self._cloud_api_version = None # type: Optional[int]
self._cloud_api_root = None # type: Optional[str]
self._sdk_version = CuraConstants.CuraSDKVersion # type: Union[str, int]
self._cloud_api_version = CuraConstants.CuraCloudAPIVersion # type: int
self._cloud_api_root = CuraConstants.CuraCloudAPIRoot # type: str
self._api_url = None # type: Optional[str]
# Network:
@ -66,31 +67,26 @@ class Toolbox(QObject, Extension):
self._old_plugin_ids = set() # type: Set[str]
self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]]
# Data:
self._metadata = {
# The responses as given by the server parsed to a list.
self._server_response_data = {
"authors": [],
"packages": [],
"plugins_showcase": [],
"plugins_available": [],
"plugins_installed": [],
"materials_showcase": [],
"materials_available": [],
"materials_installed": [],
"materials_generic": []
"packages": []
} # type: Dict[str, List[Any]]
# Models:
self._models = {
"authors": AuthorsModel(self),
"packages": PackagesModel(self),
"plugins_showcase": PackagesModel(self),
"plugins_available": PackagesModel(self),
"plugins_installed": PackagesModel(self),
"materials_showcase": AuthorsModel(self),
"materials_available": AuthorsModel(self),
"materials_installed": PackagesModel(self),
"materials_generic": PackagesModel(self)
} # type: Dict[str, ListModel]
} # type: Dict[str, Union[AuthorsModel, PackagesModel]]
self._plugins_showcase_model = PackagesModel(self)
self._plugins_available_model = PackagesModel(self)
self._plugins_installed_model = PackagesModel(self)
self._materials_showcase_model = AuthorsModel(self)
self._materials_available_model = AuthorsModel(self)
self._materials_installed_model = PackagesModel(self)
self._materials_generic_model = PackagesModel(self)
# These properties are for keeping track of the UI state:
# ----------------------------------------------------------------------
@ -168,9 +164,6 @@ class Toolbox(QObject, Extension):
def _onAppInitialized(self) -> None:
self._plugin_registry = self._application.getPluginRegistry()
self._package_manager = self._application.getPackageManager()
self._sdk_version = self._getSDKVersion()
self._cloud_api_version = self._getCloudAPIVersion()
self._cloud_api_root = self._getCloudAPIRoot()
self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
cloud_api_root = self._cloud_api_root,
cloud_api_version = self._cloud_api_version,
@ -178,44 +171,9 @@ class Toolbox(QObject, Extension):
)
self._request_urls = {
"authors": QUrl("{base_url}/authors".format(base_url = self._api_url)),
"packages": QUrl("{base_url}/packages".format(base_url = self._api_url)),
"plugins_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)),
"plugins_available": QUrl("{base_url}/packages?package_type=plugin".format(base_url = self._api_url)),
"materials_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)),
"materials_available": QUrl("{base_url}/packages?package_type=material".format(base_url = self._api_url)),
"materials_generic": QUrl("{base_url}/packages?package_type=material&tags=generic".format(base_url = self._api_url))
"packages": QUrl("{base_url}/packages".format(base_url = self._api_url))
}
# Get the API root for the packages API depending on Cura version settings.
def _getCloudAPIRoot(self) -> str:
if not hasattr(cura, "CuraVersion"):
return self.DEFAULT_CLOUD_API_ROOT
if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): # type: ignore
return self.DEFAULT_CLOUD_API_ROOT
if not cura.CuraVersion.CuraCloudAPIRoot: # type: ignore
return self.DEFAULT_CLOUD_API_ROOT
return cura.CuraVersion.CuraCloudAPIRoot # type: ignore
# Get the cloud API version from CuraVersion
def _getCloudAPIVersion(self) -> int:
if not hasattr(cura, "CuraVersion"):
return self.DEFAULT_CLOUD_API_VERSION
if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): # type: ignore
return self.DEFAULT_CLOUD_API_VERSION
if not cura.CuraVersion.CuraCloudAPIVersion: # type: ignore
return self.DEFAULT_CLOUD_API_VERSION
return cura.CuraVersion.CuraCloudAPIVersion # type: ignore
# Get the packages version depending on Cura version settings.
def _getSDKVersion(self) -> Union[int, str]:
if not hasattr(cura, "CuraVersion"):
return self._application.getAPIVersion().getMajor()
if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
return self._application.getAPIVersion().getMajor()
if not cura.CuraVersion.CuraSDKVersion: # type: ignore
return self._application.getAPIVersion().getMajor()
return cura.CuraVersion.CuraSDKVersion # type: ignore
@pyqtSlot()
def browsePackages(self) -> None:
# Create the network manager:
@ -231,12 +189,6 @@ class Toolbox(QObject, Extension):
# Make remote requests:
self._makeRequestByType("packages")
self._makeRequestByType("authors")
# TODO: Uncomment in the future when the tag-filtered api calls work in the cloud server
# self._makeRequestByType("plugins_showcase")
# self._makeRequestByType("plugins_available")
# self._makeRequestByType("materials_showcase")
# self._makeRequestByType("materials_available")
# self._makeRequestByType("materials_generic")
# Gather installed packages:
self._updateInstalledModels()
@ -281,7 +233,7 @@ class Toolbox(QObject, Extension):
"description": plugin_data["plugin"]["description"]
}
return formatted
except:
except KeyError:
Logger.log("w", "Unable to convert plugin meta data %s", str(plugin_data))
return None
@ -319,13 +271,10 @@ class Toolbox(QObject, Extension):
if plugin_id not in all_plugin_package_ids)
self._old_plugin_metadata = {k: v for k, v in self._old_plugin_metadata.items() if k in self._old_plugin_ids}
self._metadata["plugins_installed"] = all_packages["plugin"] + list(self._old_plugin_metadata.values())
self._models["plugins_installed"].setMetadata(self._metadata["plugins_installed"])
self._plugins_installed_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values()))
self.metadataChanged.emit()
if "material" in all_packages:
self._metadata["materials_installed"] = all_packages["material"]
# TODO: ADD MATERIALS HERE ONCE MATERIALS PORTION OF TOOLBOX IS LIVE
self._models["materials_installed"].setMetadata(self._metadata["materials_installed"])
self._materials_installed_model.setMetadata(all_packages["material"])
self.metadataChanged.emit()
@pyqtSlot(str)
@ -479,7 +428,7 @@ class Toolbox(QObject, Extension):
def getRemotePackage(self, package_id: str) -> Optional[Dict]:
# TODO: make the lookup in a dict, not a loop. canUpdate is called for every item.
remote_package = None
for package in self._metadata["packages"]:
for package in self._server_response_data["packages"]:
if package["package_id"] == package_id:
remote_package = package
break
@ -491,11 +440,8 @@ class Toolbox(QObject, Extension):
def canUpdate(self, package_id: str) -> bool:
local_package = self._package_manager.getInstalledPackageInfo(package_id)
if local_package is None:
Logger.log("i", "Could not find package [%s] as installed in the package manager, fall back to check the old plugins",
package_id)
local_package = self.getOldPluginPackageMetadata(package_id)
if local_package is None:
Logger.log("i", "Could not find package [%s] in the old plugins", package_id)
return False
remote_package = self.getRemotePackage(package_id)
@ -545,8 +491,8 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, result = int)
def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int:
count = 0
for package in self._metadata["materials_installed"]:
if package["author"]["author_id"] == author_id:
for package in self._materials_installed_model.items:
if package["author_id"] == author_id:
count += 1
return count
@ -554,7 +500,7 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, result = int)
def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int:
count = 0
for package in self._metadata["packages"]:
for package in self._server_response_data["packages"]:
if package["package_type"] == "material":
if package["author"]["author_id"] == author_id:
count += 1
@ -568,34 +514,30 @@ class Toolbox(QObject, Extension):
# Check for plugins that were installed with the old plugin browser
def isOldPlugin(self, plugin_id: str) -> bool:
if plugin_id in self._old_plugin_ids:
return True
return False
return plugin_id in self._old_plugin_ids
def getOldPluginPackageMetadata(self, plugin_id: str) -> Optional[Dict[str, Any]]:
return self._old_plugin_metadata.get(plugin_id)
def loadingComplete(self) -> bool:
def isLoadingComplete(self) -> bool:
populated = 0
for list in self._metadata.items():
if len(list) > 0:
for metadata_list in self._server_response_data.items():
if metadata_list:
populated += 1
if populated == len(self._metadata.items()):
return True
return False
return populated == len(self._server_response_data.items())
# Make API Calls
# --------------------------------------------------------------------------
def _makeRequestByType(self, type: str) -> None:
Logger.log("i", "Marketplace: Requesting %s metadata from server.", type)
request = QNetworkRequest(self._request_urls[type])
def _makeRequestByType(self, request_type: str) -> None:
Logger.log("i", "Requesting %s metadata from server.", request_type)
request = QNetworkRequest(self._request_urls[request_type])
request.setRawHeader(*self._request_header)
if self._network_manager:
self._network_manager.get(request)
@pyqtSlot(str)
def startDownload(self, url: str) -> None:
Logger.log("i", "Marketplace: Attempting to download & install package from %s.", url)
Logger.log("i", "Attempting to download & install package from %s.", url)
url = QUrl(url)
self._download_request = QNetworkRequest(url)
if hasattr(QNetworkRequest, "FollowRedirectsAttribute"):
@ -612,15 +554,15 @@ class Toolbox(QObject, Extension):
@pyqtSlot()
def cancelDownload(self) -> None:
Logger.log("i", "Marketplace: User cancelled the download of a package.")
Logger.log("i", "User cancelled the download of a package.")
self.resetDownload()
def resetDownload(self) -> None:
if self._download_reply:
try:
self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
except TypeError: #Raised when the method is not connected to the signal yet.
pass #Don't need to disconnect.
except TypeError: # Raised when the method is not connected to the signal yet.
pass # Don't need to disconnect.
self._download_reply.abort()
self._download_reply = None
self._download_request = None
@ -646,22 +588,8 @@ class Toolbox(QObject, Extension):
self.resetDownload()
return
# HACK: These request are not handled independently at this moment, but together from the "packages" call
do_not_handle = [
"materials_available",
"materials_showcase",
"materials_generic",
"plugins_available",
"plugins_showcase",
]
if reply.operation() == QNetworkAccessManager.GetOperation:
for type, url in self._request_urls.items():
# HACK: Do nothing because we'll handle these from the "packages" call
if type in do_not_handle:
continue
for response_type, url in self._request_urls.items():
if reply.url() == url:
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200:
try:
@ -674,38 +602,32 @@ class Toolbox(QObject, Extension):
return
# Create model and apply metadata:
if not self._models[type]:
Logger.log("e", "Could not find the %s model.", type)
if not self._models[response_type]:
Logger.log("e", "Could not find the %s model.", response_type)
break
self._metadata[type] = json_data["data"]
self._models[type].setMetadata(self._metadata[type])
self._server_response_data[response_type] = json_data["data"]
self._models[response_type].setMetadata(self._server_response_data[response_type])
# Do some auto filtering
# TODO: Make multiple API calls in the future to handle this
if type is "packages":
self._models[type].setFilter({"type": "plugin"})
self.buildMaterialsModels()
self.buildPluginsModels()
if type is "authors":
self._models[type].setFilter({"package_types": "material"})
if type is "materials_generic":
self._models[type].setFilter({"tags": "generic"})
if response_type is "packages":
self._models[response_type].setFilter({"type": "plugin"})
self.reBuildMaterialsModels()
self.reBuildPluginsModels()
elif response_type is "authors":
self._models[response_type].setFilter({"package_types": "material"})
self._models[response_type].setFilter({"tags": "generic"})
self.metadataChanged.emit()
if self.loadingComplete() is True:
if self.isLoadingComplete():
self.setViewPage("overview")
return
except json.decoder.JSONDecodeError:
Logger.log("w", "Marketplace: Received invalid JSON for %s.", type)
Logger.log("w", "Received invalid JSON for %s.", response_type)
break
else:
self.setViewPage("errored")
self.resetDownload()
return
else:
# Ignore any operation that is not a get operation
pass
@ -716,7 +638,13 @@ class Toolbox(QObject, Extension):
self.setDownloadProgress(new_progress)
if bytes_sent == bytes_total:
self.setIsDownloading(False)
cast(QNetworkReply, self._download_reply).downloadProgress.disconnect(self._onDownloadProgress)
self._download_reply = cast(QNetworkReply, self._download_reply)
self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
# Check if the download was sucessfull
if self._download_reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
Logger.log("w", "Failed to download package. The following error was returned: %s", json.loads(bytes(self._download_reply.readAll()).decode("utf-8")))
return
# Must not delete the temporary file on Windows
self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
file_path = self._temp_plugin_file.name
@ -726,10 +654,10 @@ class Toolbox(QObject, Extension):
self._onDownloadComplete(file_path)
def _onDownloadComplete(self, file_path: str) -> None:
Logger.log("i", "Marketplace: Download complete.")
Logger.log("i", "Download complete.")
package_info = self._package_manager.getPackageInfo(file_path)
if not package_info:
Logger.log("w", "Marketplace: Package file [%s] was not a valid CuraPackage.", file_path)
Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path)
return
license_content = self._package_manager.getPackageLicense(file_path)
@ -738,7 +666,6 @@ class Toolbox(QObject, Extension):
return
self.install(file_path)
return
# Getter & Setters for Properties:
# --------------------------------------------------------------------------
@ -761,8 +688,9 @@ class Toolbox(QObject, Extension):
return self._is_downloading
def setActivePackage(self, package: Dict[str, Any]) -> None:
self._active_package = package
self.activePackageChanged.emit()
if self._active_package != package:
self._active_package = package
self.activePackageChanged.emit()
## The active package is the package that is currently being downloaded
@pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
@ -770,16 +698,18 @@ class Toolbox(QObject, Extension):
return self._active_package
def setViewCategory(self, category: str = "plugin") -> None:
self._view_category = category
self.viewChanged.emit()
if self._view_category != category:
self._view_category = category
self.viewChanged.emit()
@pyqtProperty(str, fset = setViewCategory, notify = viewChanged)
def viewCategory(self) -> str:
return self._view_category
def setViewPage(self, page: str = "overview") -> None:
self._view_page = page
self.viewChanged.emit()
if self._view_page != page:
self._view_page = page
self.viewChanged.emit()
@pyqtProperty(str, fset = setViewPage, notify = viewChanged)
def viewPage(self) -> str:
@ -787,48 +717,48 @@ class Toolbox(QObject, Extension):
# Exposed Models:
# --------------------------------------------------------------------------
@pyqtProperty(QObject, notify = metadataChanged)
@pyqtProperty(QObject, constant=True)
def authorsModel(self) -> AuthorsModel:
return cast(AuthorsModel, self._models["authors"])
@pyqtProperty(QObject, notify = metadataChanged)
@pyqtProperty(QObject, constant=True)
def packagesModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["packages"])
@pyqtProperty(QObject, notify = metadataChanged)
@pyqtProperty(QObject, constant=True)
def pluginsShowcaseModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["plugins_showcase"])
return self._plugins_showcase_model
@pyqtProperty(QObject, notify = metadataChanged)
@pyqtProperty(QObject, constant=True)
def pluginsAvailableModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["plugins_available"])
return self._plugins_available_model
@pyqtProperty(QObject, notify = metadataChanged)
@pyqtProperty(QObject, constant=True)
def pluginsInstalledModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["plugins_installed"])
return self._plugins_installed_model
@pyqtProperty(QObject, notify = metadataChanged)
@pyqtProperty(QObject, constant=True)
def materialsShowcaseModel(self) -> AuthorsModel:
return cast(AuthorsModel, self._models["materials_showcase"])
return self._materials_showcase_model
@pyqtProperty(QObject, notify = metadataChanged)
@pyqtProperty(QObject, constant=True)
def materialsAvailableModel(self) -> AuthorsModel:
return cast(AuthorsModel, self._models["materials_available"])
return self._materials_available_model
@pyqtProperty(QObject, notify = metadataChanged)
@pyqtProperty(QObject, constant=True)
def materialsInstalledModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["materials_installed"])
return self._materials_installed_model
@pyqtProperty(QObject, notify=metadataChanged)
@pyqtProperty(QObject, constant=True)
def materialsGenericModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["materials_generic"])
return self._materials_generic_model
# Filter Models:
# --------------------------------------------------------------------------
@pyqtSlot(str, str, str)
def filterModelByProp(self, model_type: str, filter_type: str, parameter: str) -> None:
if not self._models[model_type]:
Logger.log("w", "Marketplace: Couldn't filter %s model because it doesn't exist.", model_type)
Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type)
return
self._models[model_type].setFilter({filter_type: parameter})
self.filterChanged.emit()
@ -836,7 +766,7 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, "QVariantMap")
def setFilters(self, model_type: str, filter_dict: dict) -> None:
if not self._models[model_type]:
Logger.log("w", "Marketplace: Couldn't filter %s model because it doesn't exist.", model_type)
Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type)
return
self._models[model_type].setFilter(filter_dict)
self.filterChanged.emit()
@ -844,21 +774,21 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str)
def removeFilters(self, model_type: str) -> None:
if not self._models[model_type]:
Logger.log("w", "Marketplace: Couldn't remove filters on %s model because it doesn't exist.", model_type)
Logger.log("w", "Couldn't remove filters on %s model because it doesn't exist.", model_type)
return
self._models[model_type].setFilter({})
self.filterChanged.emit()
# HACK(S):
# --------------------------------------------------------------------------
def buildMaterialsModels(self) -> None:
self._metadata["materials_showcase"] = []
self._metadata["materials_available"] = []
self._metadata["materials_generic"] = []
def reBuildMaterialsModels(self) -> None:
materials_showcase_metadata = []
materials_available_metadata = []
materials_generic_metadata = []
processed_authors = [] # type: List[str]
processed_authors = [] # type: List[str]
for item in self._metadata["packages"]:
for item in self._server_response_data["packages"]:
if item["package_type"] == "material":
author = item["author"]
@ -867,30 +797,29 @@ class Toolbox(QObject, Extension):
# Generic materials to be in the same section
if "generic" in item["tags"]:
self._metadata["materials_generic"].append(item)
materials_generic_metadata.append(item)
else:
if "showcase" in item["tags"]:
self._metadata["materials_showcase"].append(author)
materials_showcase_metadata.append(author)
else:
self._metadata["materials_available"].append(author)
materials_available_metadata.append(author)
processed_authors.append(author["author_id"])
self._models["materials_showcase"].setMetadata(self._metadata["materials_showcase"])
self._models["materials_available"].setMetadata(self._metadata["materials_available"])
self._models["materials_generic"].setMetadata(self._metadata["materials_generic"])
self._materials_showcase_model.setMetadata(materials_showcase_metadata)
self._materials_available_model.setMetadata(materials_available_metadata)
self._materials_generic_model.setMetadata(materials_generic_metadata)
def buildPluginsModels(self) -> None:
self._metadata["plugins_showcase"] = []
self._metadata["plugins_available"] = []
def reBuildPluginsModels(self) -> None:
plugins_showcase_metadata = []
plugins_available_metadata = []
for item in self._metadata["packages"]:
for item in self._server_response_data["packages"]:
if item["package_type"] == "plugin":
if "showcase" in item["tags"]:
self._metadata["plugins_showcase"].append(item)
plugins_showcase_metadata.append(item)
else:
self._metadata["plugins_available"].append(item)
plugins_available_metadata.append(item)
self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"])
self._models["plugins_available"].setMetadata(self._metadata["plugins_available"])
self._plugins_showcase_model.setMetadata(plugins_showcase_metadata)
self._plugins_available_model.setMetadata(plugins_available_metadata)

View file

@ -50,6 +50,23 @@
}
}
},
"CuraDrive": {
"package_info": {
"package_id": "CuraDrive",
"package_type": "plugin",
"display_name": "Cura Backups",
"description": "Backup and restore your configuration.",
"package_version": "1.2.0",
"sdk_version": 5,
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
"display_name": "Ultimaker B.V.",
"email": "plugins@ultimaker.com",
"website": "https://ultimaker.com"
}
}
},
"CuraEngineBackend": {
"package_info": {
"package_id": "CuraEngineBackend",