mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-13 09:47:50 -06:00
Merge branch 'CURA-7290_manual_account_sync' of github.com:Ultimaker/Cura
This commit is contained in:
commit
280d3f07a6
9 changed files with 156 additions and 100 deletions
|
@ -23,6 +23,7 @@ class SyncState:
|
||||||
SYNCING = 0
|
SYNCING = 0
|
||||||
SUCCESS = 1
|
SUCCESS = 1
|
||||||
ERROR = 2
|
ERROR = 2
|
||||||
|
IDLE = 3
|
||||||
|
|
||||||
|
|
||||||
## The account API provides a version-proof bridge to use Ultimaker Accounts
|
## The account API provides a version-proof bridge to use Ultimaker Accounts
|
||||||
|
@ -50,6 +51,7 @@ class Account(QObject):
|
||||||
"""
|
"""
|
||||||
lastSyncDateTimeChanged = pyqtSignal()
|
lastSyncDateTimeChanged = pyqtSignal()
|
||||||
syncStateChanged = pyqtSignal(int) # because SyncState is an int Enum
|
syncStateChanged = pyqtSignal(int) # because SyncState is an int Enum
|
||||||
|
manualSyncEnabledChanged = pyqtSignal(bool)
|
||||||
|
|
||||||
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
@ -58,7 +60,8 @@ class Account(QObject):
|
||||||
|
|
||||||
self._error_message = None # type: Optional[Message]
|
self._error_message = None # type: Optional[Message]
|
||||||
self._logged_in = False
|
self._logged_in = False
|
||||||
self._sync_state = SyncState.SUCCESS
|
self._sync_state = SyncState.IDLE
|
||||||
|
self._manual_sync_enabled = False
|
||||||
self._last_sync_str = "-"
|
self._last_sync_str = "-"
|
||||||
|
|
||||||
self._callback_port = 32118
|
self._callback_port = 32118
|
||||||
|
@ -106,16 +109,21 @@ class Account(QObject):
|
||||||
:param state: One of SyncState
|
:param state: One of SyncState
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
Logger.info("Service {service} enters sync state {state}", service = service_name, state = state)
|
||||||
|
|
||||||
prev_state = self._sync_state
|
prev_state = self._sync_state
|
||||||
|
|
||||||
self._sync_services[service_name] = state
|
self._sync_services[service_name] = state
|
||||||
|
|
||||||
if any(val == SyncState.SYNCING for val in self._sync_services.values()):
|
if any(val == SyncState.SYNCING for val in self._sync_services.values()):
|
||||||
self._sync_state = SyncState.SYNCING
|
self._sync_state = SyncState.SYNCING
|
||||||
|
self._setManualSyncEnabled(False)
|
||||||
elif any(val == SyncState.ERROR for val in self._sync_services.values()):
|
elif any(val == SyncState.ERROR for val in self._sync_services.values()):
|
||||||
self._sync_state = SyncState.ERROR
|
self._sync_state = SyncState.ERROR
|
||||||
|
self._setManualSyncEnabled(True)
|
||||||
else:
|
else:
|
||||||
self._sync_state = SyncState.SUCCESS
|
self._sync_state = SyncState.SUCCESS
|
||||||
|
self._setManualSyncEnabled(False)
|
||||||
|
|
||||||
if self._sync_state != prev_state:
|
if self._sync_state != prev_state:
|
||||||
self.syncStateChanged.emit(self._sync_state)
|
self.syncStateChanged.emit(self._sync_state)
|
||||||
|
@ -157,11 +165,31 @@ class Account(QObject):
|
||||||
self._logged_in = logged_in
|
self._logged_in = logged_in
|
||||||
self.loginStateChanged.emit(logged_in)
|
self.loginStateChanged.emit(logged_in)
|
||||||
if logged_in:
|
if logged_in:
|
||||||
self.sync()
|
self._setManualSyncEnabled(False)
|
||||||
|
self._sync()
|
||||||
else:
|
else:
|
||||||
if self._update_timer.isActive():
|
if self._update_timer.isActive():
|
||||||
self._update_timer.stop()
|
self._update_timer.stop()
|
||||||
|
|
||||||
|
def _sync(self) -> None:
|
||||||
|
"""Signals all sync services to start syncing
|
||||||
|
|
||||||
|
This can be considered a forced sync: even when a
|
||||||
|
sync is currently running, a sync will be requested.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._update_timer.isActive():
|
||||||
|
self._update_timer.stop()
|
||||||
|
elif self._sync_state == SyncState.SYNCING:
|
||||||
|
Logger.warning("Starting a new sync while previous sync was not completed\n{}", str(self._sync_services))
|
||||||
|
|
||||||
|
self.syncRequested.emit()
|
||||||
|
|
||||||
|
def _setManualSyncEnabled(self, enabled: bool) -> None:
|
||||||
|
if self._manual_sync_enabled != enabled:
|
||||||
|
self._manual_sync_enabled = enabled
|
||||||
|
self.manualSyncEnabledChanged.emit(enabled)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@pyqtSlot(bool)
|
@pyqtSlot(bool)
|
||||||
def login(self, force_logout_before_login: bool = False) -> None:
|
def login(self, force_logout_before_login: bool = False) -> None:
|
||||||
|
@ -212,20 +240,23 @@ class Account(QObject):
|
||||||
def lastSyncDateTime(self) -> str:
|
def lastSyncDateTime(self) -> str:
|
||||||
return self._last_sync_str
|
return self._last_sync_str
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=manualSyncEnabledChanged)
|
||||||
|
def manualSyncEnabled(self) -> bool:
|
||||||
|
return self._manual_sync_enabled
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def sync(self) -> None:
|
@pyqtSlot(bool)
|
||||||
"""Signals all sync services to start syncing
|
def sync(self, user_initiated: bool = False) -> None:
|
||||||
|
if user_initiated:
|
||||||
|
self._setManualSyncEnabled(False)
|
||||||
|
|
||||||
This can be considered a forced sync: even when a
|
self._sync()
|
||||||
sync is currently running, a sync will be requested.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._update_timer.isActive():
|
@pyqtSlot()
|
||||||
self._update_timer.stop()
|
def popupOpened(self) -> None:
|
||||||
elif self._sync_state == SyncState.SYNCING:
|
self._setManualSyncEnabled(True)
|
||||||
Logger.warning("Starting a new sync while previous sync was not completed\n{}", str(self._sync_services))
|
self._sync_state = SyncState.IDLE
|
||||||
|
self.syncStateChanged.emit(self._sync_state)
|
||||||
self.syncRequested.emit()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def logout(self) -> None:
|
def logout(self) -> None:
|
||||||
|
|
|
@ -67,6 +67,7 @@ class CloudPackageChecker(QObject):
|
||||||
self._application.getHttpRequestManager().get(url,
|
self._application.getHttpRequestManager().get(url,
|
||||||
callback = self._onUserPackagesRequestFinished,
|
callback = self._onUserPackagesRequestFinished,
|
||||||
error_callback = self._onUserPackagesRequestFinished,
|
error_callback = self._onUserPackagesRequestFinished,
|
||||||
|
timeout=10,
|
||||||
scope = self._scope)
|
scope = self._scope)
|
||||||
|
|
||||||
def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
||||||
|
|
|
@ -6,11 +6,15 @@ from time import time
|
||||||
from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any, cast
|
from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any, cast
|
||||||
|
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt5.QtCore import QUrl
|
||||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
|
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
||||||
|
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
|
||||||
from cura.API import Account
|
from cura.API import Account
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.UltimakerCloud import UltimakerCloudAuthentication
|
from cura.UltimakerCloud import UltimakerCloudAuthentication
|
||||||
|
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
|
||||||
from .ToolPathUploader import ToolPathUploader
|
from .ToolPathUploader import ToolPathUploader
|
||||||
from ..Models.BaseModel import BaseModel
|
from ..Models.BaseModel import BaseModel
|
||||||
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
||||||
|
@ -33,16 +37,20 @@ class CloudApiClient:
|
||||||
CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH)
|
CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH)
|
||||||
CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH)
|
CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH)
|
||||||
|
|
||||||
|
DEFAULT_REQUEST_TIMEOUT = 10 # seconds
|
||||||
|
|
||||||
# In order to avoid garbage collection we keep the callbacks in this list.
|
# In order to avoid garbage collection we keep the callbacks in this list.
|
||||||
_anti_gc_callbacks = [] # type: List[Callable[[], None]]
|
_anti_gc_callbacks = [] # type: List[Callable[[Any], None]]
|
||||||
|
|
||||||
## Initializes a new cloud API client.
|
## Initializes a new cloud API client.
|
||||||
# \param account: The user's account object
|
# \param account: The user's account object
|
||||||
# \param on_error: The callback to be called whenever we receive errors from the server.
|
# \param on_error: The callback to be called whenever we receive errors from the server.
|
||||||
def __init__(self, account: Account, on_error: Callable[[List[CloudError]], None]) -> None:
|
def __init__(self, app: CuraApplication, on_error: Callable[[List[CloudError]], None]) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._manager = QNetworkAccessManager()
|
self._app = app
|
||||||
self._account = account
|
self._account = app.getCuraAPI().account
|
||||||
|
self._scope = JsonDecoratorScope(UltimakerCloudScope(app))
|
||||||
|
self._http = HttpRequestManager.getInstance()
|
||||||
self._on_error = on_error
|
self._on_error = on_error
|
||||||
self._upload = None # type: Optional[ToolPathUploader]
|
self._upload = None # type: Optional[ToolPathUploader]
|
||||||
|
|
||||||
|
@ -55,16 +63,21 @@ class CloudApiClient:
|
||||||
# \param on_finished: The function to be called after the result is parsed.
|
# \param on_finished: The function to be called after the result is parsed.
|
||||||
def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any], failed: Callable) -> None:
|
def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any], failed: Callable) -> None:
|
||||||
url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT)
|
url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT)
|
||||||
reply = self._manager.get(self._createEmptyRequest(url))
|
self._http.get(url,
|
||||||
self._addCallback(reply, on_finished, CloudClusterResponse, failed)
|
scope = self._scope,
|
||||||
|
callback = self._parseCallback(on_finished, CloudClusterResponse, failed),
|
||||||
|
error_callback = failed,
|
||||||
|
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||||
|
|
||||||
## Retrieves the status of the given cluster.
|
## Retrieves the status of the given cluster.
|
||||||
# \param cluster_id: The ID of the cluster.
|
# \param cluster_id: The ID of the cluster.
|
||||||
# \param on_finished: The function to be called after the result is parsed.
|
# \param on_finished: The function to be called after the result is parsed.
|
||||||
def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None:
|
def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None:
|
||||||
url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id)
|
url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id)
|
||||||
reply = self._manager.get(self._createEmptyRequest(url))
|
self._http.get(url,
|
||||||
self._addCallback(reply, on_finished, CloudClusterStatus)
|
scope = self._scope,
|
||||||
|
callback = self._parseCallback(on_finished, CloudClusterStatus),
|
||||||
|
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||||
|
|
||||||
## Requests the cloud to register the upload of a print job mesh.
|
## Requests the cloud to register the upload of a print job mesh.
|
||||||
# \param request: The request object.
|
# \param request: The request object.
|
||||||
|
@ -72,9 +85,13 @@ class CloudApiClient:
|
||||||
def requestUpload(self, request: CloudPrintJobUploadRequest,
|
def requestUpload(self, request: CloudPrintJobUploadRequest,
|
||||||
on_finished: Callable[[CloudPrintJobResponse], Any]) -> None:
|
on_finished: Callable[[CloudPrintJobResponse], Any]) -> None:
|
||||||
url = "{}/jobs/upload".format(self.CURA_API_ROOT)
|
url = "{}/jobs/upload".format(self.CURA_API_ROOT)
|
||||||
body = json.dumps({"data": request.toDict()})
|
data = json.dumps({"data": request.toDict()}).encode()
|
||||||
reply = self._manager.put(self._createEmptyRequest(url), body.encode())
|
|
||||||
self._addCallback(reply, on_finished, CloudPrintJobResponse)
|
self._http.put(url,
|
||||||
|
scope = self._scope,
|
||||||
|
data = data,
|
||||||
|
callback = self._parseCallback(on_finished, CloudPrintJobResponse),
|
||||||
|
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||||
|
|
||||||
## Uploads a print job tool path to the cloud.
|
## Uploads a print job tool path to the cloud.
|
||||||
# \param print_job: The object received after requesting an upload with `self.requestUpload`.
|
# \param print_job: The object received after requesting an upload with `self.requestUpload`.
|
||||||
|
@ -84,7 +101,7 @@ class CloudApiClient:
|
||||||
# \param on_error: A function to be called if the upload fails.
|
# \param on_error: A function to be called if the upload fails.
|
||||||
def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any],
|
def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any],
|
||||||
on_progress: Callable[[int], Any], on_error: Callable[[], Any]):
|
on_progress: Callable[[int], Any], on_error: Callable[[], Any]):
|
||||||
self._upload = ToolPathUploader(self._manager, print_job, mesh, on_finished, on_progress, on_error)
|
self._upload = ToolPathUploader(self._http, print_job, mesh, on_finished, on_progress, on_error)
|
||||||
self._upload.start()
|
self._upload.start()
|
||||||
|
|
||||||
# Requests a cluster to print the given print job.
|
# Requests a cluster to print the given print job.
|
||||||
|
@ -93,8 +110,11 @@ class CloudApiClient:
|
||||||
# \param on_finished: The function to be called after the result is parsed.
|
# \param on_finished: The function to be called after the result is parsed.
|
||||||
def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None:
|
def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None:
|
||||||
url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id)
|
url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id)
|
||||||
reply = self._manager.post(self._createEmptyRequest(url), b"")
|
self._http.post(url,
|
||||||
self._addCallback(reply, on_finished, CloudPrintResponse)
|
scope = self._scope,
|
||||||
|
data = b"",
|
||||||
|
callback = self._parseCallback(on_finished, CloudPrintResponse),
|
||||||
|
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||||
|
|
||||||
## Send a print job action to the cluster for the given print job.
|
## Send a print job action to the cluster for the given print job.
|
||||||
# \param cluster_id: The ID of the cluster.
|
# \param cluster_id: The ID of the cluster.
|
||||||
|
@ -104,7 +124,10 @@ class CloudApiClient:
|
||||||
data: Optional[Dict[str, Any]] = None) -> None:
|
data: Optional[Dict[str, Any]] = None) -> None:
|
||||||
body = json.dumps({"data": data}).encode() if data else b""
|
body = json.dumps({"data": data}).encode() if data else b""
|
||||||
url = "{}/clusters/{}/print_jobs/{}/action/{}".format(self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action)
|
url = "{}/clusters/{}/print_jobs/{}/action/{}".format(self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action)
|
||||||
self._manager.post(self._createEmptyRequest(url), body)
|
self._http.post(url,
|
||||||
|
scope = self._scope,
|
||||||
|
data = body,
|
||||||
|
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||||
|
|
||||||
## We override _createEmptyRequest in order to add the user credentials.
|
## We override _createEmptyRequest in order to add the user credentials.
|
||||||
# \param url: The URL to request
|
# \param url: The URL to request
|
||||||
|
@ -162,13 +185,12 @@ class CloudApiClient:
|
||||||
# \param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either
|
# \param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either
|
||||||
# a list or a single item.
|
# a list or a single item.
|
||||||
# \param model: The type of the model to convert the response to.
|
# \param model: The type of the model to convert the response to.
|
||||||
def _addCallback(self,
|
def _parseCallback(self,
|
||||||
reply: QNetworkReply,
|
on_finished: Union[Callable[[CloudApiClientModel], Any],
|
||||||
on_finished: Union[Callable[[CloudApiClientModel], Any],
|
Callable[[List[CloudApiClientModel]], Any]],
|
||||||
Callable[[List[CloudApiClientModel]], Any]],
|
model: Type[CloudApiClientModel],
|
||||||
model: Type[CloudApiClientModel],
|
on_error: Optional[Callable] = None) -> Callable[[QNetworkReply], None]:
|
||||||
on_error: Optional[Callable] = None) -> None:
|
def parse(reply: QNetworkReply) -> None:
|
||||||
def parse() -> None:
|
|
||||||
self._anti_gc_callbacks.remove(parse)
|
self._anti_gc_callbacks.remove(parse)
|
||||||
|
|
||||||
# Don't try to parse the reply if we didn't get one
|
# Don't try to parse the reply if we didn't get one
|
||||||
|
@ -184,6 +206,4 @@ class CloudApiClient:
|
||||||
self._parseModels(response, on_finished, model)
|
self._parseModels(response, on_finished, model)
|
||||||
|
|
||||||
self._anti_gc_callbacks.append(parse)
|
self._anti_gc_callbacks.append(parse)
|
||||||
reply.finished.connect(parse)
|
return parse
|
||||||
if on_error is not None:
|
|
||||||
reply.error.connect(on_error)
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import os
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from PyQt5.QtCore import QTimer
|
from PyQt5.QtCore import QTimer
|
||||||
|
from PyQt5.QtNetwork import QNetworkReply
|
||||||
|
|
||||||
from UM import i18nCatalog
|
from UM import i18nCatalog
|
||||||
from UM.Logger import Logger # To log errors talking to the API.
|
from UM.Logger import Logger # To log errors talking to the API.
|
||||||
|
@ -40,7 +41,7 @@ class CloudOutputDeviceManager:
|
||||||
# Persistent dict containing the remote clusters for the authenticated user.
|
# Persistent dict containing the remote clusters for the authenticated user.
|
||||||
self._remote_clusters = {} # type: Dict[str, CloudOutputDevice]
|
self._remote_clusters = {} # type: Dict[str, CloudOutputDevice]
|
||||||
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
|
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
|
||||||
self._api = CloudApiClient(self._account, on_error = lambda error: Logger.log("e", str(error)))
|
self._api = CloudApiClient(CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error)))
|
||||||
self._account.loginStateChanged.connect(self._onLoginStateChanged)
|
self._account.loginStateChanged.connect(self._onLoginStateChanged)
|
||||||
|
|
||||||
# Ensure we don't start twice.
|
# Ensure we don't start twice.
|
||||||
|
@ -118,7 +119,7 @@ class CloudOutputDeviceManager:
|
||||||
self._syncing = False
|
self._syncing = False
|
||||||
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS)
|
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS)
|
||||||
|
|
||||||
def _onGetRemoteClusterFailed(self):
|
def _onGetRemoteClusterFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
|
||||||
self._syncing = False
|
self._syncing = False
|
||||||
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
|
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# !/usr/bin/env python
|
# !/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
|
from typing import Callable, Any, Tuple, cast, Dict, Optional
|
||||||
from typing import Optional, Callable, Any, Tuple, cast
|
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
||||||
|
|
||||||
from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
|
from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
|
||||||
|
|
||||||
|
@ -23,16 +23,16 @@ class ToolPathUploader:
|
||||||
BYTES_PER_REQUEST = 256 * 1024
|
BYTES_PER_REQUEST = 256 * 1024
|
||||||
|
|
||||||
## Creates a mesh upload object.
|
## Creates a mesh upload object.
|
||||||
# \param manager: The network access manager that will handle the HTTP requests.
|
# \param http: The HttpRequestManager that will handle the HTTP requests.
|
||||||
# \param print_job: The print job response that was returned by the cloud after registering the upload.
|
# \param print_job: The print job response that was returned by the cloud after registering the upload.
|
||||||
# \param data: The mesh bytes to be uploaded.
|
# \param data: The mesh bytes to be uploaded.
|
||||||
# \param on_finished: The method to be called when done.
|
# \param on_finished: The method to be called when done.
|
||||||
# \param on_progress: The method to be called when the progress changes (receives a percentage 0-100).
|
# \param on_progress: The method to be called when the progress changes (receives a percentage 0-100).
|
||||||
# \param on_error: The method to be called when an error occurs.
|
# \param on_error: The method to be called when an error occurs.
|
||||||
def __init__(self, manager: QNetworkAccessManager, print_job: CloudPrintJobResponse, data: bytes,
|
def __init__(self, http: HttpRequestManager, print_job: CloudPrintJobResponse, data: bytes,
|
||||||
on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]
|
on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
self._manager = manager
|
self._http = http
|
||||||
self._print_job = print_job
|
self._print_job = print_job
|
||||||
self._data = data
|
self._data = data
|
||||||
|
|
||||||
|
@ -43,25 +43,12 @@ class ToolPathUploader:
|
||||||
self._sent_bytes = 0
|
self._sent_bytes = 0
|
||||||
self._retries = 0
|
self._retries = 0
|
||||||
self._finished = False
|
self._finished = False
|
||||||
self._reply = None # type: Optional[QNetworkReply]
|
|
||||||
|
|
||||||
## Returns the print job for which this object was created.
|
## Returns the print job for which this object was created.
|
||||||
@property
|
@property
|
||||||
def printJob(self):
|
def printJob(self):
|
||||||
return self._print_job
|
return self._print_job
|
||||||
|
|
||||||
## Creates a network request to the print job upload URL, adding the needed content range header.
|
|
||||||
def _createRequest(self) -> QNetworkRequest:
|
|
||||||
request = QNetworkRequest(QUrl(self._print_job.upload_url))
|
|
||||||
request.setHeader(QNetworkRequest.ContentTypeHeader, self._print_job.content_type)
|
|
||||||
|
|
||||||
first_byte, last_byte = self._chunkRange()
|
|
||||||
content_range = "bytes {}-{}/{}".format(first_byte, last_byte - 1, len(self._data))
|
|
||||||
request.setRawHeader(b"Content-Range", content_range.encode())
|
|
||||||
Logger.log("i", "Uploading %s to %s", content_range, self._print_job.upload_url)
|
|
||||||
|
|
||||||
return request
|
|
||||||
|
|
||||||
## Determines the bytes that should be uploaded next.
|
## Determines the bytes that should be uploaded next.
|
||||||
# \return: A tuple with the first and the last byte to upload.
|
# \return: A tuple with the first and the last byte to upload.
|
||||||
def _chunkRange(self) -> Tuple[int, int]:
|
def _chunkRange(self) -> Tuple[int, int]:
|
||||||
|
@ -88,13 +75,23 @@ class ToolPathUploader:
|
||||||
raise ValueError("The upload is already finished")
|
raise ValueError("The upload is already finished")
|
||||||
|
|
||||||
first_byte, last_byte = self._chunkRange()
|
first_byte, last_byte = self._chunkRange()
|
||||||
request = self._createRequest()
|
content_range = "bytes {}-{}/{}".format(first_byte, last_byte - 1, len(self._data))
|
||||||
|
|
||||||
# now send the reply and subscribe to the results
|
headers = {
|
||||||
self._reply = self._manager.put(request, self._data[first_byte:last_byte])
|
"Content-Type": cast(str, self._print_job.content_type),
|
||||||
self._reply.finished.connect(self._finishedCallback)
|
"Content-Range": content_range
|
||||||
self._reply.uploadProgress.connect(self._progressCallback)
|
} # type: Dict[str, str]
|
||||||
self._reply.error.connect(self._errorCallback)
|
|
||||||
|
Logger.log("i", "Uploading %s to %s", content_range, self._print_job.upload_url)
|
||||||
|
|
||||||
|
self._http.put(
|
||||||
|
url = cast(str, self._print_job.upload_url),
|
||||||
|
headers_dict = headers,
|
||||||
|
data = self._data[first_byte:last_byte],
|
||||||
|
callback = self._finishedCallback,
|
||||||
|
error_callback = self._errorCallback,
|
||||||
|
upload_progress_callback = self._progressCallback
|
||||||
|
)
|
||||||
|
|
||||||
## Handles an update to the upload progress
|
## Handles an update to the upload progress
|
||||||
# \param bytes_sent: The amount of bytes sent in the current request.
|
# \param bytes_sent: The amount of bytes sent in the current request.
|
||||||
|
@ -106,16 +103,14 @@ class ToolPathUploader:
|
||||||
self._on_progress(int(total_sent / len(self._data) * 100))
|
self._on_progress(int(total_sent / len(self._data) * 100))
|
||||||
|
|
||||||
## Handles an error uploading.
|
## Handles an error uploading.
|
||||||
def _errorCallback(self) -> None:
|
def _errorCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
|
||||||
reply = cast(QNetworkReply, self._reply)
|
|
||||||
body = bytes(reply.readAll()).decode()
|
body = bytes(reply.readAll()).decode()
|
||||||
Logger.log("e", "Received error while uploading: %s", body)
|
Logger.log("e", "Received error while uploading: %s", body)
|
||||||
self.stop()
|
self.stop()
|
||||||
self._on_error()
|
self._on_error()
|
||||||
|
|
||||||
## Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed.
|
## Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed.
|
||||||
def _finishedCallback(self) -> None:
|
def _finishedCallback(self, reply: QNetworkReply) -> None:
|
||||||
reply = cast(QNetworkReply, self._reply)
|
|
||||||
Logger.log("i", "Finished callback %s %s",
|
Logger.log("i", "Finished callback %s %s",
|
||||||
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString())
|
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString())
|
||||||
|
|
||||||
|
@ -133,7 +128,7 @@ class ToolPathUploader:
|
||||||
|
|
||||||
# Http codes that are not to be retried are assumed to be errors.
|
# Http codes that are not to be retried are assumed to be errors.
|
||||||
if status_code > 308:
|
if status_code > 308:
|
||||||
self._errorCallback()
|
self._errorCallback(reply, None)
|
||||||
return
|
return
|
||||||
|
|
||||||
Logger.log("d", "status_code: %s, Headers: %s, body: %s", status_code,
|
Logger.log("d", "status_code: %s, Headers: %s, body: %s", status_code,
|
||||||
|
|
|
@ -108,7 +108,15 @@ Item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: popup.opened ? popup.close() : popup.open()
|
onClicked: {
|
||||||
|
if (popup.opened)
|
||||||
|
{
|
||||||
|
popup.close()
|
||||||
|
} else {
|
||||||
|
Cura.API.account.popupOpened()
|
||||||
|
popup.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Popup
|
Popup
|
||||||
|
@ -119,6 +127,7 @@ Item
|
||||||
x: parent.width - width
|
x: parent.width - width
|
||||||
|
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||||
|
onOpened: Cura.API.account.popupOpened()
|
||||||
|
|
||||||
opacity: opened ? 1 : 0
|
opacity: opened ? 1 : 0
|
||||||
Behavior on opacity { NumberAnimation { duration: 100 } }
|
Behavior on opacity { NumberAnimation { duration: 100 } }
|
||||||
|
|
|
@ -7,11 +7,7 @@ import Cura 1.1 as Cura
|
||||||
Row // sync state icon + message
|
Row // sync state icon + message
|
||||||
{
|
{
|
||||||
|
|
||||||
property alias iconSource: icon.source
|
id: syncRow
|
||||||
property alias labelText: stateLabel.text
|
|
||||||
property alias syncButtonVisible: accountSyncButton.visible
|
|
||||||
property alias animateIconRotation: updateAnimator.running
|
|
||||||
|
|
||||||
width: childrenRect.width
|
width: childrenRect.width
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
@ -23,7 +19,7 @@ Row // sync state icon + message
|
||||||
width: 20 * screenScaleFactor
|
width: 20 * screenScaleFactor
|
||||||
height: width
|
height: width
|
||||||
|
|
||||||
source: UM.Theme.getIcon("update")
|
source: Cura.API.account.manualSyncEnabled ? UM.Theme.getIcon("update") : UM.Theme.getIcon("checked")
|
||||||
color: palette.text
|
color: palette.text
|
||||||
|
|
||||||
RotationAnimator
|
RotationAnimator
|
||||||
|
@ -54,10 +50,11 @@ Row // sync state icon + message
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
id: stateLabel
|
id: stateLabel
|
||||||
text: catalog.i18nc("@state", "Checking...")
|
text: catalog.i18nc("@state", catalog.i18nc("@label", "You are in sync with your account"))
|
||||||
color: UM.Theme.getColor("text")
|
color: UM.Theme.getColor("text")
|
||||||
font: UM.Theme.getFont("medium")
|
font: UM.Theme.getFont("medium")
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
|
visible: !Cura.API.account.manualSyncEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
Label
|
||||||
|
@ -67,11 +64,13 @@ Row // sync state icon + message
|
||||||
color: UM.Theme.getColor("secondary_button_text")
|
color: UM.Theme.getColor("secondary_button_text")
|
||||||
font: UM.Theme.getFont("medium")
|
font: UM.Theme.getFont("medium")
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
|
visible: Cura.API.account.manualSyncEnabled
|
||||||
|
height: visible ? accountSyncButton.intrinsicHeight : 0
|
||||||
|
|
||||||
MouseArea
|
MouseArea
|
||||||
{
|
{
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: Cura.API.account.sync()
|
onClicked: Cura.API.account.sync(true)
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onEntered: accountSyncButton.font.underline = true
|
onEntered: accountSyncButton.font.underline = true
|
||||||
onExited: accountSyncButton.font.underline = false
|
onExited: accountSyncButton.font.underline = false
|
||||||
|
@ -82,25 +81,25 @@ Row // sync state icon + message
|
||||||
signal syncStateChanged(string newState)
|
signal syncStateChanged(string newState)
|
||||||
|
|
||||||
onSyncStateChanged: {
|
onSyncStateChanged: {
|
||||||
if(newState == Cura.AccountSyncState.SYNCING){
|
if(newState == Cura.AccountSyncState.IDLE){
|
||||||
syncRow.iconSource = UM.Theme.getIcon("update")
|
icon.source = UM.Theme.getIcon("update")
|
||||||
syncRow.labelText = catalog.i18nc("@label", "Checking...")
|
} else if(newState == Cura.AccountSyncState.SYNCING){
|
||||||
|
icon.source = UM.Theme.getIcon("update")
|
||||||
|
stateLabel.text = catalog.i18nc("@label", "Checking...")
|
||||||
} else if (newState == Cura.AccountSyncState.SUCCESS) {
|
} else if (newState == Cura.AccountSyncState.SUCCESS) {
|
||||||
syncRow.iconSource = UM.Theme.getIcon("checked")
|
icon.source = UM.Theme.getIcon("checked")
|
||||||
syncRow.labelText = catalog.i18nc("@label", "You are up to date")
|
stateLabel.text = catalog.i18nc("@label", "You are in sync with your account")
|
||||||
} else if (newState == Cura.AccountSyncState.ERROR) {
|
} else if (newState == Cura.AccountSyncState.ERROR) {
|
||||||
syncRow.iconSource = UM.Theme.getIcon("warning_light")
|
icon.source = UM.Theme.getIcon("warning_light")
|
||||||
syncRow.labelText = catalog.i18nc("@label", "Something went wrong...")
|
stateLabel.text = catalog.i18nc("@label", "Something went wrong...")
|
||||||
} else {
|
} else {
|
||||||
print("Error: unexpected sync state: " + newState)
|
print("Error: unexpected sync state: " + newState)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(newState == Cura.AccountSyncState.SYNCING){
|
if(newState == Cura.AccountSyncState.SYNCING){
|
||||||
syncRow.animateIconRotation = true
|
updateAnimator.running = true
|
||||||
syncRow.syncButtonVisible = false
|
|
||||||
} else {
|
} else {
|
||||||
syncRow.animateIconRotation = false
|
updateAnimator.running = false
|
||||||
syncRow.syncButtonVisible = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,10 @@ import Cura 1.1 as Cura
|
||||||
|
|
||||||
Column
|
Column
|
||||||
{
|
{
|
||||||
width: Math.max(title.width, accountButton.width) + 2 * UM.Theme.getSize("default_margin").width
|
width: Math.max(
|
||||||
|
Math.max(title.width, accountButton.width) + 2 * UM.Theme.getSize("default_margin").width,
|
||||||
|
syncRow.width
|
||||||
|
)
|
||||||
|
|
||||||
spacing: UM.Theme.getSize("default_margin").height
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
|
|
||||||
|
@ -29,13 +32,10 @@ Column
|
||||||
color: UM.Theme.getColor("text")
|
color: UM.Theme.getColor("text")
|
||||||
}
|
}
|
||||||
|
|
||||||
SyncState
|
SyncState {
|
||||||
{
|
|
||||||
id: syncRow
|
id: syncRow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
id: lastSyncLabel
|
id: lastSyncLabel
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
<title>checked</title>
|
<title>checked</title>
|
||||||
<desc>Created with Sketch.</desc>
|
<desc>Created with Sketch.</desc>
|
||||||
<g id="checked" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
<g id="checked" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
<g id="Check-circle" fill="#000000" fill-rule="nonzero">
|
<g transform="translate(1.000000, 1.000000)" fill="#000000" fill-rule="nonzero">
|
||||||
<path d="M8,0 C3.581722,0 1.42108547e-15,3.581722 1.42108547e-15,8 C1.42108547e-15,12.418278 3.581722,16 8,16 C12.418278,16 16,12.418278 16,8 C16,5.87826808 15.1571453,3.84343678 13.6568542,2.34314575 C12.1565632,0.842854723 10.1217319,0 8,0 Z M8,14.4 C4.4653776,14.4 1.6,11.5346224 1.6,8 C1.6,4.4653776 4.4653776,1.6 8,1.6 C11.5346224,1.6 14.4,4.4653776 14.4,8 C14.4,9.69738553 13.7257162,11.3252506 12.5254834,12.5254834 C11.3252506,13.7257162 9.69738553,14.4 8,14.4 Z" id="Shape"></path>
|
<path d="M7,0 C3.13400675,0 1.55431223e-15,3.13400675 1.55431223e-15,7 C1.55431223e-15,10.8659933 3.13400675,14 7,14 C10.8659933,14 14,10.8659933 14,7 C14,5.14348457 13.2625021,3.36300718 11.9497474,2.05025253 C10.6369928,0.737497883 8.85651541,0 7,0 Z M7,12.6 C3.9072054,12.6 1.4,10.0927946 1.4,7 C1.4,3.9072054 3.9072054,1.4 7,1.4 C10.0927946,1.4 12.6,3.9072054 12.6,7 C12.6,8.48521234 12.0100017,9.90959428 10.959798,10.959798 C9.90959428,12.0100017 8.48521234,12.6 7,12.6 Z" id="Shape"></path>
|
||||||
<polygon id="Path" points="11.44 5.04 7.2 9.28 4.56 6.64 3.44 7.76 7.2 11.52 12.56 6.16"></polygon>
|
<polygon id="Path" points="10.01 4.41 6.3 8.12 3.99 5.81 3.01 6.79 6.3 10.08 10.99 5.39"></polygon>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Loading…
Add table
Add a link
Reference in a new issue