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

View file

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

View file

@ -1,15 +1,14 @@
# # Copyright (c) 2018 Ultimaker B.V.
# This file contains all constant values in Cura # 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_DISPLAY_NAME = "Ultimaker Cura"
DEFAULT_CURA_VERSION = "master" DEFAULT_CURA_VERSION = "master"
DEFAULT_CURA_BUILD_TYPE = "" DEFAULT_CURA_BUILD_TYPE = ""
DEFAULT_CURA_DEBUG_MODE = False DEFAULT_CURA_DEBUG_MODE = False
DEFAULT_CURA_SDK_VERSION = "5.0.0" DEFAULT_CURA_SDK_VERSION = "6.0.0"
try: try:
from cura.CuraVersion import CuraAppDisplayName # type: ignore from cura.CuraVersion import CuraAppDisplayName # type: ignore
@ -35,26 +34,3 @@ try:
from cura.CuraVersion import CuraSDKVersion # type: ignore from cura.CuraVersion import CuraSDKVersion # type: ignore
except ImportError: except ImportError:
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION 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.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
from cura import CuraConstants from cura import ApplicationMetadata
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Decorators import override from UM.Decorators import override
@ -167,11 +167,11 @@ class CuraApplication(QtApplication):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(name = "cura", super().__init__(name = "cura",
app_display_name = CuraConstants.CuraAppDisplayName, app_display_name = ApplicationMetadata.CuraAppDisplayName,
version = CuraConstants.CuraVersion, version = ApplicationMetadata.CuraVersion,
api_version = CuraConstants.CuraSDKVersion, api_version = ApplicationMetadata.CuraSDKVersion,
buildtype = CuraConstants.CuraBuildType, buildtype = ApplicationMetadata.CuraBuildType,
is_debug_mode = CuraConstants.CuraDebugMode, is_debug_mode = ApplicationMetadata.CuraDebugMode,
tray_icon_name = "cura-icon-32.png", tray_icon_name = "cura-icon-32.png",
**kwargs) **kwargs)
@ -957,7 +957,7 @@ class CuraApplication(QtApplication):
engine.rootContext().setContextProperty("CuraApplication", self) engine.rootContext().setContextProperty("CuraApplication", self)
engine.rootContext().setContextProperty("PrintInformation", self._print_information) engine.rootContext().setContextProperty("PrintInformation", self._print_information)
engine.rootContext().setContextProperty("CuraActions", self._cura_actions) 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") 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@" CuraSDKVersion = "@CURA_SDK_VERSION@"
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@" CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@" 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. # Copyright (c) 2018 Ultimaker B.V.
import os # Cura is released under the terms of the LGPLv3 or higher.
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 from .src.DrivePluginExtension import DrivePluginExtension
def getMetaData(): def getMetaData():
return {} return {}
def register(app): def register(app):
return {"extension": DrivePluginExtension(app)} return {"extension": DrivePluginExtension()}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Backup and restore your configuration.", "description": "Backup and restore your configuration.",
"version": "1.2.0", "version": "1.2.0",
"api": 5, "api": 6,
"i18n-catalog": "cura" "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 base64
import hashlib import hashlib
from datetime import datetime from datetime import datetime
@ -9,56 +11,55 @@ import requests
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message 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 .UploadBackupJob import UploadBackupJob
from .Settings import Settings 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: class DriveApiService:
""" BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
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. # Emit signal when restoring backup started or finished.
onRestoringStateChanged = Signal() restoringStateChanged = Signal()
# Emit signal when creating backup started or finished. # Emit signal when creating backup started or finished.
onCreatingStateChanged = Signal() creatingStateChanged = Signal()
def __init__(self, cura_api) -> None: def __init__(self) -> None:
"""Create a new instance of the Drive API service and set the cura_api object.""" self._cura_api = CuraApplication.getInstance().getCuraAPI()
self._cura_api = cura_api
def getBackups(self) -> List[Dict[str, Any]]: def getBackups(self) -> List[Dict[str, Any]]:
"""Get all backups from the API."""
access_token = self._cura_api.account.accessToken access_token = self._cura_api.account.accessToken
if not access_token: if not access_token:
Logger.log("w", "Could not get access token.") Logger.log("w", "Could not get access token.")
return [] 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) "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) 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, Message(catalog.i18nc("@info:backup_status", "There was an error listing your backups."), title = catalog.i18nc("@info:title", "Backup")).show()
lifetime = 10).show()
return [] return []
return backup_list_request.json()["data"] return backup_list_request.json()["data"]
def createBackup(self) -> None: def createBackup(self) -> None:
"""Create a backup and upload it to CuraDrive cloud storage.""" self.creatingStateChanged.emit(is_creating = True)
self.onCreatingStateChanged.emit(is_creating=True)
# Create the backup. # Create the backup.
backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup() backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup()
if not backup_zip_file or not backup_meta_data: 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 return
# Create an upload entry for the backup. # 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_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)) backup_upload_url = self._requestBackupUpload(backup_meta_data, len(backup_zip_file))
if not backup_upload_url: 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 return
# Upload the backup to storage. # Upload the backup to storage.
@ -75,29 +76,21 @@ class DriveApiService:
upload_backup_job.start() upload_backup_job.start()
def _onUploadFinished(self, job: "UploadBackupJob") -> None: def _onUploadFinished(self, job: "UploadBackupJob") -> None:
"""
Callback handler for the upload job.
:param job: The executed job.
"""
if job.backup_upload_error_message != "": if job.backup_upload_error_message != "":
# If the job contains an error message we pass it along so the UI can display it. # 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: else:
self.onCreatingStateChanged.emit(is_creating=False) self.creatingStateChanged.emit(is_creating = False)
def restoreBackup(self, backup: Dict[str, Any]) -> None: def restoreBackup(self, backup: Dict[str, Any]) -> None:
""" self.restoringStateChanged.emit(is_restoring = True)
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") download_url = backup.get("download_url")
if not download_url: if not download_url:
# If there is no download URL, we can't restore the backup. # If there is no download URL, we can't restore the backup.
return self._emitRestoreError() return self._emitRestoreError()
download_package = requests.get(download_url, stream = True) download_package = requests.get(download_url, stream = True)
if download_package.status_code != 200: if download_package.status_code >= 300:
# Something went wrong when attempting to download the backup. # 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) Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
return self._emitRestoreError() return self._emitRestoreError()
@ -116,60 +109,49 @@ class DriveApiService:
# Tell Cura to place the backup back in the user data folder. # Tell Cura to place the backup back in the user data folder.
with open(temporary_backup_file.name, "rb") as read_backup: with open(temporary_backup_file.name, "rb") as read_backup:
self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("data")) self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("metadata", {}))
self.onRestoringStateChanged.emit(is_restoring=False) self.restoringStateChanged.emit(is_restoring = False)
def _emitRestoreError(self, error_message: str = Settings.translatable_messages["backup_restore_error_message"]): def _emitRestoreError(self) -> None:
"""Helper method for emitting a signal when restoring failed.""" self.restoringStateChanged.emit(is_restoring = False,
self.onRestoringStateChanged.emit( error_message = catalog.i18nc("@info:backup_status",
is_restoring=False, "There was an error trying to restore your backup."))
error_message=error_message
)
# 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 @staticmethod
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool: 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: 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 return known_hash == local_md5_hash
def deleteBackup(self, backup_id: str) -> bool: 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 access_token = self._cura_api.account.accessToken
if not access_token: if not access_token:
Logger.log("w", "Could not get access token.") Logger.log("w", "Could not get access token.")
return False 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) "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) Logger.log("w", "Could not delete backup: %s", delete_backup.text)
return False return False
return True 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]: 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 access_token = self._cura_api.account.accessToken
if not access_token: if not access_token:
Logger.log("w", "Could not get access token.") Logger.log("w", "Could not get access token.")
return None return None
backup_upload_request = requests.put(self.PUT_BACKUP_URL, json={ backup_upload_request = requests.put(self.BACKUP_URL, json = {
"data": { "data": {
"backup_size": backup_size, "backup_size": backup_size,
"metadata": backup_metadata "metadata": backup_metadata
@ -178,7 +160,8 @@ class DriveApiService:
"Authorization": "Bearer {}".format(access_token) "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) Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text)
return None 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 import os
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional, List, Dict, Any
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
from UM.Extension import Extension from UM.Extension import Extension
from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from cura.CuraApplication import CuraApplication
from .Settings import Settings from .Settings import Settings
from .DriveApiService import DriveApiService 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): 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. # Signal emitted when the list of backups changed.
backupsChanged = pyqtSignal() backupsChanged = pyqtSignal()
@ -32,170 +36,127 @@ class DrivePluginExtension(QObject, Extension):
DATE_FORMAT = "%d/%m/%Y %H:%M:%S" DATE_FORMAT = "%d/%m/%Y %H:%M:%S"
def __init__(self, application): def __init__(self) -> None:
super(DrivePluginExtension, self).__init__() QObject.__init__(self, None)
Extension.__init__(self)
# Re-usable instance of application.
self._application = application
# Local data caching for the UI. # Local data caching for the UI.
self._drive_window = None # type: Optional[QObject] 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_restoring_backup = False
self._is_creating_backup = False self._is_creating_backup = False
# Initialize services. # Initialize services.
self._preferences = self._application.getPreferences() preferences = CuraApplication.getInstance().getPreferences()
self._cura_api = self._application.getCuraAPI() self._drive_api_service = DriveApiService()
self._drive_api_service = DriveApiService(self._cura_api)
# Attach signals. # Attach signals.
self._cura_api.account.loginStateChanged.connect(self._onLoginStateChanged) CuraApplication.getInstance().getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
self._drive_api_service.onRestoringStateChanged.connect(self._onRestoringStateChanged) self._drive_api_service.restoringStateChanged.connect(self._onRestoringStateChanged)
self._drive_api_service.onCreatingStateChanged.connect(self._onCreatingStateChanged) self._drive_api_service.creatingStateChanged.connect(self._onCreatingStateChanged)
# Register preferences. # Register preferences.
self._preferences.addPreference(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, False) preferences.addPreference(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, False)
self._preferences.addPreference(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, datetime.now() preferences.addPreference(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY,
.strftime(self.DATE_FORMAT)) datetime.now().strftime(self.DATE_FORMAT))
# Register menu items. # Register the menu item
self._updateMenuItems() self.addMenuItem(catalog.i18nc("@item:inmenu", "Manage backups"), self.showDriveWindow)
# Make auto-backup on boot if required. # Make auto-backup on boot if required.
self._application.engineCreatedSignal.connect(self._autoBackup) CuraApplication.getInstance().engineCreatedSignal.connect(self._autoBackup)
def showDriveWindow(self) -> None: def showDriveWindow(self) -> None:
"""Show the Drive UI popup window."""
if not self._drive_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() self.refreshBackups()
if self._drive_window: if self._drive_window:
self._drive_window.show() 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: def _autoBackup(self) -> None:
"""Automatically make a backup on boot if enabled.""" preferences = CuraApplication.getInstance().getPreferences()
if self._preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._lastBackupTooLongAgo(): if preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._isLastBackupTooLongAgo():
self.createBackup() self.createBackup()
def _lastBackupTooLongAgo(self) -> bool: def _isLastBackupTooLongAgo(self) -> bool:
"""Check if the last backup was longer than 1 day ago."""
current_date = datetime.now() current_date = datetime.now()
last_backup_date = self._getLastBackupDate() last_backup_date = self._getLastBackupDate()
date_diff = current_date - last_backup_date date_diff = current_date - last_backup_date
return date_diff.days > 1 return date_diff.days > 1
def _getLastBackupDate(self) -> "datetime": def _getLastBackupDate(self) -> "datetime":
"""Get the last backup date as datetime object.""" preferences = CuraApplication.getInstance().getPreferences()
last_backup_date = self._preferences.getValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY) last_backup_date = preferences.getValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY)
return datetime.strptime(last_backup_date, self.DATE_FORMAT) return datetime.strptime(last_backup_date, self.DATE_FORMAT)
def _storeBackupDate(self) -> None: def _storeBackupDate(self) -> None:
"""Store the current date as last backup date."""
backup_date = datetime.now().strftime(self.DATE_FORMAT) 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: def _onLoginStateChanged(self, logged_in: bool = False) -> None:
"""Callback handler for changes in the login state."""
if logged_in: if logged_in:
self.refreshBackups() self.refreshBackups()
def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: str = None) -> None: 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._is_restoring_backup = is_restoring
self.restoringStateChanged.emit() self.restoringStateChanged.emit()
if error_message: 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: 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._is_creating_backup = is_creating
self.creatingStateChanged.emit() self.creatingStateChanged.emit()
if error_message: if error_message:
Message(error_message, title = Settings.MESSAGE_TITLE, lifetime = 5).show() Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
else: else:
self._storeBackupDate() 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. # We've finished creating a new backup, to the list has to be updated.
self.refreshBackups() self.refreshBackups()
@pyqtSlot(bool, name = "toggleAutoBackup") @pyqtSlot(bool, name = "toggleAutoBackup")
def toggleAutoBackup(self, enabled: bool) -> None: def toggleAutoBackup(self, enabled: bool) -> None:
"""Enable or disable the auto-backup feature.""" preferences = CuraApplication.getInstance().getPreferences()
self._preferences.setValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, enabled) preferences.setValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, enabled)
self.preferencesChanged.emit()
@pyqtProperty(bool, notify = preferencesChanged) @pyqtProperty(bool, notify = preferencesChanged)
def autoBackupEnabled(self) -> bool: def autoBackupEnabled(self) -> bool:
"""Check if auto-backup is enabled or not.""" preferences = CuraApplication.getInstance().getPreferences()
return bool(self._preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY)) return bool(preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY))
@pyqtProperty(QObject, notify = backupsChanged) @pyqtProperty("QVariantList", notify = backupsChanged)
def backups(self) -> BackupListModel: def backups(self) -> List[Dict[str, Any]]:
""" return self._backups
Get a list of the backups.
:return: The backups as Qt List Model.
"""
return self._backups_list_model
@pyqtSlot(name = "refreshBackups") @pyqtSlot(name = "refreshBackups")
def refreshBackups(self) -> None: def refreshBackups(self) -> None:
""" self._backups = self._drive_api_service.getBackups()
Forcefully refresh the backups list.
"""
self._backups_list_model.loadBackups(self._drive_api_service.getBackups())
self.backupsChanged.emit() self.backupsChanged.emit()
@pyqtProperty(bool, notify = restoringStateChanged) @pyqtProperty(bool, notify = restoringStateChanged)
def isRestoringBackup(self) -> bool: def isRestoringBackup(self) -> bool:
"""
Get the current restoring state.
:return: Boolean if we are restoring or not.
"""
return self._is_restoring_backup return self._is_restoring_backup
@pyqtProperty(bool, notify = creatingStateChanged) @pyqtProperty(bool, notify = creatingStateChanged)
def isCreatingBackup(self) -> bool: def isCreatingBackup(self) -> bool:
"""
Get the current creating state.
:return: Boolean if we are creating or not.
"""
return self._is_creating_backup return self._is_creating_backup
@pyqtSlot(str, name = "restoreBackup") @pyqtSlot(str, name = "restoreBackup")
def restoreBackup(self, backup_id: str) -> None: def restoreBackup(self, backup_id: str) -> None:
""" for backup in self._backups:
Download and restore a backup by ID. if backup.get("backup_id") == backup_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) self._drive_api_service.restoreBackup(backup)
return
Logger.log("w", "Unable to find backup with the ID %s", backup_id)
@pyqtSlot(name = "createBackup") @pyqtSlot(name = "createBackup")
def createBackup(self) -> None: def createBackup(self) -> None:
"""
Create a new backup.
"""
self._drive_api_service.createBackup() self._drive_api_service.createBackup()
@pyqtSlot(str, name = "deleteBackup") @pyqtSlot(str, name = "deleteBackup")
def deleteBackup(self, backup_id: str) -> None: 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._drive_api_service.deleteBackup(backup_id)
self.refreshBackups() self.refreshBackups()

View file

@ -1,37 +1,13 @@
# Copyright (c) 2018 Ultimaker B.V. # 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: class Settings:
""" # Keeps the plugin settings.
Keeps the application settings.
"""
DRIVE_API_VERSION = 1 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_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled"
AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date" 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. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import requests import requests
from UM.Job import Job from UM.Job import Job
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from .Settings import Settings from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class UploadBackupJob(Job): class UploadBackupJob(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.
"""
# 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: def __init__(self, signed_upload_url: str, backup_zip: bytes) -> None:
super().__init__() super().__init__()
self._signed_upload_url = signed_upload_url self._signed_upload_url = signed_upload_url
@ -22,18 +24,18 @@ class UploadBackupJob(Job):
self.backup_upload_error_message = "" self.backup_upload_error_message = ""
def run(self) -> None: def run(self) -> None:
Message(Settings.translatable_messages["uploading_backup"], title = Settings.MESSAGE_TITLE, upload_message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1)
lifetime = 10).show() upload_message.show()
backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip) 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 self.backup_upload_error_message = backup_upload.text
Logger.log("w", "Could not upload backup file: %s", 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, Message(catalog.i18nc("@info:backup_status", "There was an error while uploading your backup."), title = self.MESSAGE_TITLE).show()
lifetime = 10).show()
else: else:
self._upload_success = True self._upload_success = True
Message(Settings.translatable_messages["uploading_backup_success"], title = Settings.MESSAGE_TITLE, Message(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."), title = self.MESSAGE_TITLE).show()
lifetime = 10).show()
self.finished.emit(self) 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. // Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import UM 1.1 as UM import UM 1.1 as UM
ScrollView
{
property alias model: backupList.model
width: parent.width
ListView ListView
{ {
id: backupList id: backupList
width: parent.width width: parent.width
clip: true
delegate: Item delegate: Item
{ {
width: parent.width width: parent.width
@ -21,11 +26,12 @@ ListView
width: parent.width width: parent.width
} }
Divider Rectangle
{ {
width: parent.width id: divider
anchors.top: backupListItem.bottom 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. // Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import UM 1.3 as UM import UM 1.3 as UM
import Cura 1.0 as Cura
import "../components" import "../components"
@ -13,30 +16,31 @@ RowLayout
width: parent.width width: parent.width
property bool showInfoButton: false property bool showInfoButton: false
ActionButton Cura.PrimaryButton
{ {
id: infoButton id: infoButton
text: catalog.i18nc("@button", "Want more?") text: catalog.i18nc("@button", "Want more?")
iconSource: "../images/info.svg" iconSource: UM.Theme.getIcon("info")
onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2") onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2")
visible: backupListFooter.showInfoButton visible: backupListFooter.showInfoButton
} }
ActionButton Cura.PrimaryButton
{ {
id: createBackupButton id: createBackupButton
text: catalog.i18nc("@button", "Backup Now") text: catalog.i18nc("@button", "Backup Now")
iconSource: "../images/backup.svg" iconSource: UM.Theme.getIcon("plus")
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup && !backupListFooter.showInfoButton
onClicked: CuraDrive.createBackup() onClicked: CuraDrive.createBackup()
busy: CuraDrive.isCreatingBackup busy: CuraDrive.isCreatingBackup
} }
ActionCheckBox Cura.CheckBoxWithTooltip
{ {
id: autoBackupEnabled id: autoBackupEnabled
checked: CuraDrive.autoBackupEnabled checked: CuraDrive.autoBackupEnabled
onClicked: CuraDrive.toggleAutoBackup(autoBackupEnabled.checked) 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. // Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Dialogs 1.1 import QtQuick.Dialogs 1.1
import UM 1.1 as UM import UM 1.1 as UM
import Cura 1.0 as Cura
Item Item
{ {
@ -25,60 +28,58 @@ Item
RowLayout RowLayout
{ {
id: dataRow id: dataRow
spacing: UM.Theme.getSize("default_margin").width * 2 spacing: UM.Theme.getSize("wide_margin").width
width: parent.width width: parent.width
height: 50 * screenScaleFactor height: 50 * screenScaleFactor
ActionButton UM.SimpleButton
{ {
color: "transparent" width: UM.Theme.getSize("section_icon").width
hoverColor: "transparent" height: UM.Theme.getSize("section_icon").height
textColor: UM.Theme.getColor("text") color: UM.Theme.getColor("small_button_text")
textHoverColor: UM.Theme.getColor("primary") hoverColor: UM.Theme.getColor("small_button_text_hover")
iconSource: "../images/info.svg" iconSource: UM.Theme.getIcon("info")
onClicked: backupListItem.showDetails = !backupListItem.showDetails onClicked: backupListItem.showDetails = !backupListItem.showDetails
} }
Label 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") color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
Layout.minimumWidth: 100 * screenScaleFactor Layout.minimumWidth: 100 * screenScaleFactor
Layout.maximumWidth: 500 * screenScaleFactor Layout.maximumWidth: 500 * screenScaleFactor
Layout.fillWidth: true Layout.fillWidth: true
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
Label Label
{ {
text: model["data"]["description"] text: modelData.metadata.description
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
Layout.minimumWidth: 100 * screenScaleFactor Layout.minimumWidth: 100 * screenScaleFactor
Layout.maximumWidth: 500 * screenScaleFactor Layout.maximumWidth: 500 * screenScaleFactor
Layout.fillWidth: true Layout.fillWidth: true
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
ActionButton Cura.SecondaryButton
{ {
text: catalog.i18nc("@button", "Restore") 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 enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
onClicked: confirmRestoreDialog.visible = true onClicked: confirmRestoreDialog.visible = true
} }
ActionButton UM.SimpleButton
{ {
color: "transparent" width: UM.Theme.getSize("message_close").width
hoverColor: "transparent" height: UM.Theme.getSize("message_close").height
textColor: UM.Theme.getColor("setting_validation_error") color: UM.Theme.getColor("small_button_text")
textHoverColor: UM.Theme.getColor("setting_validation_error") hoverColor: UM.Theme.getColor("small_button_text_hover")
iconSource: "../images/delete.svg" iconSource: UM.Theme.getIcon("cross1")
onClicked: confirmDeleteDialog.visible = true onClicked: confirmDeleteDialog.visible = true
} }
} }
@ -86,7 +87,7 @@ Item
BackupListItemDetails BackupListItemDetails
{ {
id: backupDetails id: backupDetails
backupDetailsData: model backupDetailsData: modelData
width: parent.width width: parent.width
visible: parent.showDetails visible: parent.showDetails
anchors.top: dataRow.bottom anchors.top: dataRow.bottom
@ -98,7 +99,7 @@ Item
title: catalog.i18nc("@dialog:title", "Delete Backup") 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.") text: catalog.i18nc("@dialog:info", "Are you sure you want to delete this backup? This cannot be undone.")
standardButtons: StandardButton.Yes | StandardButton.No standardButtons: StandardButton.Yes | StandardButton.No
onYes: CuraDrive.deleteBackup(model["backup_id"]) onYes: CuraDrive.deleteBackup(modelData.backup_id)
} }
MessageDialog MessageDialog
@ -107,6 +108,6 @@ Item
title: catalog.i18nc("@dialog:title", "Restore Backup") 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?") 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 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. // Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
@ -9,53 +11,53 @@ ColumnLayout
{ {
id: backupDetails id: backupDetails
width: parent.width width: parent.width
spacing: 10 * screenScaleFactor spacing: UM.Theme.getSize("default_margin").width
property var backupDetailsData property var backupDetailsData
// Cura version // Cura version
BackupListItemDetailsRow BackupListItemDetailsRow
{ {
iconSource: "../images/cura.svg" iconSource: UM.Theme.getIcon("application")
label: catalog.i18nc("@backuplist:label", "Cura Version") label: catalog.i18nc("@backuplist:label", "Cura Version")
value: backupDetailsData["data"]["cura_release"] value: backupDetailsData.metadata.cura_release
} }
// Machine count. // Machine count.
BackupListItemDetailsRow BackupListItemDetailsRow
{ {
iconSource: "../images/printer.svg" iconSource: UM.Theme.getIcon("printer_single")
label: catalog.i18nc("@backuplist:label", "Machines") label: catalog.i18nc("@backuplist:label", "Machines")
value: backupDetailsData["data"]["machine_count"] value: backupDetailsData.metadata.machine_count
} }
// Meterial count. // Material count
BackupListItemDetailsRow BackupListItemDetailsRow
{ {
iconSource: "../images/material.svg" iconSource: UM.Theme.getIcon("category_material")
label: catalog.i18nc("@backuplist:label", "Materials") label: catalog.i18nc("@backuplist:label", "Materials")
value: backupDetailsData["data"]["material_count"] value: backupDetailsData.metadata.material_count
} }
// Meterial count. // Profile count.
BackupListItemDetailsRow BackupListItemDetailsRow
{ {
iconSource: "../images/profile.svg" iconSource: UM.Theme.getIcon("settings")
label: catalog.i18nc("@backuplist:label", "Profiles") label: catalog.i18nc("@backuplist:label", "Profiles")
value: backupDetailsData["data"]["profile_count"] value: backupDetailsData.metadata.profile_count
} }
// Meterial count. // Plugin count.
BackupListItemDetailsRow BackupListItemDetailsRow
{ {
iconSource: "../images/plugin.svg" iconSource: UM.Theme.getIcon("plugin")
label: catalog.i18nc("@backuplist:label", "Plugins") label: catalog.i18nc("@backuplist:label", "Plugins")
value: backupDetailsData["data"]["plugin_count"] value: backupDetailsData.metadata.plugin_count
} }
// Spacer. // Spacer.
Item Item
{ {
width: parent.width 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. // Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
@ -11,42 +13,40 @@ RowLayout
width: parent.width width: parent.width
height: 40 * screenScaleFactor height: 40 * screenScaleFactor
property var iconSource property alias iconSource: icon.source
property var label property alias label: detailName.text
property var value property alias value: detailValue.text
// Spacing. UM.RecolorImage
Item
{
width: 40 * screenScaleFactor
}
Icon
{ {
id: icon
width: 18 * screenScaleFactor width: 18 * screenScaleFactor
iconSource: detailsRow.iconSource height: width
source: ""
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
} }
Label Label
{ {
text: detailsRow.label id: detailName
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
Layout.minimumWidth: 50 * screenScaleFactor Layout.minimumWidth: 50 * screenScaleFactor
Layout.maximumWidth: 100 * screenScaleFactor Layout.maximumWidth: 100 * screenScaleFactor
Layout.fillWidth: true Layout.fillWidth: true
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
Label Label
{ {
text: detailsRow.value id: detailValue
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
Layout.minimumWidth: 50 * screenScaleFactor Layout.minimumWidth: 50 * screenScaleFactor
Layout.maximumWidth: 100 * screenScaleFactor Layout.maximumWidth: 100 * screenScaleFactor
Layout.fillWidth: true Layout.fillWidth: true
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering 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. // Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtQuick.Window 2.2 import QtQuick.Window 2.2
@ -14,18 +16,18 @@ Window
id: curaDriveDialog id: curaDriveDialog
minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width) minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width)
minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height) minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height)
maximumWidth: minimumWidth * 1.2 maximumWidth: Math.round(minimumWidth * 1.2)
maximumHeight: minimumHeight * 1.2 maximumHeight: Math.round(minimumHeight * 1.2)
width: minimumWidth width: minimumWidth
height: minimumHeight height: minimumHeight
color: UM.Theme.getColor("sidebar") color: UM.Theme.getColor("main_background")
title: catalog.i18nc("@title:window", "Cura Backups") title: catalog.i18nc("@title:window", "Cura Backups")
// Globally available. // Globally available.
UM.I18nCatalog UM.I18nCatalog
{ {
id: catalog id: catalog
name: "cura_drive" name: "cura"
} }
WelcomePage WelcomePage

View file

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

View file

@ -1,4 +1,6 @@
// Copyright (c) 2018 Ultimaker B.V. // Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtQuick.Window 2.2 import QtQuick.Window 2.2
@ -8,12 +10,14 @@ import Cura 1.1 as Cura
import "../components" import "../components"
Column Column
{ {
id: welcomePage id: welcomePage
spacing: UM.Theme.getSize("wide_margin").height spacing: UM.Theme.getSize("wide_margin").height
width: parent.width width: parent.width
topPadding: 150 * screenScaleFactor height: childrenRect.height
anchors.centerIn: parent
Image Image
{ {
@ -38,11 +42,15 @@ Column
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
ActionButton Cura.PrimaryButton
{ {
id: loginButton id: loginButton
onClicked: Cura.API.account.login() width: UM.Theme.getSize("account_button").width
text: catalog.i18nc("@button", "Sign In") height: UM.Theme.getSize("account_button").height
anchors.horizontalCenter: parent.horizontalCenter 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.i18n import i18nCatalog
from UM.Version import Version from UM.Version import Version
import cura from cura import ApplicationMetadata
from cura import CuraConstants from cura import UltimakerCloudAuthentication
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from .AuthorsModel import AuthorsModel 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 ## The Toolbox class is responsible of communicating with the server through the API
class Toolbox(QObject, Extension): 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: def __init__(self, application: CuraApplication) -> None:
super().__init__() super().__init__()
self._application = application # type: CuraApplication self._application = application # type: CuraApplication
self._sdk_version = CuraConstants.CuraSDKVersion # type: Union[str, int] self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
self._cloud_api_version = CuraConstants.CuraCloudAPIVersion # type: int self._cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: int
self._cloud_api_root = CuraConstants.CuraCloudAPIRoot # type: str self._cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str
self._api_url = None # type: Optional[str] self._api_url = None # type: Optional[str]
# Network: # Network:

View file

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

View file

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

View file

@ -4,6 +4,7 @@
import QtQuick 2.7 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtGraphicalEffects 1.0 // For the dropshadow import QtGraphicalEffects 1.0 // For the dropshadow
import UM 1.1 as UM import UM 1.1 as UM
import Cura 1.0 as Cura import Cura 1.0 as Cura
@ -30,6 +31,7 @@ Button
property color outlineDisabledColor: outlineColor property color outlineDisabledColor: outlineColor
property alias shadowColor: shadow.color property alias shadowColor: shadow.color
property alias shadowEnabled: shadow.visible property alias shadowEnabled: shadow.visible
property alias busy: busyIndicator.visible
property alias toolTipContentAlignment: tooltip.contentAlignment property alias toolTipContentAlignment: tooltip.contentAlignment
@ -55,7 +57,7 @@ Button
width: visible ? height : 0 width: visible ? height : 0
sourceSize.width: width sourceSize.width: width
sourceSize.height: height 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 visible: source != "" && !button.isIconOnRightSide
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@ -117,4 +119,16 @@ Button
id: tooltip id: tooltip
visible: button.hovered 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" shortcut: "Ctrl+P"
onTriggered: onTriggered:
{ {
if (prepareButton.enabled) if (sliceButton.enabled)
{ {
sliceOrStopSlicing() 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 ToolbarButton 1.0 ToolbarButton.qml
SettingView 1.0 SettingView.qml SettingView 1.0 SettingView.qml
ProfileMenu 1.0 ProfileMenu.qml ProfileMenu 1.0 ProfileMenu.qml
CheckBoxWithTooltip 1.0 CheckBoxWithTooltip.qml
ToolTip 1.0 ToolTip.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")) color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_hover") : (control.enabled ? Theme.getColor("checkbox") : Theme.getColor("checkbox_disabled"))
Behavior on color { ColorAnimation { duration: 50; } } 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.width: Theme.getSize("default_lining").width
border.color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_border_hover") : Theme.getColor("checkbox_border") border.color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_border_hover") : Theme.getColor("checkbox_border")