Fix merge conflicts
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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@"
|
||||||
|
|
24
cura/UltimakerCloudAuthentication.py
Normal 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
|
|
@ -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"
|
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 {}
|
return {}
|
||||||
|
|
||||||
def register(app):
|
|
||||||
return {"extension": DrivePluginExtension(app)}
|
def register(app):
|
||||||
|
return {"extension": DrivePluginExtension()}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,35 +76,27 @@ 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()
|
||||||
|
|
||||||
# We store the file in a temporary path fist to ensure integrity.
|
# 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:
|
with open(temporary_backup_file.name, "wb") as write_backup:
|
||||||
for chunk in download_package:
|
for chunk in download_package:
|
||||||
write_backup.write(chunk)
|
write_backup.write(chunk)
|
||||||
|
@ -116,69 +109,59 @@ 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
|
||||||
}
|
}
|
||||||
}, headers={
|
}, headers = {
|
||||||
"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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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.")
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
||||||
ListView
|
ScrollView
|
||||||
{
|
{
|
||||||
|
property alias model: backupList.model
|
||||||
|
width: parent.width
|
||||||
|
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 {}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
Before Width: | Height: | Size: 3 KiB |
|
@ -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 |
|
@ -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 |
|
@ -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 |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 13 KiB |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
Before Width: | Height: | Size: 1.6 KiB |
|
@ -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 |
|
@ -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 |
Before Width: | Height: | Size: 8.1 KiB |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -194,7 +194,7 @@ Column
|
||||||
shortcut: "Ctrl+P"
|
shortcut: "Ctrl+P"
|
||||||
onTriggered:
|
onTriggered:
|
||||||
{
|
{
|
||||||
if (prepareButton.enabled)
|
if (sliceButton.enabled)
|
||||||
{
|
{
|
||||||
sliceOrStopSlicing()
|
sliceOrStopSlicing()
|
||||||
}
|
}
|
||||||
|
|
63
resources/qml/CheckBoxWithTooltip.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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")
|
||||||
|
|