Fix merge conflicts

This commit is contained in:
ChrisTerBeke 2019-01-10 15:17:53 +01:00
commit 56f0a341bc
54 changed files with 415 additions and 738 deletions

View file

@ -6,7 +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 import UltimakerCloudAuthentication
from cura.OAuth2.AuthorizationService import AuthorizationService
from cura.OAuth2.Models import OAuth2Settings
@ -38,7 +38,7 @@ class Account(QObject):
self._logged_in = False
self._callback_port = 32118
self._oauth_root = CuraConstants.CuraCloudAccountAPIRoot
self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
self._oauth_settings = OAuth2Settings(
OAUTH_SERVER_URL= self._oauth_root,

View file

@ -1,6 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Tuple, Optional, TYPE_CHECKING
from typing import Tuple, Optional, TYPE_CHECKING, Dict, Any
from cura.Backups.BackupsManager import BackupsManager
@ -24,12 +24,12 @@ class Backups:
## Create a new back-up using the BackupsManager.
# \return Tuple containing a ZIP file with the back-up data and a dict
# with metadata about the back-up.
def createBackup(self) -> Tuple[Optional[bytes], Optional[dict]]:
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]:
return self.manager.createBackup()
## Restore a back-up using the BackupsManager.
# \param zip_file A ZIP file containing the actual back-up data.
# \param meta_data Some metadata needed for restoring a back-up, like the
# Cura version number.
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None:
return self.manager.restoreBackup(zip_file, meta_data)

View file

@ -1,15 +1,14 @@
#
# This file contains all constant values in Cura
#
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# -------------
# Cura Versions
# -------------
# ---------
# Genearl constants used in Cura
# ---------
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"
DEFAULT_CURA_SDK_VERSION = "6.0.0"
try:
from cura.CuraVersion import CuraAppDisplayName # type: ignore
@ -35,26 +34,3 @@ 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

@ -117,7 +117,7 @@ from cura.ObjectsModel import ObjectsModel
from cura.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
from cura import CuraConstants
from cura import ApplicationMetadata
from UM.FlameProfiler import pyqtSlot
from UM.Decorators import override
@ -167,11 +167,11 @@ class CuraApplication(QtApplication):
def __init__(self, *args, **kwargs):
super().__init__(name = "cura",
app_display_name = CuraConstants.CuraAppDisplayName,
version = CuraConstants.CuraVersion,
api_version = CuraConstants.CuraSDKVersion,
buildtype = CuraConstants.CuraBuildType,
is_debug_mode = CuraConstants.CuraDebugMode,
app_display_name = ApplicationMetadata.CuraAppDisplayName,
version = ApplicationMetadata.CuraVersion,
api_version = ApplicationMetadata.CuraSDKVersion,
buildtype = ApplicationMetadata.CuraBuildType,
is_debug_mode = ApplicationMetadata.CuraDebugMode,
tray_icon_name = "cura-icon-32.png",
**kwargs)
@ -957,7 +957,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", CuraConstants.CuraSDKVersion)
engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion)
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")

View file

@ -8,3 +8,4 @@ CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
CuraSDKVersion = "@CURA_SDK_VERSION@"
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"

View file

@ -0,0 +1,24 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# ---------
# Constants used for the Cloud API
# ---------
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
DEFAULT_CLOUD_API_VERSION = 1 # type: int
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

@ -1,14 +1,12 @@
# Copyright (c) 2017 Ultimaker B.V.
import os
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
is_testing = os.getenv('ENV_NAME', "development") == "testing"
from .src.DrivePluginExtension import DrivePluginExtension
# 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():
def getMetaData():
return {}
def register(app):
return {"extension": DrivePluginExtension(app)}
def register(app):
return {"extension": DrivePluginExtension()}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"description": "Backup and restore your configuration.",
"version": "1.2.0",
"api": 5,
"api": 6,
"i18n-catalog": "cura"
}

View file

@ -1,4 +1,6 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import base64
import hashlib
from datetime import datetime
@ -9,56 +11,55 @@ import requests
from UM.Logger import Logger
from UM.Message import Message
from UM.Signal import Signal
from UM.Signal import Signal, signalemitter
from cura.CuraApplication import CuraApplication
from .UploadBackupJob import UploadBackupJob
from .Settings import Settings
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
## The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling.
@signalemitter
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)
BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
# Emit signal when restoring backup started or finished.
onRestoringStateChanged = Signal()
restoringStateChanged = Signal()
# Emit signal when creating backup started or finished.
onCreatingStateChanged = Signal()
creatingStateChanged = 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 __init__(self) -> None:
self._cura_api = CuraApplication.getInstance().getCuraAPI()
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={
backup_list_request = requests.get(self.BACKUP_URL, headers = {
"Authorization": "Bearer {}".format(access_token)
})
if backup_list_request.status_code > 299:
# HTTP status 300s mean redirection. 400s and 500s are errors.
# Technically 300s are not errors, but the use case here relies on "requests" to handle redirects automatically.
if backup_list_request.status_code >= 300:
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()
Message(catalog.i18nc("@info:backup_status", "There was an error listing your backups."), title = catalog.i18nc("@info:title", "Backup")).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)
self.creatingStateChanged.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.")
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not create backup.")
return
# Create an upload entry for the backup.
@ -66,7 +67,7 @@ class DriveApiService:
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.")
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not upload backup.")
return
# Upload the backup to storage.
@ -75,35 +76,27 @@ class DriveApiService:
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)
self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message)
else:
self.onCreatingStateChanged.emit(is_creating=False)
self.creatingStateChanged.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)
self.restoringStateChanged.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:
download_package = requests.get(download_url, stream = True)
if download_package.status_code >= 300:
# 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)
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)
@ -116,69 +109,59 @@ class DriveApiService:
# 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)
self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("metadata", {}))
self.restoringStateChanged.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
)
def _emitRestoreError(self) -> None:
self.restoringStateChanged.emit(is_restoring = False,
error_message = catalog.i18nc("@info:backup_status",
"There was an error trying to restore your backup."))
# 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.
@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")
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 = {
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
"Authorization": "Bearer {}".format(access_token)
})
if delete_backup.status_code > 299:
if delete_backup.status_code >= 300:
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
return False
return True
# 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.
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={
backup_upload_request = requests.put(self.BACKUP_URL, json = {
"data": {
"backup_size": backup_size,
"metadata": backup_metadata
}
}, headers={
}, headers = {
"Authorization": "Bearer {}".format(access_token)
})
if backup_upload_request.status_code > 299:
# Any status code of 300 or above indicates an error.
if backup_upload_request.status_code >= 300:
Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text)
return None

View file

@ -1,22 +1,26 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from datetime import datetime
from typing import Optional
from typing import Optional, List, Dict, Any
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
from UM.Extension import Extension
from UM.Logger import Logger
from UM.Message import Message
from cura.CuraApplication import CuraApplication
from .Settings import Settings
from .DriveApiService import DriveApiService
from .models.BackupListModel import BackupListModel
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
# The DivePluginExtension provides functionality to backup and restore your Cura configuration to Ultimaker's cloud.
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()
@ -32,170 +36,127 @@ class DrivePluginExtension(QObject, Extension):
DATE_FORMAT = "%d/%m/%Y %H:%M:%S"
def __init__(self, application):
super(DrivePluginExtension, self).__init__()
# Re-usable instance of application.
self._application = application
def __init__(self) -> None:
QObject.__init__(self, None)
Extension.__init__(self)
# Local data caching for the UI.
self._drive_window = None # type: Optional[QObject]
self._backups_list_model = BackupListModel()
self._backups = [] # type: List[Dict[str, Any]]
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)
preferences = CuraApplication.getInstance().getPreferences()
self._drive_api_service = DriveApiService()
# 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)
CuraApplication.getInstance().getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
self._drive_api_service.restoringStateChanged.connect(self._onRestoringStateChanged)
self._drive_api_service.creatingStateChanged.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))
preferences.addPreference(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, False)
preferences.addPreference(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY,
datetime.now().strftime(self.DATE_FORMAT))
# Register menu items.
self._updateMenuItems()
# Register the menu item
self.addMenuItem(catalog.i18nc("@item:inmenu", "Manage backups"), self.showDriveWindow)
# Make auto-backup on boot if required.
self._application.engineCreatedSignal.connect(self._autoBackup)
CuraApplication.getInstance().engineCreatedSignal.connect(self._autoBackup)
def showDriveWindow(self) -> None:
"""Show the Drive UI popup window."""
if not self._drive_window:
self._drive_window = self.createDriveWindow()
plugin_dir_path = CuraApplication.getInstance().getPluginRegistry().getPluginPath("CuraDrive")
path = os.path.join(plugin_dir_path, "src", "qml", "main.qml")
self._drive_window = CuraApplication.getInstance().createQmlComponent(path, {"CuraDrive": self})
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():
preferences = CuraApplication.getInstance().getPreferences()
if preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._isLastBackupTooLongAgo():
self.createBackup()
def _lastBackupTooLongAgo(self) -> bool:
"""Check if the last backup was longer than 1 day ago."""
def _isLastBackupTooLongAgo(self) -> bool:
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)
preferences = CuraApplication.getInstance().getPreferences()
last_backup_date = 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)
preferences = CuraApplication.getInstance().getPreferences()
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()
Message(error_message, title = catalog.i18nc("@info:title", "Backup")).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()
Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
else:
self._storeBackupDate()
if not is_creating:
if not is_creating and not error_message:
# 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()
preferences = CuraApplication.getInstance().getPreferences()
preferences.setValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, enabled)
@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))
preferences = CuraApplication.getInstance().getPreferences()
return bool(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
@pyqtProperty("QVariantList", notify = backupsChanged)
def backups(self) -> List[Dict[str, Any]]:
return self._backups
@pyqtSlot(name = "refreshBackups")
def refreshBackups(self) -> None:
"""
Forcefully refresh the backups list.
"""
self._backups_list_model.loadBackups(self._drive_api_service.getBackups())
self._backups = 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)
for backup in self._backups:
if backup.get("backup_id") == backup_id:
self._drive_api_service.restoreBackup(backup)
return
Logger.log("w", "Unable to find backup with the ID %s", backup_id)
@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

@ -1,37 +1,13 @@
# Copyright (c) 2018 Ultimaker B.V.
from UM import i18nCatalog
# Cura is released under the terms of the LGPLv3 or higher.
from cura import CuraConstants
from cura import UltimakerCloudAuthentication
class Settings:
"""
Keeps the application settings.
"""
# Keeps the plugin settings.
DRIVE_API_VERSION = 1
DRIVE_API_URL = "{}/cura-drive/v{}".format(CuraConstants.CuraCloudAPIRoot, str(DRIVE_API_VERSION))
DRIVE_API_URL = "{}/cura-drive/v{}".format(UltimakerCloudAuthentication.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

@ -1,19 +1,21 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import requests
from UM.Job import Job
from UM.Logger import Logger
from UM.Message import Message
from .Settings import Settings
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
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.
"""
MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups")
# 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
@ -22,18 +24,18 @@ class UploadBackupJob(Job):
self.backup_upload_error_message = ""
def run(self) -> None:
Message(Settings.translatable_messages["uploading_backup"], title = Settings.MESSAGE_TITLE,
lifetime = 10).show()
upload_message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1)
upload_message.show()
backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip)
if backup_upload.status_code not in (200, 201):
upload_message.hide()
if backup_upload.status_code >= 300:
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()
Message(catalog.i18nc("@info:backup_status", "There was an error while uploading your backup."), title = self.MESSAGE_TITLE).show()
else:
self._upload_success = True
Message(Settings.translatable_messages["uploading_backup_success"], title = Settings.MESSAGE_TITLE,
lifetime = 10).show()
Message(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."), title = self.MESSAGE_TITLE).show()
self.finished.emit(self)

View file

@ -1,38 +0,0 @@
# 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

@ -1,67 +0,0 @@
// 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

@ -1,49 +0,0 @@
// 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

@ -1,29 +0,0 @@
// 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

@ -1,15 +1,20 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import UM 1.1 as UM
ListView
ScrollView
{
property alias model: backupList.model
width: parent.width
ListView
{
id: backupList
width: parent.width
clip: true
delegate: Item
{
width: parent.width
@ -21,11 +26,12 @@ ListView
width: parent.width
}
Divider
Rectangle
{
width: parent.width
anchors.top: backupListItem.bottom
id: divider
color: UM.Theme.getColor("lining")
height: UM.Theme.getSize("default_lining").height
}
}
}
ScrollBar.vertical: RightSideScrollBar {}
}

View file

@ -1,9 +1,12 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import UM 1.3 as UM
import Cura 1.0 as Cura
import "../components"
@ -13,30 +16,31 @@ RowLayout
width: parent.width
property bool showInfoButton: false
ActionButton
Cura.PrimaryButton
{
id: infoButton
text: catalog.i18nc("@button", "Want more?")
iconSource: "../images/info.svg"
iconSource: UM.Theme.getIcon("info")
onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2")
visible: backupListFooter.showInfoButton
}
ActionButton
Cura.PrimaryButton
{
id: createBackupButton
text: catalog.i18nc("@button", "Backup Now")
iconSource: "../images/backup.svg"
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
iconSource: UM.Theme.getIcon("plus")
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup && !backupListFooter.showInfoButton
onClicked: CuraDrive.createBackup()
busy: CuraDrive.isCreatingBackup
}
ActionCheckBox
Cura.CheckBoxWithTooltip
{
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.")
text: catalog.i18nc("@checkbox:description", "Auto Backup")
tooltip: catalog.i18nc("@checkbox:description", "Automatically create a backup each day that Cura is started.")
}
}

View file

@ -1,10 +1,13 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
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
import Cura 1.0 as Cura
Item
{
@ -25,60 +28,58 @@ Item
RowLayout
{
id: dataRow
spacing: UM.Theme.getSize("default_margin").width * 2
spacing: UM.Theme.getSize("wide_margin").width
width: parent.width
height: 50 * screenScaleFactor
ActionButton
UM.SimpleButton
{
color: "transparent"
hoverColor: "transparent"
textColor: UM.Theme.getColor("text")
textHoverColor: UM.Theme.getColor("primary")
iconSource: "../images/info.svg"
width: UM.Theme.getSize("section_icon").width
height: UM.Theme.getSize("section_icon").height
color: UM.Theme.getColor("small_button_text")
hoverColor: UM.Theme.getColor("small_button_text_hover")
iconSource: UM.Theme.getIcon("info")
onClicked: backupListItem.showDetails = !backupListItem.showDetails
}
Label
{
text: new Date(model["generated_time"]).toLocaleString(UM.Preferences.getValue("general/language"))
text: new Date(modelData.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
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
Label
{
text: model["data"]["description"]
text: modelData.metadata.description
color: UM.Theme.getColor("text")
elide: Text.ElideRight
Layout.minimumWidth: 100 * screenScaleFactor
Layout.maximumWidth: 500 * screenScaleFactor
Layout.fillWidth: true
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
ActionButton
Cura.SecondaryButton
{
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
UM.SimpleButton
{
color: "transparent"
hoverColor: "transparent"
textColor: UM.Theme.getColor("setting_validation_error")
textHoverColor: UM.Theme.getColor("setting_validation_error")
iconSource: "../images/delete.svg"
width: UM.Theme.getSize("message_close").width
height: UM.Theme.getSize("message_close").height
color: UM.Theme.getColor("small_button_text")
hoverColor: UM.Theme.getColor("small_button_text_hover")
iconSource: UM.Theme.getIcon("cross1")
onClicked: confirmDeleteDialog.visible = true
}
}
@ -86,7 +87,7 @@ Item
BackupListItemDetails
{
id: backupDetails
backupDetailsData: model
backupDetailsData: modelData
width: parent.width
visible: parent.showDetails
anchors.top: dataRow.bottom
@ -98,7 +99,7 @@ Item
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"])
onYes: CuraDrive.deleteBackup(modelData.backup_id)
}
MessageDialog
@ -107,6 +108,6 @@ Item
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"])
onYes: CuraDrive.restoreBackup(modelData.backup_id)
}
}

View file

@ -1,4 +1,6 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
@ -9,53 +11,53 @@ ColumnLayout
{
id: backupDetails
width: parent.width
spacing: 10 * screenScaleFactor
spacing: UM.Theme.getSize("default_margin").width
property var backupDetailsData
// Cura version
BackupListItemDetailsRow
{
iconSource: "../images/cura.svg"
iconSource: UM.Theme.getIcon("application")
label: catalog.i18nc("@backuplist:label", "Cura Version")
value: backupDetailsData["data"]["cura_release"]
value: backupDetailsData.metadata.cura_release
}
// Machine count.
BackupListItemDetailsRow
{
iconSource: "../images/printer.svg"
iconSource: UM.Theme.getIcon("printer_single")
label: catalog.i18nc("@backuplist:label", "Machines")
value: backupDetailsData["data"]["machine_count"]
value: backupDetailsData.metadata.machine_count
}
// Meterial count.
// Material count
BackupListItemDetailsRow
{
iconSource: "../images/material.svg"
iconSource: UM.Theme.getIcon("category_material")
label: catalog.i18nc("@backuplist:label", "Materials")
value: backupDetailsData["data"]["material_count"]
value: backupDetailsData.metadata.material_count
}
// Meterial count.
// Profile count.
BackupListItemDetailsRow
{
iconSource: "../images/profile.svg"
iconSource: UM.Theme.getIcon("settings")
label: catalog.i18nc("@backuplist:label", "Profiles")
value: backupDetailsData["data"]["profile_count"]
value: backupDetailsData.metadata.profile_count
}
// Meterial count.
// Plugin count.
BackupListItemDetailsRow
{
iconSource: "../images/plugin.svg"
iconSource: UM.Theme.getIcon("plugin")
label: catalog.i18nc("@backuplist:label", "Plugins")
value: backupDetailsData["data"]["plugin_count"]
value: backupDetailsData.metadata.plugin_count
}
// Spacer.
Item
{
width: parent.width
height: 10 * screenScaleFactor
height: UM.Theme.getSize("default_margin").height
}
}

View file

@ -1,4 +1,6 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
@ -11,42 +13,40 @@ RowLayout
width: parent.width
height: 40 * screenScaleFactor
property var iconSource
property var label
property var value
property alias iconSource: icon.source
property alias label: detailName.text
property alias value: detailValue.text
// Spacing.
Item
{
width: 40 * screenScaleFactor
}
Icon
UM.RecolorImage
{
id: icon
width: 18 * screenScaleFactor
iconSource: detailsRow.iconSource
height: width
source: ""
color: UM.Theme.getColor("text")
}
Label
{
text: detailsRow.label
id: detailName
color: UM.Theme.getColor("text")
elide: Text.ElideRight
Layout.minimumWidth: 50 * screenScaleFactor
Layout.maximumWidth: 100 * screenScaleFactor
Layout.fillWidth: true
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
Label
{
text: detailsRow.value
id: detailValue
color: UM.Theme.getColor("text")
elide: Text.ElideRight
Layout.minimumWidth: 50 * screenScaleFactor
Layout.maximumWidth: 100 * screenScaleFactor
Layout.fillWidth: true
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
}

View file

@ -1,11 +0,0 @@
// 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

@ -1,56 +0,0 @@
// 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

@ -1,13 +0,0 @@
// 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.

Before

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,12 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 734 B

View file

@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 154 B

View file

@ -1,7 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View file

@ -1,7 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 281 B

View file

@ -1,7 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 295 B

View file

@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,4 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,7 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 628 B

View file

@ -1,7 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -1,14 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,7 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 691 B

View file

@ -1,4 +1,6 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Window 2.2
@ -14,18 +16,18 @@ 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
maximumWidth: Math.round(minimumWidth * 1.2)
maximumHeight: Math.round(minimumHeight * 1.2)
width: minimumWidth
height: minimumHeight
color: UM.Theme.getColor("sidebar")
color: UM.Theme.getColor("main_background")
title: catalog.i18nc("@title:window", "Cura Backups")
// Globally available.
UM.I18nCatalog
{
id: catalog
name: "cura_drive"
name: "cura"
}
WelcomePage

View file

@ -1,4 +1,6 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
@ -12,11 +14,11 @@ Item
{
id: backupsPage
anchors.fill: parent
anchors.margins: UM.Theme.getSize("default_margin").width * 3
anchors.margins: UM.Theme.getSize("wide_margin").width
ColumnLayout
{
spacing: UM.Theme.getSize("default_margin").height * 2
spacing: UM.Theme.getSize("wide_margin").height
width: parent.width
anchors.fill: parent

View file

@ -1,4 +1,6 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Window 2.2
@ -8,12 +10,14 @@ import Cura 1.1 as Cura
import "../components"
Column
{
id: welcomePage
spacing: UM.Theme.getSize("wide_margin").height
width: parent.width
topPadding: 150 * screenScaleFactor
height: childrenRect.height
anchors.centerIn: parent
Image
{
@ -38,11 +42,15 @@ Column
renderType: Text.NativeRendering
}
ActionButton
Cura.PrimaryButton
{
id: loginButton
onClicked: Cura.API.account.login()
text: catalog.i18nc("@button", "Sign In")
width: UM.Theme.getSize("account_button").width
height: UM.Theme.getSize("account_button").height
anchors.horizontalCenter: parent.horizontalCenter
text: catalog.i18nc("@button", "Sign in")
onClicked: Cura.API.account.login()
fixedWidthMode: true
}
}

View file

@ -16,8 +16,8 @@ from UM.Extension import Extension
from UM.i18n import i18nCatalog
from UM.Version import Version
import cura
from cura import CuraConstants
from cura import ApplicationMetadata
from cura import UltimakerCloudAuthentication
from cura.CuraApplication import CuraApplication
from .AuthorsModel import AuthorsModel
@ -31,17 +31,14 @@ 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
def __init__(self, application: CuraApplication) -> None:
super().__init__()
self._application = application # type: CuraApplication
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._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
self._cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: int
self._cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str
self._api_url = None # type: Optional[str]
# Network:

View file

@ -9,7 +9,7 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
from UM.Logger import Logger
from cura import CuraConstants
from cura import UltimakerCloudAuthentication
from cura.API import Account
from .MeshUploader import MeshUploader
from ..Models import BaseModel
@ -30,7 +30,7 @@ CloudApiClientModel = TypeVar("Model", bound = BaseModel)
class CloudApiClient:
# The cloud URL to use for this remote cluster.
ROOT_PATH = CuraConstants.CuraCloudAPIRoot
ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot
CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH)
CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH)

View file

@ -57,7 +57,7 @@
"display_name": "Cura Backups",
"description": "Backup and restore your configuration.",
"package_version": "1.2.0",
"sdk_version": 5,
"sdk_version": 6,
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",

View file

@ -4,6 +4,7 @@
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtGraphicalEffects 1.0 // For the dropshadow
import UM 1.1 as UM
import Cura 1.0 as Cura
@ -30,6 +31,7 @@ Button
property color outlineDisabledColor: outlineColor
property alias shadowColor: shadow.color
property alias shadowEnabled: shadow.visible
property alias busy: busyIndicator.visible
property alias toolTipContentAlignment: tooltip.contentAlignment
@ -55,7 +57,7 @@ Button
width: visible ? height : 0
sourceSize.width: width
sourceSize.height: height
color: button.hovered ? button.textHoverColor : button.textColor
color: button.enabled ? (button.hovered ? button.textHoverColor : button.textColor) : button.textDisabledColor
visible: source != "" && !button.isIconOnRightSide
anchors.verticalCenter: parent.verticalCenter
}
@ -117,4 +119,16 @@ Button
id: tooltip
visible: button.hovered
}
BusyIndicator
{
id: busyIndicator
anchors.centerIn: parent
width: height
height: parent.height
visible: false
}
}

View file

@ -194,7 +194,7 @@ Column
shortcut: "Ctrl+P"
onTriggered:
{
if (prepareButton.enabled)
if (sliceButton.enabled)
{
sliceOrStopSlicing()
}

View file

@ -0,0 +1,63 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 2.1
import UM 1.3 as UM
CheckBox
{
id: checkbox
hoverEnabled: true
property alias tooltip: tooltip.text
indicator: Rectangle
{
implicitWidth: UM.Theme.getSize("checkbox").width
implicitHeight: UM.Theme.getSize("checkbox").height
x: 0
anchors.verticalCenter: parent.verticalCenter
color: UM.Theme.getColor("main_background")
radius: UM.Theme.getSize("checkbox_radius").width
border.width: UM.Theme.getSize("default_lining").width
border.color: checkbox.hovered ? UM.Theme.getColor("checkbox_border_hover") : UM.Theme.getColor("checkbox_border")
UM.RecolorImage
{
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.round(parent.width / 2.5)
height: Math.round(parent.height / 2.5)
sourceSize.height: width
color: UM.Theme.getColor("checkbox_mark")
source: UM.Theme.getIcon("check")
opacity: checkbox.checked
Behavior on opacity { NumberAnimation { duration: 100; } }
}
}
contentItem: Label
{
anchors
{
left: checkbox.indicator.right
leftMargin: UM.Theme.getSize("narrow_margin").width
}
text: checkbox.text
color: UM.Theme.getColor("checkbox_text")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
ToolTip
{
id: tooltip
text: ""
delay: 500
visible: text != "" && checkbox.hovered
}
}

View file

@ -15,4 +15,5 @@ ViewsSelector 1.0 ViewsSelector.qml
ToolbarButton 1.0 ToolbarButton.qml
SettingView 1.0 SettingView.qml
ProfileMenu 1.0 ProfileMenu.qml
CheckBoxWithTooltip 1.0 CheckBoxWithTooltip.qml
ToolTip 1.0 ToolTip.qml

View file

@ -478,7 +478,7 @@ QtObject
color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_hover") : (control.enabled ? Theme.getColor("checkbox") : Theme.getColor("checkbox_disabled"))
Behavior on color { ColorAnimation { duration: 50; } }
radius: control.exclusiveGroup ? Math.round(Theme.getSize("checkbox").width / 2) : UM.Theme.getSize("checkbox_radius").width
radius: control.exclusiveGroup ? Math.round(Theme.getSize("checkbox").width / 2) : Theme.getSize("checkbox_radius").width
border.width: Theme.getSize("default_lining").width
border.color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_border_hover") : Theme.getColor("checkbox_border")