mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-18 05:56:00 +08:00
Merge pull request #4960 from Ultimaker/CURA-6005_cura_drive_plugin
[4.0] CURA-6005 Added CuraDrive plugin to Cura build
This commit is contained in:
commit
980953b00c
1
.gitignore
vendored
1
.gitignore
vendored
@ -42,7 +42,6 @@ plugins/cura-siemensnx-plugin
|
||||
plugins/CuraBlenderPlugin
|
||||
plugins/CuraCloudPlugin
|
||||
plugins/CuraDrivePlugin
|
||||
plugins/CuraDrive
|
||||
plugins/CuraLiveScriptingPlugin
|
||||
plugins/CuraOpenSCADPlugin
|
||||
plugins/CuraPrintProfileCreator
|
||||
|
@ -6,6 +6,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Message import Message
|
||||
from cura import UltimakerCloudAuthentication
|
||||
|
||||
from cura.OAuth2.AuthorizationService import AuthorizationService
|
||||
from cura.OAuth2.Models import OAuth2Settings
|
||||
@ -37,15 +38,16 @@ class Account(QObject):
|
||||
self._logged_in = False
|
||||
|
||||
self._callback_port = 32118
|
||||
self._oauth_root = "https://account.ultimaker.com"
|
||||
self._cloud_api_root = "https://api.ultimaker.com"
|
||||
self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
|
||||
|
||||
self._oauth_settings = OAuth2Settings(
|
||||
OAUTH_SERVER_URL= self._oauth_root,
|
||||
CALLBACK_PORT=self._callback_port,
|
||||
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
|
||||
CLIENT_ID="um----------------------------ultimaker_cura",
|
||||
CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.write",
|
||||
CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download "
|
||||
"packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write "
|
||||
"cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write",
|
||||
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
|
||||
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
|
||||
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Tuple, Optional, TYPE_CHECKING
|
||||
from typing import Tuple, Optional, TYPE_CHECKING, Dict, Any
|
||||
|
||||
from cura.Backups.BackupsManager import BackupsManager
|
||||
|
||||
@ -24,12 +24,12 @@ class Backups:
|
||||
## Create a new back-up using the BackupsManager.
|
||||
# \return Tuple containing a ZIP file with the back-up data and a dict
|
||||
# with metadata about the back-up.
|
||||
def createBackup(self) -> Tuple[Optional[bytes], Optional[dict]]:
|
||||
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]:
|
||||
return self.manager.createBackup()
|
||||
|
||||
## Restore a back-up using the BackupsManager.
|
||||
# \param zip_file A ZIP file containing the actual back-up data.
|
||||
# \param meta_data Some metadata needed for restoring a back-up, like the
|
||||
# Cura version number.
|
||||
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
|
||||
def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None:
|
||||
return self.manager.restoreBackup(zip_file, meta_data)
|
||||
|
36
cura/ApplicationMetadata.py
Normal file
36
cura/ApplicationMetadata.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
# ---------
|
||||
# Genearl constants used in Cura
|
||||
# ---------
|
||||
DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
|
||||
DEFAULT_CURA_VERSION = "master"
|
||||
DEFAULT_CURA_BUILD_TYPE = ""
|
||||
DEFAULT_CURA_DEBUG_MODE = False
|
||||
DEFAULT_CURA_SDK_VERSION = "6.0.0"
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraAppDisplayName # type: ignore
|
||||
except ImportError:
|
||||
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraVersion # type: ignore
|
||||
except ImportError:
|
||||
CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value]
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraBuildType # type: ignore
|
||||
except ImportError:
|
||||
CuraBuildType = DEFAULT_CURA_BUILD_TYPE
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraDebugMode # type: ignore
|
||||
except ImportError:
|
||||
CuraDebugMode = DEFAULT_CURA_DEBUG_MODE
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraSDKVersion # type: ignore
|
||||
except ImportError:
|
||||
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
|
@ -117,6 +117,8 @@ from cura.ObjectsModel import ObjectsModel
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
|
||||
|
||||
from cura import ApplicationMetadata
|
||||
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Decorators import override
|
||||
|
||||
@ -164,11 +166,11 @@ class CuraApplication(QtApplication):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(name = "cura",
|
||||
app_display_name = CuraAppDisplayName,
|
||||
version = CuraVersion,
|
||||
api_version = CuraSDKVersion,
|
||||
buildtype = CuraBuildType,
|
||||
is_debug_mode = CuraDebugMode,
|
||||
app_display_name = ApplicationMetadata.CuraAppDisplayName,
|
||||
version = ApplicationMetadata.CuraVersion,
|
||||
api_version = ApplicationMetadata.CuraSDKVersion,
|
||||
buildtype = ApplicationMetadata.CuraBuildType,
|
||||
is_debug_mode = ApplicationMetadata.CuraDebugMode,
|
||||
tray_icon_name = "cura-icon-32.png",
|
||||
**kwargs)
|
||||
|
||||
@ -954,7 +956,7 @@ class CuraApplication(QtApplication):
|
||||
engine.rootContext().setContextProperty("CuraApplication", self)
|
||||
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
|
||||
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
|
||||
engine.rootContext().setContextProperty("CuraSDKVersion", CuraSDKVersion)
|
||||
engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion)
|
||||
|
||||
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@"
|
||||
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
||||
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
||||
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
|
||||
|
24
cura/UltimakerCloudAuthentication.py
Normal file
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
|
12
plugins/CuraDrive/__init__.py
Normal file
12
plugins/CuraDrive/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from .src.DrivePluginExtension import DrivePluginExtension
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
||||
|
||||
def register(app):
|
||||
return {"extension": DrivePluginExtension()}
|
8
plugins/CuraDrive/plugin.json
Normal file
8
plugins/CuraDrive/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Cura Backups",
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Backup and restore your configuration.",
|
||||
"version": "1.2.0",
|
||||
"api": 6,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
168
plugins/CuraDrive/src/DriveApiService.py
Normal file
168
plugins/CuraDrive/src/DriveApiService.py
Normal file
@ -0,0 +1,168 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Any, Optional, List, Dict
|
||||
|
||||
import requests
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal, signalemitter
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from .UploadBackupJob import UploadBackupJob
|
||||
from .Settings import Settings
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling.
|
||||
@signalemitter
|
||||
class DriveApiService:
|
||||
BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
|
||||
|
||||
# Emit signal when restoring backup started or finished.
|
||||
restoringStateChanged = Signal()
|
||||
|
||||
# Emit signal when creating backup started or finished.
|
||||
creatingStateChanged = Signal()
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._cura_api = CuraApplication.getInstance().getCuraAPI()
|
||||
|
||||
def getBackups(self) -> List[Dict[str, Any]]:
|
||||
access_token = self._cura_api.account.accessToken
|
||||
if not access_token:
|
||||
Logger.log("w", "Could not get access token.")
|
||||
return []
|
||||
|
||||
backup_list_request = requests.get(self.BACKUP_URL, headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
|
||||
# HTTP status 300s mean redirection. 400s and 500s are errors.
|
||||
# Technically 300s are not errors, but the use case here relies on "requests" to handle redirects automatically.
|
||||
if backup_list_request.status_code >= 300:
|
||||
Logger.log("w", "Could not get backups list from remote: %s", backup_list_request.text)
|
||||
Message(catalog.i18nc("@info:backup_status", "There was an error listing your backups."), title = catalog.i18nc("@info:title", "Backup")).show()
|
||||
return []
|
||||
return backup_list_request.json()["data"]
|
||||
|
||||
def createBackup(self) -> None:
|
||||
self.creatingStateChanged.emit(is_creating = True)
|
||||
|
||||
# Create the backup.
|
||||
backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup()
|
||||
if not backup_zip_file or not backup_meta_data:
|
||||
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not create backup.")
|
||||
return
|
||||
|
||||
# Create an upload entry for the backup.
|
||||
timestamp = datetime.now().isoformat()
|
||||
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
|
||||
backup_upload_url = self._requestBackupUpload(backup_meta_data, len(backup_zip_file))
|
||||
if not backup_upload_url:
|
||||
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not upload backup.")
|
||||
return
|
||||
|
||||
# Upload the backup to storage.
|
||||
upload_backup_job = UploadBackupJob(backup_upload_url, backup_zip_file)
|
||||
upload_backup_job.finished.connect(self._onUploadFinished)
|
||||
upload_backup_job.start()
|
||||
|
||||
def _onUploadFinished(self, job: "UploadBackupJob") -> None:
|
||||
if job.backup_upload_error_message != "":
|
||||
# If the job contains an error message we pass it along so the UI can display it.
|
||||
self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message)
|
||||
else:
|
||||
self.creatingStateChanged.emit(is_creating = False)
|
||||
|
||||
def restoreBackup(self, backup: Dict[str, Any]) -> None:
|
||||
self.restoringStateChanged.emit(is_restoring = True)
|
||||
download_url = backup.get("download_url")
|
||||
if not download_url:
|
||||
# If there is no download URL, we can't restore the backup.
|
||||
return self._emitRestoreError()
|
||||
|
||||
download_package = requests.get(download_url, stream = True)
|
||||
if download_package.status_code >= 300:
|
||||
# Something went wrong when attempting to download the backup.
|
||||
Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
|
||||
return self._emitRestoreError()
|
||||
|
||||
# We store the file in a temporary path fist to ensure integrity.
|
||||
temporary_backup_file = NamedTemporaryFile(delete = False)
|
||||
with open(temporary_backup_file.name, "wb") as write_backup:
|
||||
for chunk in download_package:
|
||||
write_backup.write(chunk)
|
||||
|
||||
if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash", "")):
|
||||
# Don't restore the backup if the MD5 hashes do not match.
|
||||
# This can happen if the download was interrupted.
|
||||
Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
|
||||
return self._emitRestoreError()
|
||||
|
||||
# Tell Cura to place the backup back in the user data folder.
|
||||
with open(temporary_backup_file.name, "rb") as read_backup:
|
||||
self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("metadata", {}))
|
||||
self.restoringStateChanged.emit(is_restoring = False)
|
||||
|
||||
def _emitRestoreError(self) -> None:
|
||||
self.restoringStateChanged.emit(is_restoring = False,
|
||||
error_message = catalog.i18nc("@info:backup_status",
|
||||
"There was an error trying to restore your backup."))
|
||||
|
||||
# Verify the MD5 hash of a file.
|
||||
# \param file_path Full path to the file.
|
||||
# \param known_hash The known MD5 hash of the file.
|
||||
# \return: Success or not.
|
||||
@staticmethod
|
||||
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
|
||||
with open(file_path, "rb") as read_backup:
|
||||
local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8")
|
||||
return known_hash == local_md5_hash
|
||||
|
||||
def deleteBackup(self, backup_id: str) -> bool:
|
||||
access_token = self._cura_api.account.accessToken
|
||||
if not access_token:
|
||||
Logger.log("w", "Could not get access token.")
|
||||
return False
|
||||
|
||||
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
if delete_backup.status_code >= 300:
|
||||
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
|
||||
return False
|
||||
return True
|
||||
|
||||
# Request a backup upload slot from the API.
|
||||
# \param backup_metadata: A dict containing some meta data about the backup.
|
||||
# \param backup_size The size of the backup file in bytes.
|
||||
# \return: The upload URL for the actual backup file if successful, otherwise None.
|
||||
def _requestBackupUpload(self, backup_metadata: Dict[str, Any], backup_size: int) -> Optional[str]:
|
||||
access_token = self._cura_api.account.accessToken
|
||||
if not access_token:
|
||||
Logger.log("w", "Could not get access token.")
|
||||
return None
|
||||
|
||||
backup_upload_request = requests.put(self.BACKUP_URL, json = {
|
||||
"data": {
|
||||
"backup_size": backup_size,
|
||||
"metadata": backup_metadata
|
||||
}
|
||||
}, headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
|
||||
# Any status code of 300 or above indicates an error.
|
||||
if backup_upload_request.status_code >= 300:
|
||||
Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text)
|
||||
return None
|
||||
|
||||
return backup_upload_request.json()["data"]["upload_url"]
|
162
plugins/CuraDrive/src/DrivePluginExtension.py
Normal file
162
plugins/CuraDrive/src/DrivePluginExtension.py
Normal file
@ -0,0 +1,162 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
|
||||
from UM.Extension import Extension
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from .Settings import Settings
|
||||
from .DriveApiService import DriveApiService
|
||||
|
||||
from 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):
|
||||
|
||||
# Signal emitted when the list of backups changed.
|
||||
backupsChanged = pyqtSignal()
|
||||
|
||||
# Signal emitted when restoring has started. Needed to prevent parallel restoring.
|
||||
restoringStateChanged = pyqtSignal()
|
||||
|
||||
# Signal emitted when creating has started. Needed to prevent parallel creation of backups.
|
||||
creatingStateChanged = pyqtSignal()
|
||||
|
||||
# Signal emitted when preferences changed (like auto-backup).
|
||||
preferencesChanged = pyqtSignal()
|
||||
|
||||
DATE_FORMAT = "%d/%m/%Y %H:%M:%S"
|
||||
|
||||
def __init__(self) -> None:
|
||||
QObject.__init__(self, None)
|
||||
Extension.__init__(self)
|
||||
|
||||
# Local data caching for the UI.
|
||||
self._drive_window = None # type: Optional[QObject]
|
||||
self._backups = [] # type: List[Dict[str, Any]]
|
||||
self._is_restoring_backup = False
|
||||
self._is_creating_backup = False
|
||||
|
||||
# Initialize services.
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
self._drive_api_service = DriveApiService()
|
||||
|
||||
# Attach signals.
|
||||
CuraApplication.getInstance().getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
|
||||
self._drive_api_service.restoringStateChanged.connect(self._onRestoringStateChanged)
|
||||
self._drive_api_service.creatingStateChanged.connect(self._onCreatingStateChanged)
|
||||
|
||||
# Register preferences.
|
||||
preferences.addPreference(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, False)
|
||||
preferences.addPreference(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY,
|
||||
datetime.now().strftime(self.DATE_FORMAT))
|
||||
|
||||
# Register the menu item
|
||||
self.addMenuItem(catalog.i18nc("@item:inmenu", "Manage backups"), self.showDriveWindow)
|
||||
|
||||
# Make auto-backup on boot if required.
|
||||
CuraApplication.getInstance().engineCreatedSignal.connect(self._autoBackup)
|
||||
|
||||
def showDriveWindow(self) -> None:
|
||||
if not self._drive_window:
|
||||
plugin_dir_path = CuraApplication.getInstance().getPluginRegistry().getPluginPath("CuraDrive")
|
||||
path = os.path.join(plugin_dir_path, "src", "qml", "main.qml")
|
||||
self._drive_window = CuraApplication.getInstance().createQmlComponent(path, {"CuraDrive": self})
|
||||
self.refreshBackups()
|
||||
if self._drive_window:
|
||||
self._drive_window.show()
|
||||
|
||||
def _autoBackup(self) -> None:
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
if preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._isLastBackupTooLongAgo():
|
||||
self.createBackup()
|
||||
|
||||
def _isLastBackupTooLongAgo(self) -> bool:
|
||||
current_date = datetime.now()
|
||||
last_backup_date = self._getLastBackupDate()
|
||||
date_diff = current_date - last_backup_date
|
||||
return date_diff.days > 1
|
||||
|
||||
def _getLastBackupDate(self) -> "datetime":
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
last_backup_date = preferences.getValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY)
|
||||
return datetime.strptime(last_backup_date, self.DATE_FORMAT)
|
||||
|
||||
def _storeBackupDate(self) -> None:
|
||||
backup_date = datetime.now().strftime(self.DATE_FORMAT)
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
preferences.setValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, backup_date)
|
||||
|
||||
def _onLoginStateChanged(self, logged_in: bool = False) -> None:
|
||||
if logged_in:
|
||||
self.refreshBackups()
|
||||
|
||||
def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: str = None) -> None:
|
||||
self._is_restoring_backup = is_restoring
|
||||
self.restoringStateChanged.emit()
|
||||
if error_message:
|
||||
Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
|
||||
|
||||
def _onCreatingStateChanged(self, is_creating: bool = False, error_message: str = None) -> None:
|
||||
self._is_creating_backup = is_creating
|
||||
self.creatingStateChanged.emit()
|
||||
if error_message:
|
||||
Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
|
||||
else:
|
||||
self._storeBackupDate()
|
||||
if not is_creating and not error_message:
|
||||
# We've finished creating a new backup, to the list has to be updated.
|
||||
self.refreshBackups()
|
||||
|
||||
@pyqtSlot(bool, name = "toggleAutoBackup")
|
||||
def toggleAutoBackup(self, enabled: bool) -> None:
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
preferences.setValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, enabled)
|
||||
|
||||
@pyqtProperty(bool, notify = preferencesChanged)
|
||||
def autoBackupEnabled(self) -> bool:
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
return bool(preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY))
|
||||
|
||||
@pyqtProperty("QVariantList", notify = backupsChanged)
|
||||
def backups(self) -> List[Dict[str, Any]]:
|
||||
return self._backups
|
||||
|
||||
@pyqtSlot(name = "refreshBackups")
|
||||
def refreshBackups(self) -> None:
|
||||
self._backups = self._drive_api_service.getBackups()
|
||||
self.backupsChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify = restoringStateChanged)
|
||||
def isRestoringBackup(self) -> bool:
|
||||
return self._is_restoring_backup
|
||||
|
||||
@pyqtProperty(bool, notify = creatingStateChanged)
|
||||
def isCreatingBackup(self) -> bool:
|
||||
return self._is_creating_backup
|
||||
|
||||
@pyqtSlot(str, name = "restoreBackup")
|
||||
def restoreBackup(self, backup_id: str) -> None:
|
||||
for backup in self._backups:
|
||||
if backup.get("backup_id") == backup_id:
|
||||
self._drive_api_service.restoreBackup(backup)
|
||||
return
|
||||
Logger.log("w", "Unable to find backup with the ID %s", backup_id)
|
||||
|
||||
@pyqtSlot(name = "createBackup")
|
||||
def createBackup(self) -> None:
|
||||
self._drive_api_service.createBackup()
|
||||
|
||||
@pyqtSlot(str, name = "deleteBackup")
|
||||
def deleteBackup(self, backup_id: str) -> None:
|
||||
self._drive_api_service.deleteBackup(backup_id)
|
||||
self.refreshBackups()
|
13
plugins/CuraDrive/src/Settings.py
Normal file
13
plugins/CuraDrive/src/Settings.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from cura import UltimakerCloudAuthentication
|
||||
|
||||
|
||||
class Settings:
|
||||
# Keeps the plugin settings.
|
||||
DRIVE_API_VERSION = 1
|
||||
DRIVE_API_URL = "{}/cura-drive/v{}".format(UltimakerCloudAuthentication.CuraCloudAPIRoot, str(DRIVE_API_VERSION))
|
||||
|
||||
AUTO_BACKUP_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled"
|
||||
AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date"
|
41
plugins/CuraDrive/src/UploadBackupJob.py
Normal file
41
plugins/CuraDrive/src/UploadBackupJob.py
Normal file
@ -0,0 +1,41 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import requests
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
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.
|
||||
def __init__(self, signed_upload_url: str, backup_zip: bytes) -> None:
|
||||
super().__init__()
|
||||
self._signed_upload_url = signed_upload_url
|
||||
self._backup_zip = backup_zip
|
||||
self._upload_success = False
|
||||
self.backup_upload_error_message = ""
|
||||
|
||||
def run(self) -> None:
|
||||
upload_message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1)
|
||||
upload_message.show()
|
||||
|
||||
backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip)
|
||||
upload_message.hide()
|
||||
|
||||
if backup_upload.status_code >= 300:
|
||||
self.backup_upload_error_message = backup_upload.text
|
||||
Logger.log("w", "Could not upload backup file: %s", backup_upload.text)
|
||||
Message(catalog.i18nc("@info:backup_status", "There was an error while uploading your backup."), title = self.MESSAGE_TITLE).show()
|
||||
else:
|
||||
self._upload_success = True
|
||||
Message(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."), title = self.MESSAGE_TITLE).show()
|
||||
|
||||
self.finished.emit(self)
|
0
plugins/CuraDrive/src/__init__.py
Normal file
0
plugins/CuraDrive/src/__init__.py
Normal file
37
plugins/CuraDrive/src/qml/components/BackupList.qml
Normal file
37
plugins/CuraDrive/src/qml/components/BackupList.qml
Normal file
@ -0,0 +1,37 @@
|
||||
// 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.2
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.1 as UM
|
||||
|
||||
ScrollView
|
||||
{
|
||||
property alias model: backupList.model
|
||||
width: parent.width
|
||||
ListView
|
||||
{
|
||||
id: backupList
|
||||
width: parent.width
|
||||
delegate: Item
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
BackupListItem
|
||||
{
|
||||
id: backupListItem
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: divider
|
||||
color: UM.Theme.getColor("lining")
|
||||
height: UM.Theme.getSize("default_lining").height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
plugins/CuraDrive/src/qml/components/BackupListFooter.qml
Normal file
46
plugins/CuraDrive/src/qml/components/BackupListFooter.qml
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
import "../components"
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: backupListFooter
|
||||
width: parent.width
|
||||
property bool showInfoButton: false
|
||||
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: infoButton
|
||||
text: catalog.i18nc("@button", "Want more?")
|
||||
iconSource: UM.Theme.getIcon("info")
|
||||
onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2")
|
||||
visible: backupListFooter.showInfoButton
|
||||
}
|
||||
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: createBackupButton
|
||||
text: catalog.i18nc("@button", "Backup Now")
|
||||
iconSource: UM.Theme.getIcon("plus")
|
||||
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup && !backupListFooter.showInfoButton
|
||||
onClicked: CuraDrive.createBackup()
|
||||
busy: CuraDrive.isCreatingBackup
|
||||
}
|
||||
|
||||
Cura.CheckBoxWithTooltip
|
||||
{
|
||||
id: autoBackupEnabled
|
||||
checked: CuraDrive.autoBackupEnabled
|
||||
onClicked: CuraDrive.toggleAutoBackup(autoBackupEnabled.checked)
|
||||
text: catalog.i18nc("@checkbox:description", "Auto Backup")
|
||||
tooltip: catalog.i18nc("@checkbox:description", "Automatically create a backup each day that Cura is started.")
|
||||
}
|
||||
}
|
113
plugins/CuraDrive/src/qml/components/BackupListItem.qml
Normal file
113
plugins/CuraDrive/src/qml/components/BackupListItem.qml
Normal file
@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Dialogs 1.1
|
||||
|
||||
import UM 1.1 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
Item
|
||||
{
|
||||
id: backupListItem
|
||||
width: parent.width
|
||||
height: showDetails ? dataRow.height + backupDetails.height : dataRow.height
|
||||
property bool showDetails: false
|
||||
|
||||
// Backup details toggle animation.
|
||||
Behavior on height
|
||||
{
|
||||
PropertyAnimation
|
||||
{
|
||||
duration: 70
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: dataRow
|
||||
spacing: UM.Theme.getSize("wide_margin").width
|
||||
width: parent.width
|
||||
height: 50 * screenScaleFactor
|
||||
|
||||
UM.SimpleButton
|
||||
{
|
||||
width: UM.Theme.getSize("section_icon").width
|
||||
height: UM.Theme.getSize("section_icon").height
|
||||
color: UM.Theme.getColor("small_button_text")
|
||||
hoverColor: UM.Theme.getColor("small_button_text_hover")
|
||||
iconSource: UM.Theme.getIcon("info")
|
||||
onClicked: backupListItem.showDetails = !backupListItem.showDetails
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: new Date(modelData.generated_time).toLocaleString(UM.Preferences.getValue("general/language"))
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
Layout.minimumWidth: 100 * screenScaleFactor
|
||||
Layout.maximumWidth: 500 * screenScaleFactor
|
||||
Layout.fillWidth: true
|
||||
font: UM.Theme.getFont("default")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: modelData.metadata.description
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
Layout.minimumWidth: 100 * screenScaleFactor
|
||||
Layout.maximumWidth: 500 * screenScaleFactor
|
||||
Layout.fillWidth: true
|
||||
font: UM.Theme.getFont("default")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
text: catalog.i18nc("@button", "Restore")
|
||||
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
|
||||
onClicked: confirmRestoreDialog.visible = true
|
||||
}
|
||||
|
||||
UM.SimpleButton
|
||||
{
|
||||
width: UM.Theme.getSize("message_close").width
|
||||
height: UM.Theme.getSize("message_close").height
|
||||
color: UM.Theme.getColor("small_button_text")
|
||||
hoverColor: UM.Theme.getColor("small_button_text_hover")
|
||||
iconSource: UM.Theme.getIcon("cross1")
|
||||
onClicked: confirmDeleteDialog.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
BackupListItemDetails
|
||||
{
|
||||
id: backupDetails
|
||||
backupDetailsData: modelData
|
||||
width: parent.width
|
||||
visible: parent.showDetails
|
||||
anchors.top: dataRow.bottom
|
||||
}
|
||||
|
||||
MessageDialog
|
||||
{
|
||||
id: confirmDeleteDialog
|
||||
title: catalog.i18nc("@dialog:title", "Delete Backup")
|
||||
text: catalog.i18nc("@dialog:info", "Are you sure you want to delete this backup? This cannot be undone.")
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
onYes: CuraDrive.deleteBackup(modelData.backup_id)
|
||||
}
|
||||
|
||||
MessageDialog
|
||||
{
|
||||
id: confirmRestoreDialog
|
||||
title: catalog.i18nc("@dialog:title", "Restore Backup")
|
||||
text: catalog.i18nc("@dialog:info", "You will need to restart Cura before your backup is restored. Do you want to close Cura now?")
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
onYes: CuraDrive.restoreBackup(modelData.backup_id)
|
||||
}
|
||||
}
|
@ -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 QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.1 as UM
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
id: backupDetails
|
||||
width: parent.width
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
property var backupDetailsData
|
||||
|
||||
// Cura version
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("application")
|
||||
label: catalog.i18nc("@backuplist:label", "Cura Version")
|
||||
value: backupDetailsData.metadata.cura_release
|
||||
}
|
||||
|
||||
// Machine count.
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("printer_single")
|
||||
label: catalog.i18nc("@backuplist:label", "Machines")
|
||||
value: backupDetailsData.metadata.machine_count
|
||||
}
|
||||
|
||||
// Material count
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("category_material")
|
||||
label: catalog.i18nc("@backuplist:label", "Materials")
|
||||
value: backupDetailsData.metadata.material_count
|
||||
}
|
||||
|
||||
// Profile count.
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("settings")
|
||||
label: catalog.i18nc("@backuplist:label", "Profiles")
|
||||
value: backupDetailsData.metadata.profile_count
|
||||
}
|
||||
|
||||
// Plugin count.
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("plugin")
|
||||
label: catalog.i18nc("@backuplist:label", "Plugins")
|
||||
value: backupDetailsData.metadata.plugin_count
|
||||
}
|
||||
|
||||
// Spacer.
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("default_margin").height
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: detailsRow
|
||||
width: parent.width
|
||||
height: 40 * screenScaleFactor
|
||||
|
||||
property alias iconSource: icon.source
|
||||
property alias label: detailName.text
|
||||
property alias value: detailValue.text
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: icon
|
||||
width: 18 * screenScaleFactor
|
||||
height: width
|
||||
source: ""
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: detailName
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
Layout.minimumWidth: 50 * screenScaleFactor
|
||||
Layout.maximumWidth: 100 * screenScaleFactor
|
||||
Layout.fillWidth: true
|
||||
font: UM.Theme.getFont("default")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: detailValue
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
Layout.minimumWidth: 50 * screenScaleFactor
|
||||
Layout.maximumWidth: 100 * screenScaleFactor
|
||||
Layout.fillWidth: true
|
||||
font: UM.Theme.getFont("default")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
BIN
plugins/CuraDrive/src/qml/images/icon.png
Normal file
BIN
plugins/CuraDrive/src/qml/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
plugins/CuraDrive/src/qml/images/loading.gif
Normal file
BIN
plugins/CuraDrive/src/qml/images/loading.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
44
plugins/CuraDrive/src/qml/main.qml
Normal file
44
plugins/CuraDrive/src/qml/main.qml
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
import "components"
|
||||
import "pages"
|
||||
|
||||
Window
|
||||
{
|
||||
id: curaDriveDialog
|
||||
minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width)
|
||||
minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height)
|
||||
maximumWidth: Math.round(minimumWidth * 1.2)
|
||||
maximumHeight: Math.round(minimumHeight * 1.2)
|
||||
width: minimumWidth
|
||||
height: minimumHeight
|
||||
color: UM.Theme.getColor("main_background")
|
||||
title: catalog.i18nc("@title:window", "Cura Backups")
|
||||
|
||||
// Globally available.
|
||||
UM.I18nCatalog
|
||||
{
|
||||
id: catalog
|
||||
name: "cura"
|
||||
}
|
||||
|
||||
WelcomePage
|
||||
{
|
||||
id: welcomePage
|
||||
visible: !Cura.API.account.isLoggedIn
|
||||
}
|
||||
|
||||
BackupsPage
|
||||
{
|
||||
id: backupsPage
|
||||
visible: Cura.API.account.isLoggedIn
|
||||
}
|
||||
}
|
75
plugins/CuraDrive/src/qml/pages/BackupsPage.qml
Normal file
75
plugins/CuraDrive/src/qml/pages/BackupsPage.qml
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
import "../components"
|
||||
|
||||
Item
|
||||
{
|
||||
id: backupsPage
|
||||
anchors.fill: parent
|
||||
anchors.margins: UM.Theme.getSize("wide_margin").width
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
spacing: UM.Theme.getSize("wide_margin").height
|
||||
width: parent.width
|
||||
anchors.fill: parent
|
||||
|
||||
Label
|
||||
{
|
||||
id: backupTitle
|
||||
text: catalog.i18nc("@title", "My Backups")
|
||||
font: UM.Theme.getFont("large")
|
||||
color: UM.Theme.getColor("text")
|
||||
Layout.fillWidth: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@empty_state",
|
||||
"You don't have any backups currently. Use the 'Backup Now' button to create one.")
|
||||
width: parent.width
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Label.WordWrap
|
||||
visible: backupList.count == 0
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
BackupList
|
||||
{
|
||||
id: backupList
|
||||
model: CuraDrive.backups
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@backup_limit_info",
|
||||
"During the preview phase, you'll be limited to 5 visible backups. Remove a backup to see older ones.")
|
||||
width: parent.width
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Label.WordWrap
|
||||
visible: backupList.count > 4
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
BackupListFooter
|
||||
{
|
||||
id: backupListFooter
|
||||
showInfoButton: backupList.count > 4
|
||||
}
|
||||
}
|
||||
}
|
56
plugins/CuraDrive/src/qml/pages/WelcomePage.qml
Normal file
56
plugins/CuraDrive/src/qml/pages/WelcomePage.qml
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
import "../components"
|
||||
|
||||
|
||||
Column
|
||||
{
|
||||
id: welcomePage
|
||||
spacing: UM.Theme.getSize("wide_margin").height
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
anchors.centerIn: parent
|
||||
|
||||
Image
|
||||
{
|
||||
id: profileImage
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "../images/icon.png"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Math.round(parent.width / 4)
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: welcomeTextLabel
|
||||
text: catalog.i18nc("@description", "Backup and synchronize your Cura settings.")
|
||||
width: Math.round(parent.width / 2)
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
wrapMode: Label.WordWrap
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: loginButton
|
||||
width: UM.Theme.getSize("account_button").width
|
||||
height: UM.Theme.getSize("account_button").height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: catalog.i18nc("@button", "Sign in")
|
||||
onClicked: Cura.API.account.login()
|
||||
fixedWidthMode: true
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,8 @@ from UM.Extension import Extension
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Version import Version
|
||||
|
||||
import cura
|
||||
from cura import ApplicationMetadata
|
||||
from cura import UltimakerCloudAuthentication
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from .AuthorsModel import AuthorsModel
|
||||
@ -30,17 +31,14 @@ i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
## The Toolbox class is responsible of communicating with the server through the API
|
||||
class Toolbox(QObject, Extension):
|
||||
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
|
||||
DEFAULT_CLOUD_API_VERSION = 1 # type: int
|
||||
|
||||
def __init__(self, application: CuraApplication) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._application = application # type: CuraApplication
|
||||
|
||||
self._sdk_version = None # type: Optional[Union[str, int]]
|
||||
self._cloud_api_version = None # type: Optional[int]
|
||||
self._cloud_api_root = None # type: Optional[str]
|
||||
self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
|
||||
self._cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: int
|
||||
self._cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str
|
||||
self._api_url = None # type: Optional[str]
|
||||
|
||||
# Network:
|
||||
@ -182,9 +180,6 @@ class Toolbox(QObject, Extension):
|
||||
def _onAppInitialized(self) -> None:
|
||||
self._plugin_registry = self._application.getPluginRegistry()
|
||||
self._package_manager = self._application.getPackageManager()
|
||||
self._sdk_version = self._getSDKVersion()
|
||||
self._cloud_api_version = self._getCloudAPIVersion()
|
||||
self._cloud_api_root = self._getCloudAPIRoot()
|
||||
self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
|
||||
cloud_api_root = self._cloud_api_root,
|
||||
cloud_api_version = self._cloud_api_version,
|
||||
@ -195,36 +190,6 @@ class Toolbox(QObject, Extension):
|
||||
"packages": QUrl("{base_url}/packages".format(base_url = self._api_url))
|
||||
}
|
||||
|
||||
# Get the API root for the packages API depending on Cura version settings.
|
||||
def _getCloudAPIRoot(self) -> str:
|
||||
if not hasattr(cura, "CuraVersion"):
|
||||
return self.DEFAULT_CLOUD_API_ROOT
|
||||
if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): # type: ignore
|
||||
return self.DEFAULT_CLOUD_API_ROOT
|
||||
if not cura.CuraVersion.CuraCloudAPIRoot: # type: ignore
|
||||
return self.DEFAULT_CLOUD_API_ROOT
|
||||
return cura.CuraVersion.CuraCloudAPIRoot # type: ignore
|
||||
|
||||
# Get the cloud API version from CuraVersion
|
||||
def _getCloudAPIVersion(self) -> int:
|
||||
if not hasattr(cura, "CuraVersion"):
|
||||
return self.DEFAULT_CLOUD_API_VERSION
|
||||
if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): # type: ignore
|
||||
return self.DEFAULT_CLOUD_API_VERSION
|
||||
if not cura.CuraVersion.CuraCloudAPIVersion: # type: ignore
|
||||
return self.DEFAULT_CLOUD_API_VERSION
|
||||
return cura.CuraVersion.CuraCloudAPIVersion # type: ignore
|
||||
|
||||
# Get the packages version depending on Cura version settings.
|
||||
def _getSDKVersion(self) -> Union[int, str]:
|
||||
if not hasattr(cura, "CuraVersion"):
|
||||
return self._application.getAPIVersion().getMajor()
|
||||
if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
|
||||
return self._application.getAPIVersion().getMajor()
|
||||
if not cura.CuraVersion.CuraSDKVersion: # type: ignore
|
||||
return self._application.getAPIVersion().getMajor()
|
||||
return cura.CuraVersion.CuraSDKVersion # type: ignore
|
||||
|
||||
@pyqtSlot()
|
||||
def browsePackages(self) -> None:
|
||||
# Create the network manager:
|
||||
|
@ -50,6 +50,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CuraDrive": {
|
||||
"package_info": {
|
||||
"package_id": "CuraDrive",
|
||||
"package_type": "plugin",
|
||||
"display_name": "Cura Backups",
|
||||
"description": "Backup and restore your configuration.",
|
||||
"package_version": "1.2.0",
|
||||
"sdk_version": 6,
|
||||
"website": "https://ultimaker.com",
|
||||
"author": {
|
||||
"author_id": "UltimakerPackages",
|
||||
"display_name": "Ultimaker B.V.",
|
||||
"email": "plugins@ultimaker.com",
|
||||
"website": "https://ultimaker.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CuraEngineBackend": {
|
||||
"package_info": {
|
||||
"package_id": "CuraEngineBackend",
|
||||
|
@ -4,6 +4,7 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtGraphicalEffects 1.0 // For the dropshadow
|
||||
|
||||
import UM 1.1 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
@ -30,6 +31,7 @@ Button
|
||||
property color outlineDisabledColor: outlineColor
|
||||
property alias shadowColor: shadow.color
|
||||
property alias shadowEnabled: shadow.visible
|
||||
property alias busy: busyIndicator.visible
|
||||
|
||||
property alias toolTipContentAlignment: tooltip.contentAlignment
|
||||
|
||||
@ -55,7 +57,7 @@ Button
|
||||
width: visible ? height : 0
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
color: button.hovered ? button.textHoverColor : button.textColor
|
||||
color: button.enabled ? (button.hovered ? button.textHoverColor : button.textColor) : button.textDisabledColor
|
||||
visible: source != "" && !button.isIconOnRightSide
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
@ -117,4 +119,16 @@ Button
|
||||
id: tooltip
|
||||
visible: button.hovered
|
||||
}
|
||||
|
||||
BusyIndicator
|
||||
{
|
||||
id: busyIndicator
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: height
|
||||
height: parent.height
|
||||
|
||||
visible: false
|
||||
}
|
||||
}
|
63
resources/qml/CheckBoxWithTooltip.qml
Normal file
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
|
||||
SettingView 1.0 SettingView.qml
|
||||
ProfileMenu 1.0 ProfileMenu.qml
|
||||
CheckBoxWithTooltip 1.0 CheckBoxWithTooltip.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"))
|
||||
Behavior on color { ColorAnimation { duration: 50; } }
|
||||
|
||||
radius: control.exclusiveGroup ? Math.round(Theme.getSize("checkbox").width / 2) : UM.Theme.getSize("checkbox_radius").width
|
||||
radius: control.exclusiveGroup ? Math.round(Theme.getSize("checkbox").width / 2) : Theme.getSize("checkbox_radius").width
|
||||
|
||||
border.width: Theme.getSize("default_lining").width
|
||||
border.color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_border_hover") : Theme.getColor("checkbox_border")
|
||||
|
Loading…
x
Reference in New Issue
Block a user