diff --git a/app/activeproject.cpp b/app/activeproject.cpp index 6f1859b0c..792957e4d 100644 --- a/app/activeproject.cpp +++ b/app/activeproject.cpp @@ -211,6 +211,11 @@ bool ActiveProject::forceLoad( const QString &filePath, bool force ) emit projectReloaded( mQgsProject ); emit positionTrackingSupportedChanged(); emit mapSketchesEnabledChanged(); + + if ( mLocalProject.isValid() ) + { + emit projectSyncCheckRequested( mLocalProject.fullName(), true ); + } } bool foundErrorsInLoadedProject = validateProject(); @@ -668,3 +673,11 @@ bool ActiveProject::photoSketchingEnabled() const return mQgsProject->readBoolEntry( QStringLiteral( "Mergin" ), QStringLiteral( "PhotoSketching/Enabled" ), false ); } + +void ActiveProject::checkForProjectUpdate() +{ + if ( isProjectLoaded() ) + { + emit projectSyncCheckRequested( projectFullName(), true ); + } +} diff --git a/app/activeproject.h b/app/activeproject.h index 92fee83b8..bc885ad53 100644 --- a/app/activeproject.h +++ b/app/activeproject.h @@ -169,6 +169,8 @@ class ActiveProject: public QObject void syncActiveProject( const LocalProject &project, SyncOptions::RequestOrigin requestOrigin ); + void projectSyncCheckRequested( const QString &projectFullName, bool withAuth ); + void mapThemeChanged( const QString &mapTheme ); void positionTrackingSupportedChanged(); @@ -192,6 +194,9 @@ class ActiveProject: public QObject void requestSync( SyncOptions::RequestOrigin requestOrigin = SyncOptions::RequestOrigin::ManualRequest ); + //! Checks whether the currently loaded project has a newer version available on the server + Q_INVOKABLE void checkForProjectUpdate(); + private: /** diff --git a/app/main.cpp b/app/main.cpp index e91cb18ec..03c2082c0 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -646,6 +646,8 @@ int main( int argc, char *argv[] ) merginApi->reloadProjectRole( activeProject.projectFullName() ); } ); + QObject::connect( &activeProject, &ActiveProject::projectSyncCheckRequested, ma.get(), &MerginApi::isProjectSyncNeeded ); + QObject::connect( ma.get(), &MerginApi::authChanged, &lambdaContext, [merginApi = ma.get(), &activeProject]() { if ( activeProject.isProjectLoaded() ) diff --git a/app/notificationmodel.cpp b/app/notificationmodel.cpp index d5fc1c74a..7209124e3 100644 --- a/app/notificationmodel.cpp +++ b/app/notificationmodel.cpp @@ -40,7 +40,8 @@ QHash NotificationModel::roleNames() const { IdRole, "id" }, { MessageRole, "message" }, { TypeRole, "type" }, - { IconRole, "icon" } + { IconRole, "icon" }, + { ActionRole, "action" } }; } @@ -60,6 +61,7 @@ QVariant NotificationModel::data( const QModelIndex &index, int role ) const if ( role == MessageRole ) return notification.message(); if ( role == TypeRole ) return notification.type(); if ( role == IconRole ) return notification.icon(); + if ( role == ActionRole ) return notification.action(); return {}; } @@ -156,6 +158,12 @@ void NotificationModel::onNotificationClicked( uint id ) emit showSyncFailedDialogClicked(); break; } + case NotificationType::ActionType::SyncProjectAction: + { + remove( id ); + emit showProjectNewVersionClicked(); + break; + } default: break; } } diff --git a/app/notificationmodel.h b/app/notificationmodel.h index 91711029c..a8cb3ec2e 100644 --- a/app/notificationmodel.h +++ b/app/notificationmodel.h @@ -43,7 +43,8 @@ class NotificationType NoAction, ShowProjectIssuesAction, ShowSwitchWorkspaceAction, - ShowSyncFailedDialog + ShowSyncFailedDialog, + SyncProjectAction }; Q_ENUM( ActionType ) @@ -89,7 +90,7 @@ class NotificationModel : public QAbstractListModel public: enum NotificationModelRoles { - IdRole = Qt::UserRole + 1, MessageRole, TypeRole, IconRole + IdRole = Qt::UserRole + 1, MessageRole, TypeRole, IconRole, ActionRole }; Q_ENUM( NotificationModelRoles ) @@ -111,6 +112,7 @@ class NotificationModel : public QAbstractListModel void rowCountChanged(); void showProjectIssuesActionClicked(); void showSwitchWorkspaceActionClicked(); + void showProjectNewVersionClicked(); void showSyncFailedDialogClicked(); private: diff --git a/app/qml/components/MMNotification.qml b/app/qml/components/MMNotification.qml index 0e7677a32..64076ce27 100644 --- a/app/qml/components/MMNotification.qml +++ b/app/qml/components/MMNotification.qml @@ -109,12 +109,18 @@ Rectangle { verticalCenter: parent.verticalCenter rightMargin: 20 * __dp } - iconSource: __style.closeIcon + iconSource: model.action === MM.NotificationType.SyncProjectAction ? __style.syncIcon : __style.closeIcon iconColor: text.color bgndColor: __style.transparentColor bgndHoverColor: __style.transparentColor - onClicked: __notificationModel.remove(model.id) + onClicked: { + if ( model.action === MM.NotificationType.SyncProjectAction ) { + __notificationModel.onNotificationClicked( model.id ) + } else { + __notificationModel.remove( model.id ) + } + } } Behavior on scale { NumberAnimation { easing.type: Easing.OutCubic; from: 0; to: 1.0; duration: 200 } } diff --git a/app/qml/main.qml b/app/qml/main.qml index d417de897..e876a7397 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -84,6 +84,8 @@ ApplicationWindow { // Stop/Start sync animation when user goes to map syncButton.iconRotateAnimationRunning = ( __syncManager.hasPendingSync( __activeProject.projectFullName() ) ) + + __activeProject.checkForProjectUpdate() } else if ( stateManager.state === "projects" ) { projectController.openPanel() @@ -1090,6 +1092,17 @@ ApplicationWindow { { ssoExpiredTokenDialog.open() } + + function onProjectSyncRequired( projectFullName ) + { + if ( __activeProject.projectFullName() === projectFullName ) + { + __notificationModel.addInfo( + qsTr( "There is a new version of the project available" ), + MM.NotificationType.SyncProjectAction + ) + } + } } Connections { @@ -1112,6 +1125,9 @@ ApplicationWindow { function onShowSyncFailedDialogClicked() { syncFailedDialog.open() } + function onShowProjectNewVersionClicked() { + __activeProject.requestSync() + } } Connections { diff --git a/app/test/testmerginapi.cpp b/app/test/testmerginapi.cpp index 2b5e0c91d..34b29f2f6 100644 --- a/app/test/testmerginapi.cpp +++ b/app/test/testmerginapi.cpp @@ -2378,6 +2378,7 @@ void TestMerginApi::testAutosync() AppSettings as; ActiveLayer al; ActiveProject activeProject( as, al, mApi->localProjectsManager() ); + QObject::connect( &activeProject, &ActiveProject::projectSyncCheckRequested, mApi, &MerginApi::isProjectSyncNeeded ); mApi->localProjectsManager().addLocalProject( projectDir, projectName ); diff --git a/core/merginapi.cpp b/core/merginapi.cpp index ab91e0611..5549be1e7 100644 --- a/core/merginapi.cpp +++ b/core/merginapi.cpp @@ -4597,3 +4597,37 @@ bool MerginApi::serverVersionIsAtLeast( const int requiredMajor, const int requi // check patch return serverPatch >= requiredPatch; } + +void MerginApi::isProjectSyncNeededFinished() +{ + QNetworkReply *r = qobject_cast( sender() ); + Q_ASSERT( r ); + + const QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); + + if ( r->error() == QNetworkReply::NoError ) + { + const QByteArray data = r->readAll(); + const MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data ); + + // skip if a sync is already in progress for this project + if ( !mTransactionalStatus.contains( projectFullName ) ) + { + const LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + if ( projectInfo.isValid() && projectInfo.localVersion != -1 && projectInfo.localVersion < serverProject.version ) + { + emit projectSyncRequired( projectFullName ); + } + } + } + r->deleteLater(); +} + +void MerginApi::isProjectSyncNeeded( const QString &projectFullName, bool withAuth ) +{ + const QNetworkReply *reply = getProjectInfo( projectFullName, withAuth ); + if ( !reply ) + return; + + connect( reply, &QNetworkReply::finished, this, &MerginApi::isProjectSyncNeededFinished ); +} diff --git a/core/merginapi.h b/core/merginapi.h index 202e042c6..1ce95e407 100644 --- a/core/merginapi.h +++ b/core/merginapi.h @@ -645,6 +645,8 @@ class MerginApi: public QObject */ Q_INVOKABLE void reloadProjectRole( const QString &projectFullName ); + Q_INVOKABLE void isProjectSyncNeeded( const QString &projectFullName, bool withAuth ); + /** * Returns the network manager used for Mergin API requests */ @@ -689,6 +691,7 @@ class MerginApi: public QObject void listProjectsFinished( const MerginProjectsList &merginProjects, int projectCount, int page, QString requestId ); void listProjectsFailed(); + void projectSyncRequired( const QString &projectFullName ); void listProjectsByNameFinished( const MerginProjectsList &merginProjects, QString requestId ); void syncProjectFinished( const QString &projectFullName, bool successfully, int version ); void projectReloadNeededAfterSync( const QString &projectFullName ); @@ -783,6 +786,8 @@ class MerginApi: public QObject // Pull slots void pullInfoReplyFinished(); + void isProjectSyncNeededFinished(); + void downloadItemReplyFinished( DownloadQueueItem item ); void cacheServerConfig();