Added CuraDirve plugin to Cura build
CURA-6005
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
|
||||
|
14
plugins/CuraDrive/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
import os
|
||||
|
||||
is_testing = os.getenv('ENV_NAME', "development") == "testing"
|
||||
|
||||
# Only load the whole plugin when not running tests as __init__.py is automatically loaded by PyTest
|
||||
if not is_testing:
|
||||
from .src.DrivePluginExtension import DrivePluginExtension
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
||||
def register(app):
|
||||
return {"extension": DrivePluginExtension(app)}
|
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.1",
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura_drive"
|
||||
}
|
185
plugins/CuraDrive/src/DriveApiService.py
Normal file
@ -0,0 +1,185 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
import base64
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
import requests
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
|
||||
from .UploadBackupJob import UploadBackupJob
|
||||
from .Settings import Settings
|
||||
|
||||
|
||||
class DriveApiService:
|
||||
"""
|
||||
The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling.
|
||||
"""
|
||||
|
||||
GET_BACKUPS_URL = "{}/backups".format(Settings.DRIVE_API_URL)
|
||||
PUT_BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
|
||||
DELETE_BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
|
||||
|
||||
# Emit signal when restoring backup started or finished.
|
||||
onRestoringStateChanged = Signal()
|
||||
|
||||
# Emit signal when creating backup started or finished.
|
||||
onCreatingStateChanged = Signal()
|
||||
|
||||
def __init__(self, cura_api) -> None:
|
||||
"""Create a new instance of the Drive API service and set the cura_api object."""
|
||||
self._cura_api = cura_api
|
||||
|
||||
def getBackups(self) -> List[Dict[str, any]]:
|
||||
"""Get all backups from the API."""
|
||||
access_token = self._cura_api.account.accessToken
|
||||
if not access_token:
|
||||
Logger.log("w", "Could not get access token.")
|
||||
return []
|
||||
|
||||
backup_list_request = requests.get(self.GET_BACKUPS_URL, headers={
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
if backup_list_request.status_code > 299:
|
||||
Logger.log("w", "Could not get backups list from remote: %s", backup_list_request.text)
|
||||
Message(Settings.translatable_messages["get_backups_error"], title = Settings.MESSAGE_TITLE,
|
||||
lifetime = 10).show()
|
||||
return []
|
||||
return backup_list_request.json()["data"]
|
||||
|
||||
def createBackup(self) -> None:
|
||||
"""Create a backup and upload it to CuraDrive cloud storage."""
|
||||
self.onCreatingStateChanged.emit(is_creating=True)
|
||||
|
||||
# Create the backup.
|
||||
backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup()
|
||||
if not backup_zip_file or not backup_meta_data:
|
||||
self.onCreatingStateChanged.emit(is_creating=False, error_message="Could not create backup.")
|
||||
return
|
||||
|
||||
# Create an upload entry for the backup.
|
||||
timestamp = datetime.now().isoformat()
|
||||
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
|
||||
backup_upload_url = self._requestBackupUpload(backup_meta_data, len(backup_zip_file))
|
||||
if not backup_upload_url:
|
||||
self.onCreatingStateChanged.emit(is_creating=False, error_message="Could not upload backup.")
|
||||
return
|
||||
|
||||
# Upload the backup to storage.
|
||||
upload_backup_job = UploadBackupJob(backup_upload_url, backup_zip_file)
|
||||
upload_backup_job.finished.connect(self._onUploadFinished)
|
||||
upload_backup_job.start()
|
||||
|
||||
def _onUploadFinished(self, job: "UploadBackupJob") -> None:
|
||||
"""
|
||||
Callback handler for the upload job.
|
||||
:param job: The executed job.
|
||||
"""
|
||||
if job.backup_upload_error_message != "":
|
||||
# If the job contains an error message we pass it along so the UI can display it.
|
||||
self.onCreatingStateChanged.emit(is_creating=False, error_message=job.backup_upload_error_message)
|
||||
else:
|
||||
self.onCreatingStateChanged.emit(is_creating=False)
|
||||
|
||||
def restoreBackup(self, backup: Dict[str, any]) -> None:
|
||||
"""
|
||||
Restore a previously exported backup from cloud storage.
|
||||
:param backup: A dict containing an entry from the API list response.
|
||||
"""
|
||||
self.onRestoringStateChanged.emit(is_restoring=True)
|
||||
download_url = backup.get("download_url")
|
||||
if not download_url:
|
||||
# If there is no download URL, we can't restore the backup.
|
||||
return self._emitRestoreError()
|
||||
|
||||
download_package = requests.get(download_url, stream=True)
|
||||
if download_package.status_code != 200:
|
||||
# Something went wrong when attempting to download the backup.
|
||||
Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
|
||||
return self._emitRestoreError()
|
||||
|
||||
# We store the file in a temporary path fist to ensure integrity.
|
||||
temporary_backup_file = NamedTemporaryFile(delete=False)
|
||||
with open(temporary_backup_file.name, "wb") as write_backup:
|
||||
for chunk in download_package:
|
||||
write_backup.write(chunk)
|
||||
|
||||
if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash")):
|
||||
# Don't restore the backup if the MD5 hashes do not match.
|
||||
# This can happen if the download was interrupted.
|
||||
Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
|
||||
return self._emitRestoreError()
|
||||
|
||||
# Tell Cura to place the backup back in the user data folder.
|
||||
with open(temporary_backup_file.name, "rb") as read_backup:
|
||||
self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("data"))
|
||||
self.onRestoringStateChanged.emit(is_restoring=False)
|
||||
|
||||
def _emitRestoreError(self, error_message: str = Settings.translatable_messages["backup_restore_error_message"]):
|
||||
"""Helper method for emitting a signal when restoring failed."""
|
||||
self.onRestoringStateChanged.emit(
|
||||
is_restoring=False,
|
||||
error_message=error_message
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
|
||||
"""
|
||||
Verify the MD5 hash of a file.
|
||||
:param file_path: Full path to the file.
|
||||
:param known_hash: The known MD5 hash of the file.
|
||||
:return: Success or not.
|
||||
"""
|
||||
with open(file_path, "rb") as read_backup:
|
||||
local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars=b"_-").decode("utf-8")
|
||||
return known_hash == local_md5_hash
|
||||
|
||||
def deleteBackup(self, backup_id: str) -> bool:
|
||||
"""
|
||||
Delete a backup from the server by ID.
|
||||
:param backup_id: The ID of the backup to delete.
|
||||
:return: Success bool.
|
||||
"""
|
||||
access_token = self._cura_api.account.accessToken
|
||||
if not access_token:
|
||||
Logger.log("w", "Could not get access token.")
|
||||
return False
|
||||
|
||||
delete_backup = requests.delete("{}/{}".format(self.DELETE_BACKUP_URL, backup_id), headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
if delete_backup.status_code > 299:
|
||||
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _requestBackupUpload(self, backup_metadata: Dict[str, any], backup_size: int) -> Optional[str]:
|
||||
"""
|
||||
Request a backup upload slot from the API.
|
||||
:param backup_metadata: A dict containing some meta data about the backup.
|
||||
:param backup_size: The size of the backup file in bytes.
|
||||
:return: The upload URL for the actual backup file if successful, otherwise None.
|
||||
"""
|
||||
access_token = self._cura_api.account.accessToken
|
||||
if not access_token:
|
||||
Logger.log("w", "Could not get access token.")
|
||||
return None
|
||||
|
||||
backup_upload_request = requests.put(self.PUT_BACKUP_URL, json={
|
||||
"data": {
|
||||
"backup_size": backup_size,
|
||||
"metadata": backup_metadata
|
||||
}
|
||||
}, headers={
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
|
||||
if backup_upload_request.status_code > 299:
|
||||
Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text)
|
||||
return None
|
||||
|
||||
return backup_upload_request.json()["data"]["upload_url"]
|
200
plugins/CuraDrive/src/DrivePluginExtension.py
Normal file
@ -0,0 +1,200 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
|
||||
from UM.Extension import Extension
|
||||
from UM.Message import Message
|
||||
|
||||
from .Settings import Settings
|
||||
from .DriveApiService import DriveApiService
|
||||
from .models.BackupListModel import BackupListModel
|
||||
|
||||
|
||||
class DrivePluginExtension(QObject, Extension):
|
||||
"""
|
||||
The DivePluginExtension provides functionality to backup and restore your Cura configuration to Ultimaker's cloud.
|
||||
"""
|
||||
|
||||
# Signal emitted when the list of backups changed.
|
||||
backupsChanged = pyqtSignal()
|
||||
|
||||
# Signal emitted when restoring has started. Needed to prevent parallel restoring.
|
||||
restoringStateChanged = pyqtSignal()
|
||||
|
||||
# Signal emitted when creating has started. Needed to prevent parallel creation of backups.
|
||||
creatingStateChanged = pyqtSignal()
|
||||
|
||||
# Signal emitted when preferences changed (like auto-backup).
|
||||
preferencesChanged = pyqtSignal()
|
||||
|
||||
DATE_FORMAT = "%d/%m/%Y %H:%M:%S"
|
||||
|
||||
def __init__(self, application):
|
||||
super(DrivePluginExtension, self).__init__()
|
||||
|
||||
# Re-usable instance of application.
|
||||
self._application = application
|
||||
|
||||
# Local data caching for the UI.
|
||||
self._drive_window = None # type: Optional[QObject]
|
||||
self._backups_list_model = BackupListModel()
|
||||
self._is_restoring_backup = False
|
||||
self._is_creating_backup = False
|
||||
|
||||
# Initialize services.
|
||||
self._preferences = self._application.getPreferences()
|
||||
self._cura_api = self._application.getCuraAPI()
|
||||
self._drive_api_service = DriveApiService(self._cura_api)
|
||||
|
||||
# Attach signals.
|
||||
self._cura_api.account.loginStateChanged.connect(self._onLoginStateChanged)
|
||||
self._drive_api_service.onRestoringStateChanged.connect(self._onRestoringStateChanged)
|
||||
self._drive_api_service.onCreatingStateChanged.connect(self._onCreatingStateChanged)
|
||||
|
||||
# Register preferences.
|
||||
self._preferences.addPreference(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, False)
|
||||
self._preferences.addPreference(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, datetime.now()
|
||||
.strftime(self.DATE_FORMAT))
|
||||
|
||||
# Register menu items.
|
||||
self._updateMenuItems()
|
||||
|
||||
# Make auto-backup on boot if required.
|
||||
self._application.engineCreatedSignal.connect(self._autoBackup)
|
||||
|
||||
def showDriveWindow(self) -> None:
|
||||
"""Show the Drive UI popup window."""
|
||||
if not self._drive_window:
|
||||
self._drive_window = self.createDriveWindow()
|
||||
self.refreshBackups()
|
||||
self._drive_window.show()
|
||||
|
||||
def createDriveWindow(self) -> Optional["QObject"]:
|
||||
"""
|
||||
Create an instance of the Drive UI popup window.
|
||||
:return: The popup window object.
|
||||
"""
|
||||
path = os.path.join(os.path.dirname(__file__), "qml", "main.qml")
|
||||
return self._application.createQmlComponent(path, {"CuraDrive": self})
|
||||
|
||||
def _updateMenuItems(self) -> None:
|
||||
"""Update the menu items."""
|
||||
self.addMenuItem(Settings.translatable_messages["extension_menu_entry"], self.showDriveWindow)
|
||||
|
||||
def _autoBackup(self) -> None:
|
||||
"""Automatically make a backup on boot if enabled."""
|
||||
if self._preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._lastBackupTooLongAgo():
|
||||
self.createBackup()
|
||||
|
||||
def _lastBackupTooLongAgo(self) -> bool:
|
||||
"""Check if the last backup was longer than 1 day ago."""
|
||||
current_date = datetime.now()
|
||||
last_backup_date = self._getLastBackupDate()
|
||||
date_diff = current_date - last_backup_date
|
||||
return date_diff.days > 1
|
||||
|
||||
def _getLastBackupDate(self) -> "datetime":
|
||||
"""Get the last backup date as datetime object."""
|
||||
last_backup_date = self._preferences.getValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY)
|
||||
return datetime.strptime(last_backup_date, self.DATE_FORMAT)
|
||||
|
||||
def _storeBackupDate(self) -> None:
|
||||
"""Store the current date as last backup date."""
|
||||
backup_date = datetime.now().strftime(self.DATE_FORMAT)
|
||||
self._preferences.setValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, backup_date)
|
||||
|
||||
def _onLoginStateChanged(self, logged_in: bool = False) -> None:
|
||||
"""Callback handler for changes in the login state."""
|
||||
if logged_in:
|
||||
self.refreshBackups()
|
||||
|
||||
def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: str = None) -> None:
|
||||
"""Callback handler for changes in the restoring state."""
|
||||
self._is_restoring_backup = is_restoring
|
||||
self.restoringStateChanged.emit()
|
||||
if error_message:
|
||||
Message(error_message, title = Settings.MESSAGE_TITLE, lifetime = 5).show()
|
||||
|
||||
def _onCreatingStateChanged(self, is_creating: bool = False, error_message: str = None) -> None:
|
||||
"""Callback handler for changes in the creation state."""
|
||||
self._is_creating_backup = is_creating
|
||||
self.creatingStateChanged.emit()
|
||||
if error_message:
|
||||
Message(error_message, title = Settings.MESSAGE_TITLE, lifetime = 5).show()
|
||||
else:
|
||||
self._storeBackupDate()
|
||||
if not is_creating:
|
||||
# We've finished creating a new backup, to the list has to be updated.
|
||||
self.refreshBackups()
|
||||
|
||||
@pyqtSlot(bool, name = "toggleAutoBackup")
|
||||
def toggleAutoBackup(self, enabled: bool) -> None:
|
||||
"""Enable or disable the auto-backup feature."""
|
||||
self._preferences.setValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, enabled)
|
||||
self.preferencesChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify = preferencesChanged)
|
||||
def autoBackupEnabled(self) -> bool:
|
||||
"""Check if auto-backup is enabled or not."""
|
||||
return bool(self._preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY))
|
||||
|
||||
@pyqtProperty(QObject, notify = backupsChanged)
|
||||
def backups(self) -> BackupListModel:
|
||||
"""
|
||||
Get a list of the backups.
|
||||
:return: The backups as Qt List Model.
|
||||
"""
|
||||
return self._backups_list_model
|
||||
|
||||
@pyqtSlot(name = "refreshBackups")
|
||||
def refreshBackups(self) -> None:
|
||||
"""
|
||||
Forcefully refresh the backups list.
|
||||
"""
|
||||
self._backups_list_model.loadBackups(self._drive_api_service.getBackups())
|
||||
self.backupsChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify = restoringStateChanged)
|
||||
def isRestoringBackup(self) -> bool:
|
||||
"""
|
||||
Get the current restoring state.
|
||||
:return: Boolean if we are restoring or not.
|
||||
"""
|
||||
return self._is_restoring_backup
|
||||
|
||||
@pyqtProperty(bool, notify = creatingStateChanged)
|
||||
def isCreatingBackup(self) -> bool:
|
||||
"""
|
||||
Get the current creating state.
|
||||
:return: Boolean if we are creating or not.
|
||||
"""
|
||||
return self._is_creating_backup
|
||||
|
||||
@pyqtSlot(str, name = "restoreBackup")
|
||||
def restoreBackup(self, backup_id: str) -> None:
|
||||
"""
|
||||
Download and restore a backup by ID.
|
||||
:param backup_id: The ID of the backup.
|
||||
"""
|
||||
index = self._backups_list_model.find("backup_id", backup_id)
|
||||
backup = self._backups_list_model.getItem(index)
|
||||
self._drive_api_service.restoreBackup(backup)
|
||||
|
||||
@pyqtSlot(name = "createBackup")
|
||||
def createBackup(self) -> None:
|
||||
"""
|
||||
Create a new backup.
|
||||
"""
|
||||
self._drive_api_service.createBackup()
|
||||
|
||||
@pyqtSlot(str, name = "deleteBackup")
|
||||
def deleteBackup(self, backup_id: str) -> None:
|
||||
"""
|
||||
Delete a backup by ID.
|
||||
:param backup_id: The ID of the backup.
|
||||
"""
|
||||
self._drive_api_service.deleteBackup(backup_id)
|
||||
self.refreshBackups()
|
36
plugins/CuraDrive/src/Settings.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
from UM import i18nCatalog
|
||||
|
||||
|
||||
class Settings:
|
||||
"""
|
||||
Keeps the application settings.
|
||||
"""
|
||||
UM_CLOUD_API_ROOT = "https://api.ultimaker.com"
|
||||
DRIVE_API_VERSION = 1
|
||||
DRIVE_API_URL = "{}/cura-drive/v{}".format(UM_CLOUD_API_ROOT, str(DRIVE_API_VERSION))
|
||||
|
||||
AUTO_BACKUP_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled"
|
||||
AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date"
|
||||
|
||||
I18N_CATALOG_ID = "cura_drive"
|
||||
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.")
|
||||
}
|
39
plugins/CuraDrive/src/UploadBackupJob.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
import requests
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
|
||||
from .Settings import Settings
|
||||
|
||||
|
||||
class UploadBackupJob(Job):
|
||||
"""
|
||||
This job is responsible for uploading the backup file to cloud storage.
|
||||
As it can take longer than some other tasks, we schedule this using a Cura Job.
|
||||
"""
|
||||
|
||||
def __init__(self, signed_upload_url: str, backup_zip: bytes):
|
||||
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):
|
||||
Message(Settings.translatable_messages["uploading_backup"], title = Settings.MESSAGE_TITLE,
|
||||
lifetime = 10).show()
|
||||
|
||||
backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip)
|
||||
if backup_upload.status_code not in (200, 201):
|
||||
self.backup_upload_error_message = backup_upload.text
|
||||
Logger.log("w", "Could not upload backup file: %s", backup_upload.text)
|
||||
Message(Settings.translatable_messages["uploading_backup_error"], title = Settings.MESSAGE_TITLE,
|
||||
lifetime = 10).show()
|
||||
else:
|
||||
self._upload_success = True
|
||||
Message(Settings.translatable_messages["uploading_backup_success"], title = Settings.MESSAGE_TITLE,
|
||||
lifetime = 10).show()
|
||||
|
||||
self.finished.emit(self)
|
0
plugins/CuraDrive/src/__init__.py
Normal file
38
plugins/CuraDrive/src/models/BackupListModel.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
from typing import 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):
|
||||
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)
|
0
plugins/CuraDrive/src/models/__init__.py
Normal file
67
plugins/CuraDrive/src/qml/components/ActionButton.qml
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.1 as UM
|
||||
|
||||
Button
|
||||
{
|
||||
id: button
|
||||
property alias cursorShape: mouseArea.cursorShape
|
||||
property var iconSource: ""
|
||||
property var busy: false
|
||||
property var color: UM.Theme.getColor("primary")
|
||||
property var hoverColor: UM.Theme.getColor("primary_hover")
|
||||
property var disabledColor: color
|
||||
property var textColor: UM.Theme.getColor("button_text")
|
||||
property var textHoverColor: UM.Theme.getColor("button_text_hover")
|
||||
property var textDisabledColor: textColor
|
||||
property var textFont: UM.Theme.getFont("action_button")
|
||||
|
||||
contentItem: RowLayout
|
||||
{
|
||||
Icon
|
||||
{
|
||||
id: buttonIcon
|
||||
iconSource: button.iconSource
|
||||
width: 16 * screenScaleFactor
|
||||
color: button.hovered ? button.textHoverColor : button.textColor
|
||||
visible: button.iconSource != "" && !loader.visible
|
||||
}
|
||||
|
||||
Icon
|
||||
{
|
||||
id: loader
|
||||
iconSource: "../images/loading.gif"
|
||||
width: 16 * screenScaleFactor
|
||||
color: button.hovered ? button.textHoverColor : button.textColor
|
||||
visible: button.busy
|
||||
animated: true
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: buttonText
|
||||
text: button.text
|
||||
color: button.enabled ? (button.hovered ? button.textHoverColor : button.textColor): button.textDisabledColor
|
||||
font: button.textFont
|
||||
visible: button.text != ""
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle
|
||||
{
|
||||
color: button.enabled ? (button.hovered ? button.hoverColor : button.color) : button.disabledColor
|
||||
}
|
||||
|
||||
MouseArea
|
||||
{
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
onPressed: mouse.accepted = false
|
||||
hoverEnabled: true
|
||||
cursorShape: button.enabled ? (hovered ? Qt.PointingHandCursor : Qt.ArrowCursor) : Qt.ForbiddenCursor
|
||||
}
|
||||
}
|
49
plugins/CuraDrive/src/qml/components/ActionCheckBox.qml
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
|
||||
CheckBox
|
||||
{
|
||||
id: checkbox
|
||||
hoverEnabled: true
|
||||
|
||||
property var label: ""
|
||||
|
||||
indicator: Rectangle {
|
||||
implicitWidth: 30 * screenScaleFactor
|
||||
implicitHeight: 30 * screenScaleFactor
|
||||
x: 0
|
||||
y: Math.round(parent.height / 2 - height / 2)
|
||||
color: UM.Theme.getColor("sidebar")
|
||||
border.color: UM.Theme.getColor("text")
|
||||
|
||||
Rectangle {
|
||||
width: 14 * screenScaleFactor
|
||||
height: 14 * screenScaleFactor
|
||||
x: 8 * screenScaleFactor
|
||||
y: 8 * screenScaleFactor
|
||||
color: UM.Theme.getColor("primary")
|
||||
visible: checkbox.checked
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Label {
|
||||
anchors
|
||||
{
|
||||
left: checkbox.indicator.right
|
||||
leftMargin: 5 * screenScaleFactor
|
||||
}
|
||||
text: catalog.i18nc("@checkbox:description", "Auto Backup")
|
||||
color: UM.Theme.getColor("text")
|
||||
renderType: Text.NativeRendering
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
ActionToolTip
|
||||
{
|
||||
text: checkbox.label
|
||||
}
|
||||
}
|
29
plugins/CuraDrive/src/qml/components/ActionToolTip.qml
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.1 as UM
|
||||
|
||||
ToolTip
|
||||
{
|
||||
id: tooltip
|
||||
visible: parent.hovered
|
||||
opacity: 0.9
|
||||
delay: 500
|
||||
|
||||
background: Rectangle
|
||||
{
|
||||
color: UM.Theme.getColor("sidebar")
|
||||
border.color: UM.Theme.getColor("primary")
|
||||
border.width: 1 * screenScaleFactor
|
||||
}
|
||||
|
||||
contentItem: Label
|
||||
{
|
||||
text: tooltip.text
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("very_small")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
31
plugins/CuraDrive/src/qml/components/BackupList.qml
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.1 as UM
|
||||
|
||||
ListView
|
||||
{
|
||||
id: backupList
|
||||
width: parent.width
|
||||
clip: true
|
||||
delegate: Item
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
BackupListItem
|
||||
{
|
||||
id: backupListItem
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Divider
|
||||
{
|
||||
width: parent.width
|
||||
anchors.top: backupListItem.bottom
|
||||
}
|
||||
}
|
||||
ScrollBar.vertical: RightSideScrollBar {}
|
||||
}
|
42
plugins/CuraDrive/src/qml/components/BackupListFooter.qml
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
|
||||
import "../components"
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: backupListFooter
|
||||
width: parent.width
|
||||
property bool showInfoButton: false
|
||||
|
||||
ActionButton
|
||||
{
|
||||
id: infoButton
|
||||
text: catalog.i18nc("@button", "Want more?")
|
||||
iconSource: "../images/info.svg"
|
||||
onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2")
|
||||
visible: backupListFooter.showInfoButton
|
||||
}
|
||||
|
||||
ActionButton
|
||||
{
|
||||
id: createBackupButton
|
||||
text: catalog.i18nc("@button", "Backup Now")
|
||||
iconSource: "../images/backup.svg"
|
||||
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
|
||||
onClicked: CuraDrive.createBackup()
|
||||
busy: CuraDrive.isCreatingBackup
|
||||
}
|
||||
|
||||
ActionCheckBox
|
||||
{
|
||||
id: autoBackupEnabled
|
||||
checked: CuraDrive.autoBackupEnabled
|
||||
onClicked: CuraDrive.toggleAutoBackup(autoBackupEnabled.checked)
|
||||
label: catalog.i18nc("@checkbox:description", "Automatically create a backup each day that Cura is started.")
|
||||
}
|
||||
}
|
112
plugins/CuraDrive/src/qml/components/BackupListItem.qml
Normal file
@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Dialogs 1.1
|
||||
|
||||
import UM 1.1 as UM
|
||||
|
||||
Item
|
||||
{
|
||||
id: backupListItem
|
||||
width: parent.width
|
||||
height: showDetails ? dataRow.height + backupDetails.height : dataRow.height
|
||||
property bool showDetails: false
|
||||
|
||||
// Backup details toggle animation.
|
||||
Behavior on height
|
||||
{
|
||||
PropertyAnimation
|
||||
{
|
||||
duration: 70
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: dataRow
|
||||
spacing: UM.Theme.getSize("default_margin").width * 2
|
||||
width: parent.width
|
||||
height: 50 * screenScaleFactor
|
||||
|
||||
ActionButton
|
||||
{
|
||||
color: "transparent"
|
||||
hoverColor: "transparent"
|
||||
textColor: UM.Theme.getColor("text")
|
||||
textHoverColor: UM.Theme.getColor("primary")
|
||||
iconSource: "../images/info.svg"
|
||||
onClicked: backupListItem.showDetails = !backupListItem.showDetails
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: new Date(model["generated_time"]).toLocaleString(UM.Preferences.getValue("general/language"))
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
Layout.minimumWidth: 100 * screenScaleFactor
|
||||
Layout.maximumWidth: 500 * screenScaleFactor
|
||||
Layout.fillWidth: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: model["data"]["description"]
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
Layout.minimumWidth: 100 * screenScaleFactor
|
||||
Layout.maximumWidth: 500 * screenScaleFactor
|
||||
Layout.fillWidth: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
ActionButton
|
||||
{
|
||||
text: catalog.i18nc("@button", "Restore")
|
||||
color: "transparent"
|
||||
hoverColor: "transparent"
|
||||
textColor: UM.Theme.getColor("text")
|
||||
textHoverColor: UM.Theme.getColor("text_link")
|
||||
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
|
||||
onClicked: confirmRestoreDialog.visible = true
|
||||
}
|
||||
|
||||
ActionButton
|
||||
{
|
||||
color: "transparent"
|
||||
hoverColor: "transparent"
|
||||
textColor: UM.Theme.getColor("setting_validation_error")
|
||||
textHoverColor: UM.Theme.getColor("setting_validation_error")
|
||||
iconSource: "../images/delete.svg"
|
||||
onClicked: confirmDeleteDialog.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
BackupListItemDetails
|
||||
{
|
||||
id: backupDetails
|
||||
backupDetailsData: model
|
||||
width: parent.width
|
||||
visible: parent.showDetails
|
||||
anchors.top: dataRow.bottom
|
||||
}
|
||||
|
||||
MessageDialog
|
||||
{
|
||||
id: confirmDeleteDialog
|
||||
title: catalog.i18nc("@dialog:title", "Delete Backup")
|
||||
text: catalog.i18nc("@dialog:info", "Are you sure you want to delete this backup? This cannot be undone.")
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
onYes: CuraDrive.deleteBackup(model["backup_id"])
|
||||
}
|
||||
|
||||
MessageDialog
|
||||
{
|
||||
id: confirmRestoreDialog
|
||||
title: catalog.i18nc("@dialog:title", "Restore Backup")
|
||||
text: catalog.i18nc("@dialog:info", "You will need to restart Cura before your backup is restored. Do you want to close Cura now?")
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
onYes: CuraDrive.restoreBackup(model["backup_id"])
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.1 as UM
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
id: backupDetails
|
||||
width: parent.width
|
||||
spacing: 10 * screenScaleFactor
|
||||
property var backupDetailsData
|
||||
|
||||
// Cura version
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: "../images/cura.svg"
|
||||
label: catalog.i18nc("@backuplist:label", "Cura Version")
|
||||
value: backupDetailsData["data"]["cura_release"]
|
||||
}
|
||||
|
||||
// Machine count.
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: "../images/printer.svg"
|
||||
label: catalog.i18nc("@backuplist:label", "Machines")
|
||||
value: backupDetailsData["data"]["machine_count"]
|
||||
}
|
||||
|
||||
// Meterial count.
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: "../images/material.svg"
|
||||
label: catalog.i18nc("@backuplist:label", "Materials")
|
||||
value: backupDetailsData["data"]["material_count"]
|
||||
}
|
||||
|
||||
// Meterial count.
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: "../images/profile.svg"
|
||||
label: catalog.i18nc("@backuplist:label", "Profiles")
|
||||
value: backupDetailsData["data"]["profile_count"]
|
||||
}
|
||||
|
||||
// Meterial count.
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: "../images/plugin.svg"
|
||||
label: catalog.i18nc("@backuplist:label", "Plugins")
|
||||
value: backupDetailsData["data"]["plugin_count"]
|
||||
}
|
||||
|
||||
// Spacer.
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: 10 * screenScaleFactor
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: detailsRow
|
||||
width: parent.width
|
||||
height: 40 * screenScaleFactor
|
||||
|
||||
property var iconSource
|
||||
property var label
|
||||
property var value
|
||||
|
||||
// Spacing.
|
||||
Item
|
||||
{
|
||||
width: 40 * screenScaleFactor
|
||||
}
|
||||
|
||||
Icon
|
||||
{
|
||||
width: 18 * screenScaleFactor
|
||||
iconSource: detailsRow.iconSource
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: detailsRow.label
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
Layout.minimumWidth: 50 * screenScaleFactor
|
||||
Layout.maximumWidth: 100 * screenScaleFactor
|
||||
Layout.fillWidth: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: detailsRow.value
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
Layout.minimumWidth: 50 * screenScaleFactor
|
||||
Layout.maximumWidth: 100 * screenScaleFactor
|
||||
Layout.fillWidth: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
11
plugins/CuraDrive/src/qml/components/Divider.qml
Normal file
@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
|
||||
import UM 1.3 as UM
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: divider
|
||||
color: UM.Theme.getColor("lining")
|
||||
height: UM.Theme.getSize("default_lining").height
|
||||
}
|
56
plugins/CuraDrive/src/qml/components/Icon.qml
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
Item
|
||||
{
|
||||
id: icon
|
||||
width: parent.height
|
||||
height: width
|
||||
property var color: "transparent"
|
||||
property var iconSource
|
||||
property bool animated: false
|
||||
|
||||
Image
|
||||
{
|
||||
id: iconImage
|
||||
width: parent.height
|
||||
height: width
|
||||
smooth: true
|
||||
source: icon.iconSource
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
antialiasing: true
|
||||
visible: !icon.animated
|
||||
}
|
||||
|
||||
AnimatedImage
|
||||
{
|
||||
id: animatedIconImage
|
||||
width: parent.height
|
||||
height: width
|
||||
smooth: true
|
||||
antialiasing: true
|
||||
source: "../images/loading.gif"
|
||||
visible: icon.animated
|
||||
}
|
||||
|
||||
ColorOverlay
|
||||
{
|
||||
anchors.fill: iconImage
|
||||
source: iconImage
|
||||
color: icon.color
|
||||
antialiasing: true
|
||||
visible: !icon.animated
|
||||
}
|
||||
|
||||
ColorOverlay
|
||||
{
|
||||
anchors.fill: animatedIconImage
|
||||
source: animatedIconImage
|
||||
color: icon.color
|
||||
antialiasing: true
|
||||
visible: icon.animated
|
||||
}
|
||||
}
|
13
plugins/CuraDrive/src/qml/components/RightSideScrollBar.qml
Normal file
@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
ScrollBar
|
||||
{
|
||||
active: true
|
||||
size: parent.height
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
BIN
plugins/CuraDrive/src/qml/images/avatar_default.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
12
plugins/CuraDrive/src/qml/images/background.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="1024px" height="1183px" viewBox="0 0 1024 1183" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Polygon 18</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Home" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" fill-opacity="0.2">
|
||||
<g id="1440px-home" transform="translate(-86.000000, -30.000000)" fill="#D1D9DB">
|
||||
<polygon id="Polygon-18" points="598 30 1110 325.603338 1110 916.810013 598 1212.41335 86 916.810013 86 325.603338"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 734 B |
3
plugins/CuraDrive/src/qml/images/backup.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="white">
|
||||
<path d="M16.5 11.3h-5.2v5.2H8.7v-5.2H3.5V8.7h5.2V3.5h2.6v5.2h5.2z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 154 B |
7
plugins/CuraDrive/src/qml/images/cura.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
|
||||
<switch>
|
||||
<g>
|
||||
<path d="M11.07 3L3 11.071V27h15.931L27 18.93V3H11.07zm10.175 8.235h-6.071c-2.02.013-3.016 1.414-3.016 3.115 0 1.702.996 3.125 3.016 3.136h6.071v3.433h-6.071c-3.996 0-6.419-2.743-6.419-6.568 0-3.826 2.423-6.548 6.419-6.548h6.071v3.432z"/>
|
||||
</g>
|
||||
</switch>
|
||||
</svg>
|
After Width: | Height: | Size: 370 B |
BIN
plugins/CuraDrive/src/qml/images/cura_logo.jpg
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
plugins/CuraDrive/src/qml/images/cura_logo.png
Normal file
After Width: | Height: | Size: 13 KiB |
7
plugins/CuraDrive/src/qml/images/delete.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 13" fill="red">
|
||||
<switch>
|
||||
<g>
|
||||
<path d="M13 2.23L8.73 6.5 13 10.77l-2.135 2.134-4.269-4.269-4.27 4.269L.191 10.77l4.27-4.27-4.27-4.27L2.326.096l4.27 4.269L10.865.096z"/>
|
||||
</g>
|
||||
</switch>
|
||||
</svg>
|
After Width: | Height: | Size: 281 B |
7
plugins/CuraDrive/src/qml/images/folder.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
|
||||
<switch>
|
||||
<g>
|
||||
<path d="M21.718 10.969V7.758H8.451L7.014 5.222h-5.83v19.436h21.211l6.422-13.69-7.099.001zm-1.098 0H8.958L3.043 23.56h-.761V6.321h4.056l1.437 2.535H20.62v2.113z"/>
|
||||
</g>
|
||||
</switch>
|
||||
</svg>
|
After Width: | Height: | Size: 295 B |
3
plugins/CuraDrive/src/qml/images/home.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" fill="white">
|
||||
<path d="m 8.5727081,5.62578 v 2.97725 q 0,0.16127 -0.1178498,0.27912 Q 8.3370085,9 8.1757405,9 H 5.7939349 V 6.61819 H 4.2060646 V 9 H 1.824259 Q 1.6629909,9 1.5451412,8.88215 1.4272914,8.7643 1.4272914,8.60303 V 5.62578 q 0,-0.006 0.0031,-0.0186 0.0031,-0.0124 0.0031,-0.0186 L 4.9999998,2.64852 8.5665054,5.58856 q 0.0062,0.0124 0.0062,0.0372 z M 9.955892,5.19779 9.5713297,5.65679 q -0.049621,0.0558 -0.130255,0.0682 h -0.018608 q -0.080634,0 -0.130255,-0.0434 L 4.9999998,2.10269 0.70778771,5.6816 Q 0.63335631,5.7312 0.55892486,5.725 0.47829087,5.7126 0.42866987,5.6568 L 0.04410752,5.1978 Q -0.00551343,5.1358 6.8917799e-4,5.05204 0.00689178,4.96834 0.06891799,4.91869 L 4.5286008,1.20331 q 0.1984838,-0.16127 0.471399,-0.16127 0.2729153,0 0.471399,0.16127 L 6.9848377,2.46864 V 1.25913 q 0,-0.0868 0.055824,-0.14266 0.055824,-0.0558 0.1426602,-0.0558 h 1.1909028 q 0.086837,0 0.1426602,0.0558 0.055824,0.0558 0.055824,0.14266 V 3.7898 l 1.3583734,1.12888 q 0.062026,0.0496 0.068229,0.13335 0.0062,0.0837 -0.043418,0.14576 z" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
BIN
plugins/CuraDrive/src/qml/images/icon.png
Normal file
After Width: | Height: | Size: 21 KiB |
4
plugins/CuraDrive/src/qml/images/info.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.5 15C11.641 15 15 11.643 15 7.5 15 3.358 11.641 0 7.5 0 3.358 0 0 3.358 0 7.5 0 11.643 3.358 15 7.5 15ZM8.6 12.369L6.472 12.369 6.472 4.57 8.6 4.57 8.6 12.369ZM7.541 1.514C8.313 1.514 8.697 1.861 8.697 2.553 8.697 2.885 8.6 3.141 8.409 3.325 8.216 3.509 7.926 3.601 7.541 3.601 6.767 3.601 6.382 3.252 6.382 2.553 6.382 1.861 6.767 1.514 7.541 1.514Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 499 B |
BIN
plugins/CuraDrive/src/qml/images/inverted_circle.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
plugins/CuraDrive/src/qml/images/loading.gif
Normal file
After Width: | Height: | Size: 6.6 KiB |
7
plugins/CuraDrive/src/qml/images/material.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
|
||||
<switch>
|
||||
<g>
|
||||
<path d="M2.995 1H5.67v24H2.995zm21.33 0H27v24h-2.675zM8.992 3.284c-.368 0-.669.224-.669.5v18.433c0 .276.3.5.669.5.369 0 .669-.224.669-.5V3.784c0-.276-.299-.5-.669-.5m4.003 0c-.368 0-.669.224-.669.5v18.433c0 .276.3.5.669.5.371 0 .669-.224.669-.5V3.784c0-.276-.298-.5-.669-.5m4.004 0c-.371 0-.669.224-.669.5v24.451c0 .277.298.5.669.5.368 0 .669-.223.669-.5V3.784c0-.276-.301-.5-.669-.5m4.003 0c-.368 0-.669.224-.669.5v18.433c0 .276.3.5.669.5.37 0 .669-.224.669-.5V3.784c-.001-.276-.3-.5-.669-.5"/>
|
||||
</g>
|
||||
</switch>
|
||||
</svg>
|
After Width: | Height: | Size: 628 B |
7
plugins/CuraDrive/src/qml/images/plugin.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
|
||||
<switch>
|
||||
<g>
|
||||
<path d="M22.32 19.715l-6.96-6.96 6.96-6.958v4.541H27V3H10.999L3 11.001V27h15.998L27 19.001v-3.828h-4.68z"/>
|
||||
</g>
|
||||
</switch>
|
||||
</svg>
|
After Width: | Height: | Size: 240 B |
BIN
plugins/CuraDrive/src/qml/images/preview_banner.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
14
plugins/CuraDrive/src/qml/images/printer.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 49 (51002) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>icn_singlePrinter</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Visual" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Printer-status-icon" transform="translate(-217.000000, -176.000000)" fill="#000000">
|
||||
<g id="icn_singlePrinter" transform="translate(217.000000, 176.000000)">
|
||||
<path d="M2,13 L14,13 L14,15 L2,15 L2,13 L2,13 Z M2,3 L14,3 L14,5 L2,5 L2,3 L2,3 Z M0,14 L2,14 L2,16 L0,16 L0,14 L0,14 Z M14,14 L16,14 L16,16 L14,16 L14,14 L14,14 Z M0,0 L2,0 L2,14 L0,14 L0,0 L0,0 Z M14,0 L16,0 L16,14 L14,14 L14,0 L14,0 Z M6,5 L10,5 L10,6 L6,6 L6,5 L6,5 Z M7,6 L9,6 L9,8 L7,8 L7,6 L7,6 Z M2,0 L14,0 L14,2 L2,2 L2,0 L2,0 Z M3,12 L13,12 L13,13 L3,13 L3,12 L3,12 Z" id="Rectangle-185-Copy-5-Copy-4"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
3
plugins/CuraDrive/src/qml/images/profile.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 13">
|
||||
<path d="M12.96 5.778c-.021-.182-.234-.32-.419-.32-.595 0-1.124-.35-1.346-.89a1.45 1.45 0 0 1 .364-1.608.361.361 0 0 0 .04-.49 6.43 6.43 0 0 0-1.03-1.04.362.362 0 0 0-.494.04c-.388.43-1.084.589-1.621.364A1.444 1.444 0 0 1 7.576.423a.36.36 0 0 0-.32-.38A6.485 6.485 0 0 0 5.795.04a.362.362 0 0 0-.321.372 1.447 1.447 0 0 1-.89 1.387c-.532.217-1.223.06-1.61-.366a.362.362 0 0 0-.49-.041A6.46 6.46 0 0 0 1.43 2.43a.362.362 0 0 0 .04.493 1.44 1.44 0 0 1 .363 1.622c-.225.534-.78.879-1.415.879a.354.354 0 0 0-.375.319A6.51 6.51 0 0 0 .04 7.222c.02.183.24.32.426.32a1.426 1.426 0 0 1 1.338.89c.227.554.08 1.2-.364 1.608a.36.36 0 0 0-.04.49c.303.384.65.734 1.029 1.04.149.12.365.103.495-.04.389-.43 1.084-.589 1.62-.364.561.235.914.802.88 1.41a.36.36 0 0 0 .318.38 6.44 6.44 0 0 0 1.463.005.362.362 0 0 0 .322-.373 1.445 1.445 0 0 1 .889-1.386c.535-.218 1.223-.058 1.61.366.128.14.34.157.49.041a6.476 6.476 0 0 0 1.052-1.04.361.361 0 0 0-.039-.493 1.44 1.44 0 0 1-.364-1.622c.22-.527.755-.88 1.33-.88l.08.001a.362.362 0 0 0 .38-.319c.058-.488.058-.985.003-1.478zM6.51 8.682a2.17 2.17 0 0 1-2.168-2.168A2.17 2.17 0 0 1 6.51 4.346a2.17 2.17 0 0 1 2.168 2.168A2.17 2.17 0 0 1 6.51 8.682z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
7
plugins/CuraDrive/src/qml/images/restore.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
|
||||
<switch>
|
||||
<g>
|
||||
<path d="M-.113 31.935c8.904 4.904 17.81 9.802 26.709 14.714 2.507 1.384 4.993 2.807 7.868 4.426C57.24 23.782 85.983 7.847 121.495 5.371c27.659-1.928 53.113 5.077 76.006 20.788 44.611 30.614 63.473 86.919 46.099 137.829-17.883 52.399-66.749 82.265-113.69 81.745v-36.688c25.195-1.141 46.785-10.612 63.364-30.267 11.82-14.013 18.14-30.322 19.349-48.541 2.323-34.992-19.005-68.519-51.909-81.916-32.223-13.12-70.379-4.319-93 22.01l31.263 18.198c-1.545 1.07-2.387 1.747-3.312 2.281a81656.22 81656.22 0 0 1-92.099 53.112c-1.128.65-2.448.967-3.679 1.439V31.935z"/>
|
||||
</g>
|
||||
</switch>
|
||||
</svg>
|
After Width: | Height: | Size: 691 B |
42
plugins/CuraDrive/src/qml/main.qml
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
import "components"
|
||||
import "pages"
|
||||
|
||||
Window
|
||||
{
|
||||
id: curaDriveDialog
|
||||
minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width)
|
||||
minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height)
|
||||
maximumWidth: minimumWidth * 1.2
|
||||
maximumHeight: minimumHeight * 1.2
|
||||
width: minimumWidth
|
||||
height: minimumHeight
|
||||
color: UM.Theme.getColor("sidebar")
|
||||
title: catalog.i18nc("@title:window", "Cura Backups")
|
||||
|
||||
// Globally available.
|
||||
UM.I18nCatalog
|
||||
{
|
||||
id: catalog
|
||||
name: "cura_drive"
|
||||
}
|
||||
|
||||
WelcomePage
|
||||
{
|
||||
id: welcomePage
|
||||
visible: !Cura.API.account.isLoggedIn
|
||||
}
|
||||
|
||||
BackupsPage
|
||||
{
|
||||
id: backupsPage
|
||||
visible: Cura.API.account.isLoggedIn
|
||||
}
|
||||
}
|
73
plugins/CuraDrive/src/qml/pages/BackupsPage.qml
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
import "../components"
|
||||
|
||||
Item
|
||||
{
|
||||
id: backupsPage
|
||||
anchors.fill: parent
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width * 3
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_margin").height * 2
|
||||
width: parent.width
|
||||
anchors.fill: parent
|
||||
|
||||
Label
|
||||
{
|
||||
id: backupTitle
|
||||
text: catalog.i18nc("@title", "My Backups")
|
||||
font: UM.Theme.getFont("large")
|
||||
color: UM.Theme.getColor("text")
|
||||
Layout.fillWidth: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@empty_state",
|
||||
"You don't have any backups currently. Use the 'Backup Now' button to create one.")
|
||||
width: parent.width
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Label.WordWrap
|
||||
visible: backupList.count == 0
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
BackupList
|
||||
{
|
||||
id: backupList
|
||||
model: CuraDrive.backups
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@backup_limit_info",
|
||||
"During the preview phase, you'll be limited to 5 visible backups. Remove a backup to see older ones.")
|
||||
width: parent.width
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Label.WordWrap
|
||||
visible: backupList.count > 4
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
BackupListFooter
|
||||
{
|
||||
id: backupListFooter
|
||||
showInfoButton: backupList.count > 4
|
||||
}
|
||||
}
|
||||
}
|
48
plugins/CuraDrive/src/qml/pages/WelcomePage.qml
Normal file
@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
import "../components"
|
||||
|
||||
Column
|
||||
{
|
||||
id: welcomePage
|
||||
spacing: UM.Theme.getSize("wide_margin").height
|
||||
width: parent.width
|
||||
topPadding: 150 * screenScaleFactor
|
||||
|
||||
Image
|
||||
{
|
||||
id: profileImage
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "../images/icon.png"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Math.round(parent.width / 4)
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: welcomeTextLabel
|
||||
text: catalog.i18nc("@description", "Backup and synchronize your Cura settings.")
|
||||
width: Math.round(parent.width / 2)
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
wrapMode: Label.WordWrap
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
ActionButton
|
||||
{
|
||||
id: loginButton
|
||||
onClicked: Cura.API.account.login()
|
||||
text: catalog.i18nc("@button", "Sign In")
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|