diff --git a/cura/API/Account.py b/cura/API/Account.py
index a04f97ef1c..8a8b708cfa 100644
--- a/cura/API/Account.py
+++ b/cura/API/Account.py
@@ -57,7 +57,6 @@ class Account(QObject):
def initialize(self) -> None:
self._authorization_service.initialize(self._application.getPreferences())
-
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
self._authorization_service.loadAuthDataFromPreferences()
diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py
index b4bf4af83e..314b5631f8 100644
--- a/cura/OAuth2/AuthorizationHelpers.py
+++ b/cura/OAuth2/AuthorizationHelpers.py
@@ -1,18 +1,20 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
-
-from base64 import b64encode
-from hashlib import sha512
+from datetime import datetime
import json
import random
-import requests
+from hashlib import sha512
+from base64 import b64encode
from typing import Optional
+import requests
+
from UM.i18n import i18nCatalog
from UM.Logger import Logger
-from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
+from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
catalog = i18nCatalog("cura")
+TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
## Class containing several helpers to deal with the authorization flow.
class AuthorizationHelpers:
@@ -72,12 +74,13 @@ class AuthorizationHelpers:
if token_response.status_code not in (200, 201):
return AuthenticationResponse(success = False, err_message = token_data["error_description"])
- return AuthenticationResponse(success = True,
- token_type = token_data["token_type"],
- access_token = token_data["access_token"],
- refresh_token = token_data["refresh_token"],
- expires_in = token_data["expires_in"],
- scope = token_data["scope"])
+ return AuthenticationResponse(success=True,
+ token_type=token_data["token_type"],
+ access_token=token_data["access_token"],
+ refresh_token=token_data["refresh_token"],
+ expires_in=token_data["expires_in"],
+ scope=token_data["scope"],
+ received_at=datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT))
## Calls the authentication API endpoint to get the token data.
# \param access_token: The encoded JWT token.
diff --git a/cura/OAuth2/AuthorizationRequestHandler.py b/cura/OAuth2/AuthorizationRequestHandler.py
index 8f0982ccbf..66ecfc2787 100644
--- a/cura/OAuth2/AuthorizationRequestHandler.py
+++ b/cura/OAuth2/AuthorizationRequestHandler.py
@@ -21,7 +21,7 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
super().__init__(request, client_address, server)
# These values will be injected by the HTTPServer that this handler belongs to.
- self.authorization_helpers = None # type: Optional["AuthorizationHelpers"]
+ self.authorization_helpers = None # type: Optional[AuthorizationHelpers]
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
self.verification_code = None # type: Optional[str]
diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py
index 5117a2da66..377ec080aa 100644
--- a/cura/OAuth2/AuthorizationService.py
+++ b/cura/OAuth2/AuthorizationService.py
@@ -3,6 +3,7 @@
import json
import webbrowser
+from datetime import datetime, timedelta
from typing import Optional, TYPE_CHECKING
from urllib.parse import urlencode
import requests.exceptions
@@ -12,7 +13,7 @@ from UM.Logger import Logger
from UM.Signal import Signal
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
-from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
+from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
from cura.OAuth2.Models import AuthenticationResponse
if TYPE_CHECKING:
@@ -87,17 +88,19 @@ class AuthorizationService:
## Get the access token as provided by the repsonse data.
def getAccessToken(self) -> Optional[str]:
- if not self.getUserProfile():
- # We check if we can get the user profile.
- # If we can't get it, that means the access token (JWT) was invalid or expired.
- Logger.log("w", "Unable to get the user profile.")
- return None
-
if self._auth_data is None:
Logger.log("d", "No auth data to retrieve the access_token from")
return None
- return self._auth_data.access_token
+ # Check if the current access token is expired and refresh it if that is the case.
+ # We have a fallback on a date far in the past for currently stored auth data in cura.cfg.
+ received_at = datetime.strptime(self._auth_data.received_at, TOKEN_TIMESTAMP_FORMAT) \
+ if self._auth_data.received_at else datetime(2000, 1, 1)
+ expiry_date = received_at + timedelta(seconds = float(self._auth_data.expires_in or 0))
+ if datetime.now() > expiry_date:
+ self.refreshAccessToken()
+
+ return self._auth_data.access_token if self._auth_data else None
## Try to refresh the access token. This should be used when it has expired.
def refreshAccessToken(self) -> None:
diff --git a/cura/OAuth2/Models.py b/cura/OAuth2/Models.py
index 62b62c42e0..468351c62b 100644
--- a/cura/OAuth2/Models.py
+++ b/cura/OAuth2/Models.py
@@ -1,6 +1,5 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
-
from typing import Optional
@@ -38,12 +37,13 @@ class AuthenticationResponse(BaseModel):
expires_in = None # type: Optional[str]
scope = None # type: Optional[str]
err_message = None # type: Optional[str]
+ received_at = None # type: Optional[str]
## Response status template.
class ResponseStatus(BaseModel):
code = 200 # type: int
- message = "" # type str
+ message = "" # type: str
## Response data template.
diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py
index 0d21f1e1b6..202224dd64 100755
--- a/cura/Settings/MachineManager.py
+++ b/cura/Settings/MachineManager.py
@@ -511,13 +511,13 @@ class MachineManager(QObject):
@pyqtProperty(str, notify = globalContainerChanged)
def activeMachineFirmwareVersion(self) -> str:
- if not self._printer_output_devices[0]:
+ if not self._printer_output_devices:
return ""
return self._printer_output_devices[0].firmwareVersion
@pyqtProperty(str, notify = globalContainerChanged)
def activeMachineAddress(self) -> str:
- if not self._printer_output_devices[0]:
+ if not self._printer_output_devices:
return ""
return self._printer_output_devices[0].address
@@ -547,14 +547,18 @@ class MachineManager(QObject):
return bool(self._printer_output_devices) and len(self._printer_output_devices[0].printers) > 1
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
- def activeMachineHasActiveNetworkConnection(self) -> bool:
+ def activeMachineHasNetworkConnection(self) -> bool:
# A network connection is only available if any output device is actually a network connected device.
return any(d.connectionType == ConnectionType.NetworkConnection for d in self._printer_output_devices)
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
- def activeMachineHasActiveCloudConnection(self) -> bool:
+ def activeMachineHasCloudConnection(self) -> bool:
# A cloud connection is only available if any output device actually is a cloud connected device.
return any(d.connectionType == ConnectionType.CloudConnection for d in self._printer_output_devices)
+
+ @pyqtProperty(bool, notify = printerConnectedStatusChanged)
+ def activeMachineIsUsingCloudConnection(self) -> bool:
+ return self.activeMachineHasCloudConnection and not self.activeMachineHasNetworkConnection
def activeMachineNetworkKey(self) -> str:
if self._global_container_stack:
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml
index 0bf3d0ca52..a23b8ab0d3 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml
@@ -24,7 +24,7 @@ Item
// If the printer is a cloud printer or not. Other items base their enabled state off of this boolean. In the future
// they might not need to though.
- property bool cloudConnection: Cura.MachineManager.activeMachineHasActiveCloudConnection
+ property bool cloudConnection: Cura.MachineManager.activeMachineIsUsingCloudConnection
width: parent.width
height: childrenRect.height
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml
index 0e541c484d..8c63e1ef1a 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml
@@ -30,7 +30,7 @@ Item
// If the printer is a cloud printer or not. Other items base their enabled state off of this boolean. In the future
// they might not need to though.
- property bool cloudConnection: Cura.MachineManager.activeMachineHasActiveCloudConnection
+ property bool cloudConnection: Cura.MachineManager.activeMachineIsUsingCloudConnection
width: 834 * screenScaleFactor // TODO: Theme!
height: childrenRect.height
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py
index 9c1e8e1cdb..adff94bbbc 100644
--- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py
+++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py
@@ -103,8 +103,9 @@ class CloudApiClient:
request = QNetworkRequest(QUrl(path))
if content_type:
request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
- if self._account.isLoggedIn:
- request.setRawHeader(b"Authorization", "Bearer {}".format(self._account.accessToken).encode())
+ access_token = self._account.accessToken
+ if access_token:
+ request.setRawHeader(b"Authorization", "Bearer {}".format(access_token).encode())
return request
## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well.
diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py
index 4bf899d193..3400cae757 100644
--- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py
+++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py
@@ -4,6 +4,7 @@ import json
from queue import Queue
from threading import Event, Thread
from time import time
+import os
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
@@ -412,13 +413,18 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"]
if active_machine:
- # Check 1: Printer isn't already configured for cloud
+ # Check 1A: Printer isn't already configured for cloud
if ConnectionType.CloudConnection.value in active_machine.configuredConnectionTypes:
Logger.log("d", "Active machine was already configured for cloud.")
return
+
+ # Check 1B: Printer isn't already configured for cloud
+ if active_machine.getMetaDataEntry("cloud_flow_complete", False):
+ Logger.log("d", "Active machine was already configured for cloud.")
+ return
# Check 2: User did not already say "Don't ask me again"
- if active_machine.getMetaDataEntry("show_cloud_message", "value") is False:
+ if active_machine.getMetaDataEntry("do_not_show_cloud_message", False):
Logger.log("d", "Active machine shouldn't ask about cloud anymore.")
return
@@ -428,7 +434,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
return
# Check 4: Machine is configured for network connectivity
- if not self._application.getMachineManager().activeMachineHasActiveNetworkConnection:
+ if not self._application.getMachineManager().activeMachineHasNetworkConnection:
Logger.log("d", "Cloud Flow not possible: Machine is not connected!")
return
@@ -448,7 +454,9 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
if not self._start_cloud_flow_message:
self._start_cloud_flow_message = Message(
text = i18n_catalog.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."),
- image_source = "../../../../../Cura/plugins/UM3NetworkPrinting/resources/svg/cloud-flow-start.svg",
+ lifetime = 0,
+ image_source = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "resources", "svg",
+ "cloud-flow-start.svg"),
image_caption = i18n_catalog.i18nc("@info:status", "Connect to Ultimaker Cloud"),
option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."),
option_state = False
@@ -469,7 +477,8 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self._cloud_flow_complete_message = Message(
text = i18n_catalog.i18nc("@info:status", "You can now send and monitor print jobs from anywhere using your Ultimaker account."),
lifetime = 30,
- image_source = "../../../../../Cura/plugins/UM3NetworkPrinting/resources/svg/cloud-flow-completed.svg",
+ image_source = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "resources", "svg",
+ "cloud-flow-completed.svg"),
image_caption = i18n_catalog.i18nc("@info:status", "Connected!")
)
self._cloud_flow_complete_message.addAction("", i18n_catalog.i18nc("@action", "Review your connection"), "", "", 1) # TODO: Icon
@@ -479,13 +488,13 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
# Set the machine's cloud flow as complete so we don't ask the user again and again for cloud connected printers
active_machine = self._application.getMachineManager().activeMachine
if active_machine:
- active_machine.setMetaDataEntry("cloud_flow_complete", True)
+ active_machine.setMetaDataEntry("do_not_show_cloud_message", True)
return
- def _onDontAskMeAgain(self, messageId: str, checked: bool) -> None:
+ def _onDontAskMeAgain(self, messageId: str) -> None:
active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"]
if active_machine:
- active_machine.setMetaDataEntry("show_cloud_message", False)
+ active_machine.setMetaDataEntry("do_not_show_cloud_message", True)
Logger.log("d", "Will not ask the user again to cloud connect for current printer.")
return
diff --git a/resources/qml/Dialogs/AboutDialog.qml b/resources/qml/Dialogs/AboutDialog.qml
index 9a92bcbe1f..604cbc16ba 100644
--- a/resources/qml/Dialogs/AboutDialog.qml
+++ b/resources/qml/Dialogs/AboutDialog.qml
@@ -28,7 +28,7 @@ UM.Dialog
anchors.topMargin: - margin
anchors.horizontalCenter: parent.horizontalCenter
- color: UM.Theme.getColor("viewport_background")
+ color: UM.Theme.getColor("main_window_header_background")
}
Image
@@ -52,7 +52,7 @@ UM.Dialog
text: catalog.i18nc("@label","version: %1").arg(UM.Application.version)
font: UM.Theme.getFont("large_bold")
- color: UM.Theme.getColor("text")
+ color: UM.Theme.getColor("button_text")
anchors.right : logo.right
anchors.top: logo.bottom
anchors.topMargin: (UM.Theme.getSize("default_margin").height / 2) | 0
diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml
index cd5e041606..e9452f4d35 100644
--- a/resources/qml/PrinterSelector/MachineSelector.qml
+++ b/resources/qml/PrinterSelector/MachineSelector.qml
@@ -11,8 +11,8 @@ Cura.ExpandablePopup
{
id: machineSelector
- property bool isNetworkPrinter: Cura.MachineManager.activeMachineHasActiveNetworkConnection
- property bool isCloudPrinter: Cura.MachineManager.activeMachineHasActiveCloudConnection
+ property bool isNetworkPrinter: Cura.MachineManager.activeMachineHasNetworkConnection
+ property bool isCloudPrinter: Cura.MachineManager.activeMachineHasCloudConnection
property bool isGroup: Cura.MachineManager.activeMachineIsGroup
contentPadding: UM.Theme.getSize("default_lining").width
diff --git a/resources/themes/cura-dark/icons/sign_in_to_cloud.svg b/resources/themes/cura-dark/icons/sign_in_to_cloud.svg
new file mode 100644
index 0000000000..09ba300b6a
--- /dev/null
+++ b/resources/themes/cura-dark/icons/sign_in_to_cloud.svg
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file
diff --git a/resources/themes/cura-light/icons/sign_in_to_cloud.svg b/resources/themes/cura-light/icons/sign_in_to_cloud.svg
index 5c8f39a85b..27471fddce 100644
--- a/resources/themes/cura-light/icons/sign_in_to_cloud.svg
+++ b/resources/themes/cura-light/icons/sign_in_to_cloud.svg
@@ -1,25 +1,14 @@
-