mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-06-04 11:14:21 +08:00
Merge branch 'CURA-7290_manual_account_sync' of github.com:Ultimaker/Cura
This commit is contained in:
commit
36cdf2edbe
@ -1,9 +1,11 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, Dict, TYPE_CHECKING
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, TYPE_CHECKING, Union
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, Q_ENUMS
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura.OAuth2.AuthorizationService import AuthorizationService
|
||||
@ -16,6 +18,13 @@ if TYPE_CHECKING:
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class SyncState:
|
||||
"""QML: Cura.AccountSyncState"""
|
||||
SYNCING = 0
|
||||
SUCCESS = 1
|
||||
ERROR = 2
|
||||
|
||||
|
||||
## The account API provides a version-proof bridge to use Ultimaker Accounts
|
||||
#
|
||||
# Usage:
|
||||
@ -26,9 +35,21 @@ i18n_catalog = i18nCatalog("cura")
|
||||
# api.account.userProfile # Who is logged in``
|
||||
#
|
||||
class Account(QObject):
|
||||
# The interval in which sync services are automatically triggered
|
||||
SYNC_INTERVAL = 30.0 # seconds
|
||||
Q_ENUMS(SyncState)
|
||||
|
||||
# Signal emitted when user logged in or out.
|
||||
loginStateChanged = pyqtSignal(bool)
|
||||
accessTokenChanged = pyqtSignal()
|
||||
syncRequested = pyqtSignal()
|
||||
"""Sync services may connect to this signal to receive sync triggers.
|
||||
Services should be resilient to receiving a signal while they are still syncing,
|
||||
either by ignoring subsequent signals or restarting a sync.
|
||||
See setSyncState() for providing user feedback on the state of your service.
|
||||
"""
|
||||
lastSyncDateTimeChanged = pyqtSignal()
|
||||
syncStateChanged = pyqtSignal(int) # because SyncState is an int Enum
|
||||
|
||||
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
@ -37,6 +58,8 @@ class Account(QObject):
|
||||
|
||||
self._error_message = None # type: Optional[Message]
|
||||
self._logged_in = False
|
||||
self._sync_state = SyncState.SUCCESS
|
||||
self._last_sync_str = "-"
|
||||
|
||||
self._callback_port = 32118
|
||||
self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
|
||||
@ -56,6 +79,16 @@ class Account(QObject):
|
||||
|
||||
self._authorization_service = AuthorizationService(self._oauth_settings)
|
||||
|
||||
# Create a timer for automatic account sync
|
||||
self._update_timer = QTimer()
|
||||
self._update_timer.setInterval(int(self.SYNC_INTERVAL * 1000))
|
||||
# The timer is restarted explicitly after an update was processed. This prevents 2 concurrent updates
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self.syncRequested)
|
||||
|
||||
self._sync_services = {} # type: Dict[str, int]
|
||||
"""contains entries "service_name" : SyncState"""
|
||||
|
||||
def initialize(self) -> None:
|
||||
self._authorization_service.initialize(self._application.getPreferences())
|
||||
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
|
||||
@ -63,6 +96,39 @@ class Account(QObject):
|
||||
self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
|
||||
self._authorization_service.loadAuthDataFromPreferences()
|
||||
|
||||
def setSyncState(self, service_name: str, state: int) -> None:
|
||||
""" Can be used to register sync services and update account sync states
|
||||
|
||||
Contract: A sync service is expected exit syncing state in all cases, within reasonable time
|
||||
|
||||
Example: `setSyncState("PluginSyncService", SyncState.SYNCING)`
|
||||
:param service_name: A unique name for your service, such as `plugins` or `backups`
|
||||
:param state: One of SyncState
|
||||
"""
|
||||
|
||||
prev_state = self._sync_state
|
||||
|
||||
self._sync_services[service_name] = state
|
||||
|
||||
if any(val == SyncState.SYNCING for val in self._sync_services.values()):
|
||||
self._sync_state = SyncState.SYNCING
|
||||
elif any(val == SyncState.ERROR for val in self._sync_services.values()):
|
||||
self._sync_state = SyncState.ERROR
|
||||
else:
|
||||
self._sync_state = SyncState.SUCCESS
|
||||
|
||||
if self._sync_state != prev_state:
|
||||
self.syncStateChanged.emit(self._sync_state)
|
||||
|
||||
if self._sync_state == SyncState.SUCCESS:
|
||||
self._last_sync_str = datetime.now().strftime("%d/%m/%Y %H:%M")
|
||||
self.lastSyncDateTimeChanged.emit()
|
||||
|
||||
if self._sync_state != SyncState.SYNCING:
|
||||
# schedule new auto update after syncing completed (for whatever reason)
|
||||
if not self._update_timer.isActive():
|
||||
self._update_timer.start()
|
||||
|
||||
def _onAccessTokenChanged(self):
|
||||
self.accessTokenChanged.emit()
|
||||
|
||||
@ -83,11 +149,18 @@ class Account(QObject):
|
||||
self._error_message.show()
|
||||
self._logged_in = False
|
||||
self.loginStateChanged.emit(False)
|
||||
if self._update_timer.isActive():
|
||||
self._update_timer.stop()
|
||||
return
|
||||
|
||||
if self._logged_in != logged_in:
|
||||
self._logged_in = logged_in
|
||||
self.loginStateChanged.emit(logged_in)
|
||||
if logged_in:
|
||||
self.sync()
|
||||
else:
|
||||
if self._update_timer.isActive():
|
||||
self._update_timer.stop()
|
||||
|
||||
@pyqtSlot()
|
||||
def login(self) -> None:
|
||||
@ -123,6 +196,25 @@ class Account(QObject):
|
||||
return None
|
||||
return user_profile.__dict__
|
||||
|
||||
@pyqtProperty(str, notify=lastSyncDateTimeChanged)
|
||||
def lastSyncDateTime(self) -> str:
|
||||
return self._last_sync_str
|
||||
|
||||
@pyqtSlot()
|
||||
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()
|
||||
|
||||
@pyqtSlot()
|
||||
def logout(self) -> None:
|
||||
if not self._logged_in:
|
||||
|
@ -48,6 +48,7 @@ from UM.Workspace.WorkspaceReader import WorkspaceReader
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura import ApplicationMetadata
|
||||
from cura.API import CuraAPI
|
||||
from cura.API.Account import Account
|
||||
from cura.Arranging.Arrange import Arrange
|
||||
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||
@ -1113,6 +1114,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
from cura.API import CuraAPI
|
||||
qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
|
||||
qmlRegisterUncreatableType(Account, "Cura", 1, 0, "AccountSyncState", "Could not create AccountSyncState")
|
||||
|
||||
# As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
|
||||
actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
|
||||
|
@ -2,7 +2,7 @@
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import json
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Set
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QObject
|
||||
@ -13,6 +13,7 @@ from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
|
||||
from cura.API.Account import SyncState
|
||||
from cura.CuraApplication import CuraApplication, ApplicationMetadata
|
||||
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
|
||||
from .SubscribedPackagesModel import SubscribedPackagesModel
|
||||
@ -20,6 +21,9 @@ from ..CloudApiModel import CloudApiModel
|
||||
|
||||
|
||||
class CloudPackageChecker(QObject):
|
||||
|
||||
SYNC_SERVICE_NAME = "CloudPackageChecker"
|
||||
|
||||
def __init__(self, application: CuraApplication) -> None:
|
||||
super().__init__()
|
||||
|
||||
@ -32,23 +36,32 @@ class CloudPackageChecker(QObject):
|
||||
self._application.initializationFinished.connect(self._onAppInitialized)
|
||||
self._i18n_catalog = i18nCatalog("cura")
|
||||
self._sdk_version = ApplicationMetadata.CuraSDKVersion
|
||||
self._last_notified_packages = set() # type: Set[str]
|
||||
"""Packages for which a notification has been shown. No need to bother the user twice fo equal content"""
|
||||
|
||||
# This is a plugin, so most of the components required are not ready when
|
||||
# this is initialized. Therefore, we wait until the application is ready.
|
||||
def _onAppInitialized(self) -> None:
|
||||
self._package_manager = self._application.getPackageManager()
|
||||
# initial check
|
||||
self._onLoginStateChanged()
|
||||
# check again whenever the login state changes
|
||||
self._getPackagesIfLoggedIn()
|
||||
|
||||
self._application.getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
|
||||
self._application.getCuraAPI().account.syncRequested.connect(self._getPackagesIfLoggedIn)
|
||||
|
||||
def _onLoginStateChanged(self) -> None:
|
||||
# reset session
|
||||
self._last_notified_packages = set()
|
||||
self._getPackagesIfLoggedIn()
|
||||
|
||||
def _getPackagesIfLoggedIn(self) -> None:
|
||||
if self._application.getCuraAPI().account.isLoggedIn:
|
||||
self._getUserSubscribedPackages()
|
||||
else:
|
||||
self._hideSyncMessage()
|
||||
|
||||
def _getUserSubscribedPackages(self) -> None:
|
||||
self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SYNCING)
|
||||
Logger.debug("Requesting subscribed packages metadata from server.")
|
||||
url = CloudApiModel.api_url_user_packages
|
||||
self._application.getHttpRequestManager().get(url,
|
||||
@ -61,6 +74,7 @@ class CloudPackageChecker(QObject):
|
||||
Logger.log("w",
|
||||
"Requesting user packages failed, response code %s while trying to connect to %s",
|
||||
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
|
||||
self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
|
||||
return
|
||||
|
||||
try:
|
||||
@ -69,15 +83,22 @@ class CloudPackageChecker(QObject):
|
||||
if "errors" in json_data:
|
||||
for error in json_data["errors"]:
|
||||
Logger.log("e", "%s", error["title"])
|
||||
self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
|
||||
return
|
||||
self._handleCompatibilityData(json_data["data"])
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.log("w", "Received invalid JSON for user subscribed packages from the Web Marketplace")
|
||||
|
||||
self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS)
|
||||
|
||||
def _handleCompatibilityData(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None:
|
||||
user_subscribed_packages = {plugin["package_id"] for plugin in subscribed_packages_payload}
|
||||
user_installed_packages = self._package_manager.getAllInstalledPackageIDs()
|
||||
|
||||
if user_subscribed_packages == self._last_notified_packages:
|
||||
# already notified user about these
|
||||
return
|
||||
|
||||
# We need to re-evaluate the dismissed packages
|
||||
# (i.e. some package might got updated to the correct SDK version in the meantime,
|
||||
# hence remove them from the Dismissed Incompatible list)
|
||||
@ -87,12 +108,13 @@ class CloudPackageChecker(QObject):
|
||||
user_installed_packages += user_dismissed_packages
|
||||
|
||||
# We check if there are packages installed in Web Marketplace but not in Cura marketplace
|
||||
package_discrepancy = list(set(user_subscribed_packages).difference(user_installed_packages))
|
||||
package_discrepancy = list(user_subscribed_packages.difference(user_installed_packages))
|
||||
if package_discrepancy:
|
||||
Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages")
|
||||
self._model.addDiscrepancies(package_discrepancy)
|
||||
self._model.initialize(self._package_manager, subscribed_packages_payload)
|
||||
self._showSyncMessage()
|
||||
self._last_notified_packages = user_subscribed_packages
|
||||
|
||||
def _showSyncMessage(self) -> None:
|
||||
"""Show the message if it is not already shown"""
|
||||
|
@ -53,10 +53,10 @@ class CloudApiClient:
|
||||
|
||||
## Retrieves all the clusters for the user that is currently logged in.
|
||||
# \param on_finished: The function to be called after the result is parsed.
|
||||
def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any]) -> None:
|
||||
def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any], failed: Callable) -> None:
|
||||
url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT)
|
||||
reply = self._manager.get(self._createEmptyRequest(url))
|
||||
self._addCallback(reply, on_finished, CloudClusterResponse)
|
||||
self._addCallback(reply, on_finished, CloudClusterResponse, failed)
|
||||
|
||||
## Retrieves the status of the given cluster.
|
||||
# \param cluster_id: The ID of the cluster.
|
||||
@ -166,16 +166,24 @@ class CloudApiClient:
|
||||
reply: QNetworkReply,
|
||||
on_finished: Union[Callable[[CloudApiClientModel], Any],
|
||||
Callable[[List[CloudApiClientModel]], Any]],
|
||||
model: Type[CloudApiClientModel]) -> None:
|
||||
model: Type[CloudApiClientModel],
|
||||
on_error: Optional[Callable] = None) -> None:
|
||||
def parse() -> None:
|
||||
self._anti_gc_callbacks.remove(parse)
|
||||
|
||||
# Don't try to parse the reply if we didn't get one
|
||||
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None:
|
||||
if on_error is not None:
|
||||
on_error()
|
||||
return
|
||||
|
||||
status_code, response = self._parseReply(reply)
|
||||
self._parseModels(response, on_finished, model)
|
||||
if status_code >= 300 and on_error is not None:
|
||||
on_error()
|
||||
else:
|
||||
self._parseModels(response, on_finished, model)
|
||||
|
||||
self._anti_gc_callbacks.append(parse)
|
||||
reply.finished.connect(parse)
|
||||
if on_error is not None:
|
||||
reply.error.connect(on_error)
|
||||
|
@ -10,6 +10,7 @@ from UM.Logger import Logger # To log errors talking to the API.
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
from cura.API import Account
|
||||
from cura.API.Account import SyncState
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
@ -27,9 +28,7 @@ class CloudOutputDeviceManager:
|
||||
|
||||
META_CLUSTER_ID = "um_cloud_cluster_id"
|
||||
META_NETWORK_KEY = "um_network_key"
|
||||
|
||||
# The interval with which the remote clusters are checked
|
||||
CHECK_CLUSTER_INTERVAL = 30.0 # seconds
|
||||
SYNC_SERVICE_NAME = "CloudOutputDeviceManager"
|
||||
|
||||
# The translation catalog for this device.
|
||||
I18N_CATALOG = i18nCatalog("cura")
|
||||
@ -44,16 +43,11 @@ class CloudOutputDeviceManager:
|
||||
self._api = CloudApiClient(self._account, on_error = lambda error: Logger.log("e", str(error)))
|
||||
self._account.loginStateChanged.connect(self._onLoginStateChanged)
|
||||
|
||||
# Create a timer to update the remote cluster list
|
||||
self._update_timer = QTimer()
|
||||
self._update_timer.setInterval(int(self.CHECK_CLUSTER_INTERVAL * 1000))
|
||||
# The timer is restarted explicitly after an update was processed. This prevents 2 concurrent updates
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._getRemoteClusters)
|
||||
|
||||
# Ensure we don't start twice.
|
||||
self._running = False
|
||||
|
||||
self._syncing = False
|
||||
|
||||
def start(self):
|
||||
"""Starts running the cloud output device manager, thus periodically requesting cloud data."""
|
||||
|
||||
@ -62,18 +56,16 @@ class CloudOutputDeviceManager:
|
||||
if not self._account.isLoggedIn:
|
||||
return
|
||||
self._running = True
|
||||
if not self._update_timer.isActive():
|
||||
self._update_timer.start()
|
||||
self._getRemoteClusters()
|
||||
|
||||
self._account.syncRequested.connect(self._getRemoteClusters)
|
||||
|
||||
def stop(self):
|
||||
"""Stops running the cloud output device manager."""
|
||||
|
||||
if not self._running:
|
||||
return
|
||||
self._running = False
|
||||
if self._update_timer.isActive():
|
||||
self._update_timer.stop()
|
||||
self._onGetRemoteClustersFinished([]) # Make sure we remove all cloud output devices.
|
||||
|
||||
def refreshConnections(self) -> None:
|
||||
@ -92,7 +84,14 @@ class CloudOutputDeviceManager:
|
||||
def _getRemoteClusters(self) -> None:
|
||||
"""Gets all remote clusters from the API."""
|
||||
|
||||
self._api.getClusters(self._onGetRemoteClustersFinished)
|
||||
if self._syncing:
|
||||
return
|
||||
|
||||
Logger.info("Syncing cloud printer clusters")
|
||||
|
||||
self._syncing = True
|
||||
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SYNCING)
|
||||
self._api.getClusters(self._onGetRemoteClustersFinished, self._onGetRemoteClusterFailed)
|
||||
|
||||
def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None:
|
||||
"""Callback for when the request for getting the clusters is finished."""
|
||||
@ -115,8 +114,13 @@ class CloudOutputDeviceManager:
|
||||
if removed_device_keys:
|
||||
# If the removed device was active we should connect to the new active device
|
||||
self._connectToActiveMachine()
|
||||
# Schedule a new update
|
||||
self._update_timer.start()
|
||||
|
||||
self._syncing = False
|
||||
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS)
|
||||
|
||||
def _onGetRemoteClusterFailed(self):
|
||||
self._syncing = False
|
||||
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
|
||||
|
||||
def _onDevicesDiscovered(self, clusters: List[CloudClusterResponse]) -> None:
|
||||
"""**Synchronously** create machines for discovered devices
|
||||
|
110
resources/qml/Account/SyncState.qml
Normal file
110
resources/qml/Account/SyncState.qml
Normal file
@ -0,0 +1,110 @@
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import UM 1.4 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
Row // sync state icon + message
|
||||
{
|
||||
|
||||
property alias iconSource: icon.source
|
||||
property alias labelText: stateLabel.text
|
||||
property alias syncButtonVisible: accountSyncButton.visible
|
||||
property alias animateIconRotation: updateAnimator.running
|
||||
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: UM.Theme.getSize("narrow_margin").height
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: icon
|
||||
width: 20 * screenScaleFactor
|
||||
height: width
|
||||
|
||||
source: UM.Theme.getIcon("update")
|
||||
color: palette.text
|
||||
|
||||
RotationAnimator
|
||||
{
|
||||
id: updateAnimator
|
||||
target: icon
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
running: true
|
||||
|
||||
// reset rotation when stopped
|
||||
onRunningChanged: {
|
||||
if(!running)
|
||||
{
|
||||
icon.rotation = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
|
||||
Label
|
||||
{
|
||||
id: stateLabel
|
||||
text: catalog.i18nc("@state", "Checking...")
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("medium")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: accountSyncButton
|
||||
text: catalog.i18nc("@button", "Check for account updates")
|
||||
color: UM.Theme.getColor("secondary_button_text")
|
||||
font: UM.Theme.getFont("medium")
|
||||
renderType: Text.NativeRendering
|
||||
|
||||
MouseArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
onClicked: Cura.API.account.sync()
|
||||
hoverEnabled: true
|
||||
onEntered: accountSyncButton.font.underline = true
|
||||
onExited: accountSyncButton.font.underline = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signal syncStateChanged(string newState)
|
||||
|
||||
onSyncStateChanged: {
|
||||
if(newState == Cura.AccountSyncState.SYNCING){
|
||||
syncRow.iconSource = UM.Theme.getIcon("update")
|
||||
syncRow.labelText = catalog.i18nc("@label", "Checking...")
|
||||
} else if (newState == Cura.AccountSyncState.SUCCESS) {
|
||||
syncRow.iconSource = UM.Theme.getIcon("checked")
|
||||
syncRow.labelText = catalog.i18nc("@label", "You are up to date")
|
||||
} else if (newState == Cura.AccountSyncState.ERROR) {
|
||||
syncRow.iconSource = UM.Theme.getIcon("warning_light")
|
||||
syncRow.labelText = catalog.i18nc("@label", "Something went wrong...")
|
||||
} else {
|
||||
print("Error: unexpected sync state: " + newState)
|
||||
}
|
||||
|
||||
if(newState == Cura.AccountSyncState.SYNCING){
|
||||
syncRow.animateIconRotation = true
|
||||
syncRow.syncButtonVisible = false
|
||||
} else {
|
||||
syncRow.animateIconRotation = false
|
||||
syncRow.syncButtonVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: Cura.API.account.syncStateChanged.connect(syncStateChanged)
|
||||
|
||||
|
||||
}
|
@ -13,6 +13,11 @@ Column
|
||||
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
|
||||
SystemPalette
|
||||
{
|
||||
id: palette
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: title
|
||||
@ -24,6 +29,24 @@ Column
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
SyncState
|
||||
{
|
||||
id: syncRow
|
||||
}
|
||||
|
||||
|
||||
|
||||
Label
|
||||
{
|
||||
id: lastSyncLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
renderType: Text.NativeRendering
|
||||
text: catalog.i18nc("@label The argument is a timestamp", "Last update: %1").arg(Cura.API.account.lastSyncDateTime)
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
}
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: accountButton
|
||||
@ -53,4 +76,5 @@ Column
|
||||
onExited: signOutButton.font.underline = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
12
resources/themes/cura-light/icons/checked.svg
Normal file
12
resources/themes/cura-light/icons/checked.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<?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 63.1 (92452) - https://sketch.com -->
|
||||
<title>checked</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="checked" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Check-circle" 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>
|
||||
<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>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
12
resources/themes/cura-light/icons/update.svg
Normal file
12
resources/themes/cura-light/icons/update.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<?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 63.1 (92452) - https://sketch.com -->
|
||||
<title>update</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="update" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Update" transform="translate(1.000000, 0.000000)" fill="#000000" fill-rule="nonzero">
|
||||
<path d="M12.8,8 C12.8,11.0927946 10.2927946,13.6 7.2,13.6 C5.73088329,13.6179028 4.31699148,13.0408041 3.28,12 L6.4,12 L6.4,10.4 L0.8,10.4 L0.8,16 L2.4,16 L2.4,13.36 C3.71086874,14.5564475 5.42526687,15.2136335 7.2,15.2 C11.1764502,15.2 14.4,11.9764502 14.4,8 L12.8,8 Z" id="Path"></path>
|
||||
<path d="M7.2,2.4 C8.66911671,2.38209724 10.0830085,2.95919594 11.12,4 L8,4 L8,5.6 L13.6,5.6 L13.6,0 L12,0 L12,2.64 C10.6891313,1.44355247 8.97473313,0.786366515 7.2,0.8 C3.2235498,0.8 0,4.0235498 0,8 L1.6,8 C1.6,4.9072054 4.1072054,2.4 7.2,2.4 Z" id="Path"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
13
resources/themes/cura-light/icons/warning_light.svg
Normal file
13
resources/themes/cura-light/icons/warning_light.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<?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 63.1 (92452) - https://sketch.com -->
|
||||
<title>warning</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="warning" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Warning" transform="translate(0.000000, 1.000000)" fill="#000000" fill-rule="nonzero">
|
||||
<path d="M15.2727273,11.6363636 L9.23636364,0.8 C9.00246505,0.33139139 8.52373903,0.0352931909 8,0.0352931909 C7.47626097,0.0352931909 6.99753495,0.33139139 6.76363636,0.8 L0.727272727,11.6363636 C0.433127714,12.0765996 0.433127714,12.6506731 0.727272727,13.0909091 C0.97144313,13.5450025 1.44810945,13.8253945 1.96363636,13.8183177 L14.0363636,13.8183177 C14.5518905,13.8253945 15.0285569,13.5450025 15.2727273,13.0909091 C15.5668723,12.6506731 15.5668723,12.0765996 15.2727273,11.6363636 L15.2727273,11.6363636 Z M1.96363636,12.3636364 L8,1.52727273 L14.0363636,12.3636364 L1.96363636,12.3636364 Z" id="Shape"></path>
|
||||
<rect id="Rectangle" x="7.27272727" y="4.36363636" width="1.45454545" height="4.36363636"></rect>
|
||||
<rect id="Rectangle" x="7.27272727" y="10.1818182" width="1.45454545" height="1.45454545"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
Loading…
x
Reference in New Issue
Block a user