From e68dc07930953acbe19bda0a01811f160156ea56 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Thu, 18 Feb 2021 16:31:06 +0100 Subject: [PATCH 01/53] add future classes --- app/layersproxymodel.cpp | 2 +- app/layersproxymodel.h | 6 +- app/main.cpp | 4 +- app/merginprojectmodel.h | 4 +- app/models/projectsmodel_future.cpp | 52 +++++++++++++++ app/models/projectsmodel_future.h | 84 ++++++++++++++++++++++++ app/models/projectsproxymodel_future.cpp | 14 ++++ app/models/projectsproxymodel_future.h | 27 ++++++++ app/project_future.cpp | 6 ++ app/project_future.h | 56 ++++++++++++++++ app/sources.pri | 9 +++ 11 files changed, 256 insertions(+), 8 deletions(-) create mode 100644 app/models/projectsmodel_future.cpp create mode 100644 app/models/projectsmodel_future.h create mode 100644 app/models/projectsproxymodel_future.cpp create mode 100644 app/models/projectsproxymodel_future.h create mode 100644 app/project_future.cpp create mode 100644 app/project_future.h diff --git a/app/layersproxymodel.cpp b/app/layersproxymodel.cpp index 55ffdbe25..a80443610 100644 --- a/app/layersproxymodel.cpp +++ b/app/layersproxymodel.cpp @@ -14,7 +14,7 @@ #include "qgsproject.h" #include "qgslayertree.h" -LayersProxyModel::LayersProxyModel( LayersModel *model, ModelTypes modelType ) : +LayersProxyModel::LayersProxyModel( LayersModel *model, LayerModelTypes modelType ) : mModelType( modelType ), mModel( model ) { diff --git a/app/layersproxymodel.h b/app/layersproxymodel.h index 5376533dc..ff88bbeca 100644 --- a/app/layersproxymodel.h +++ b/app/layersproxymodel.h @@ -18,7 +18,7 @@ #include "layersmodel.h" -enum ModelTypes +enum LayerModelTypes { ActiveLayerSelection, BrowseDataLayerSelection, @@ -30,7 +30,7 @@ class LayersProxyModel : public QgsMapLayerProxyModel Q_OBJECT public: - LayersProxyModel( LayersModel *model, ModelTypes modelType = ModelTypes::AllLayers ); + LayersProxyModel( LayersModel *model, LayerModelTypes modelType = LayerModelTypes::AllLayers ); bool filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const override; @@ -63,7 +63,7 @@ class LayersProxyModel : public QgsMapLayerProxyModel //! filters if input layer is visible in current map theme bool layerVisible( QgsMapLayer *layer ) const; - ModelTypes mModelType; + LayerModelTypes mModelType; LayersModel *mModel; /** diff --git a/app/main.cpp b/app/main.cpp index a5a928ffe..13d58cf9f 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -370,8 +370,8 @@ int main( int argc, char *argv[] ) // layer models LayersModel lm; - LayersProxyModel browseLpm( &lm, ModelTypes::BrowseDataLayerSelection ); - LayersProxyModel recordingLpm( &lm, ModelTypes::ActiveLayerSelection ); + LayersProxyModel browseLpm( &lm, LayerModelTypes::BrowseDataLayerSelection ); + LayersProxyModel recordingLpm( &lm, LayerModelTypes::ActiveLayerSelection ); ActiveLayer al; Loader loader( mtm, as, al ); diff --git a/app/merginprojectmodel.h b/app/merginprojectmodel.h index 1668b57d4..0c6cc9b59 100644 --- a/app/merginprojectmodel.h +++ b/app/merginprojectmodel.h @@ -78,10 +78,10 @@ class MerginProjectModel: public QAbstractListModel */ void updateModel( const MerginProjectList &merginProjects, QHash pendingProjects, int expectedProjectCount, int page ); - int filterCreator() const; + int filterCreator() const; // TODO: remove, no use void setFilterCreator( int filterCreator ); - int filterWriter() const; + int filterWriter() const; // TODO: remove, no use void setFilterWriter( int filterWriter ); QString searchExpression() const; diff --git a/app/models/projectsmodel_future.cpp b/app/models/projectsmodel_future.cpp new file mode 100644 index 000000000..7a99b2a88 --- /dev/null +++ b/app/models/projectsmodel_future.cpp @@ -0,0 +1,52 @@ +#include "projectsmodel_future.h" + + +ProjectsModel_future::ProjectsModel_future( + MerginApi *merginApi, + ProjectModelTypes modelType, + LocalProjectsManager &localProjectsManager, + QObject *parent ) : + QAbstractListModel( parent ), + mBackend( merginApi ), + mLocalProjectsManager( localProjectsManager ), + mModelType( modelType ) +{ + // TODO: connect to signals from LocalProjectsManager and MerginAPI +} + +void ProjectsModel_future::listProjects() +{ + mLastRequestId = mBackend->listProjects( "", modelTypeToFlag(), "", mPopulatedPage ); +} + +void ProjectsModel_future::mergeProjects() +{ + QList localProjects = mLocalProjectsManager.projects(); +} + +void ProjectsModel_future::listProjectsFinished() +{ + mergeProjects(); +} + +void ProjectsModel_future::projectSyncFinished() +{ + +} + +void ProjectsModel_future::projectSyncProgressChanged() +{ + +} + +QString ProjectsModel_future::modelTypeToFlag() const +{ + switch ( mModelType ) { + case MyProjectsModel: + return QStringLiteral( "created" ); + case SharedProjectsModel: + return QStringLiteral( "shared" ); + default: + return QStringLiteral( "" ); + } +} diff --git a/app/models/projectsmodel_future.h b/app/models/projectsmodel_future.h new file mode 100644 index 000000000..57f47cb44 --- /dev/null +++ b/app/models/projectsmodel_future.h @@ -0,0 +1,84 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef PROJECTSMODEL_FUTURE_H +#define PROJECTSMODEL_FUTURE_H + +#include +#include + +#include "localprojectsmanager.h" +#include "project_future.h" +#include "merginapi.h" + +/** + * \brief The ProjectModelTypes enum + */ +enum ProjectModelTypes +{ + LocalProjectsModel = 0, + MyProjectsModel, + SharedProjectsModel, + ExploreProjectsModel, + RecentProjectsModel +}; + +/** + * \brief The ProjectsModel_future class + */ +class ProjectsModel_future : public QAbstractListModel +{ + Q_OBJECT + + public: + + enum Roles + { + Project = Qt::UserRole + 1 + }; + Q_ENUMS( Roles ) + + ProjectsModel_future( MerginApi *merginApi, ProjectModelTypes modelType, LocalProjectsManager &localProjectsManager, QObject *parent = nullptr ); + ~ProjectsModel_future() override {}; + + // Needed methods from QAbstractListModel +// Q_INVOKABLE QVariant data( const QModelIndex &index, int role ) const override; +// Q_INVOKABLE QModelIndex index( int row, int column = 0, const QModelIndex &parent = QModelIndex() ) const override; +// QHash roleNames() const override; +// int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + + //! Called to list projects, either fetch more or get first + Q_INVOKABLE void listProjects(); + + //! Method detecting local project for remote projects + void mergeProjects(); + + public slots: + void listProjectsFinished(); + void projectSyncFinished(); + void projectSyncProgressChanged(); + + private: + + QString modelTypeToFlag() const; + + MerginApi *mBackend; + LocalProjectsManager &mLocalProjectsManager; + QList mProjects; + + ProjectModelTypes mModelType; + + //! For pagination + int mPopulatedPage = -1; + + //! For processing only my requests + QString mLastRequestId; +}; + +#endif // PROJECTSMODEL_FUTURE_H diff --git a/app/models/projectsproxymodel_future.cpp b/app/models/projectsproxymodel_future.cpp new file mode 100644 index 000000000..daba71fe4 --- /dev/null +++ b/app/models/projectsproxymodel_future.cpp @@ -0,0 +1,14 @@ +#include "projectsproxymodel_future.h" + +ProjectsProxyModel_future::ProjectsProxyModel_future( ProjectModelTypes modelType, QObject *parent ) : + QSortFilterProxyModel( parent ), + mModelType( modelType ) +{ + +} + +bool ProjectsProxyModel_future::filterAcceptsRow( int, const QModelIndex & ) const +{ + // return true if it passes search filter + return true; +} diff --git a/app/models/projectsproxymodel_future.h b/app/models/projectsproxymodel_future.h new file mode 100644 index 000000000..7f844ca09 --- /dev/null +++ b/app/models/projectsproxymodel_future.h @@ -0,0 +1,27 @@ +#ifndef PROJECTSPROXYMODEL_FUTURE_H +#define PROJECTSPROXYMODEL_FUTURE_H + +#include +#include + +#include "projectsmodel_future.h" + +/** + * @brief The ProjectsProxyModel_future class + */ +class ProjectsProxyModel_future : public QSortFilterProxyModel +{ + Q_OBJECT + public: + explicit ProjectsProxyModel_future( ProjectModelTypes modelType, QObject *parent = nullptr ); + ~ProjectsProxyModel_future() override {}; + + protected: + bool filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent ) const override; +// bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + + private: + ProjectModelTypes mModelType; +}; + +#endif // PROJECTSPROXYMODEL_FUTURE_H diff --git a/app/project_future.cpp b/app/project_future.cpp new file mode 100644 index 000000000..7d2370334 --- /dev/null +++ b/app/project_future.cpp @@ -0,0 +1,6 @@ +#include "project_future.h" + +Project_future::Project_future( QObject *parent ) : QObject( parent ) +{ + +} diff --git a/app/project_future.h b/app/project_future.h new file mode 100644 index 000000000..511150362 --- /dev/null +++ b/app/project_future.h @@ -0,0 +1,56 @@ +#ifndef PROJECT_FUTURE_H +#define PROJECT_FUTURE_H + +#include +#include + +enum ProjectStatus_future +{ + _NoVersion, //!< the project is not available locally + _UpToDate, //!< both server and local copy are in sync with no extra modifications + _OutOfDate, //!< server has newer version than what is available locally (but the project is not modified locally) + _Modified, //!< there are some local modifications in the project that need to be pushed (note: also server may have newer version) + _NonProjectItem //!< only for mock projects, acts like a hook to enable extra functionality for models working with projects . + // TODO: replace _NonProjectItem with footer property in ListView + // TODO2: add orphaned state? +}; +Q_ENUMS( ProjectStatus_future ) + +struct RemoteProject +{ + QString projectName; + QString projectNamespace; + + QString projectIdentifier() { return QString(); }; + + QDateTime serverUpdated; // available latest version of project files on server // TODO: maybe we do not need this at all + + bool pending = false; + ProjectStatus_future status = ProjectStatus_future::_NoVersion; + qreal progress = 0; +}; + + +struct MerginProject_deprecated +{ + QString projectName; + QString projectNamespace; + QString projectDir; // full path to the project directory + QDateTime clientUpdated; // client's version of project files + QDateTime serverUpdated; // available latest version of project files on server + bool pending = false; // if there is a pending request for downlaod/update a project + ProjectStatus_future status = ProjectStatus_future::_NoVersion; + qreal progress = 0; // progress in case of pending download/upload (values [0..1]) +}; + + +class Project_future : public QObject +{ + Q_OBJECT + public: + explicit Project_future( QObject *parent = nullptr ); + ~Project_future() override {} + +}; + +#endif // PROJECT_FUTURE_H diff --git a/app/sources.pri b/app/sources.pri index 7b74245a9..14560a67a 100644 --- a/app/sources.pri +++ b/app/sources.pri @@ -33,6 +33,9 @@ ios/iosutils.cpp \ inputprojutils.cpp \ codefilter.cpp \ qrdecoder.cpp \ +models/projectsproxymodel_future.cpp \ +models/projectsmodel_future.cpp \ +project_future.cpp \ exists(merginsecrets.cpp) { message("Using production Mergin API_KEYS") @@ -74,8 +77,14 @@ variablesmanager.h \ ios/iosimagepicker.h \ ios/iosutils.h \ inputprojutils.h \ +<<<<<<< HEAD codefilter.h \ qrdecoder.h \ +======= +models/projectsproxymodel_future.h \ +models/projectsmodel_future.h \ +project_future.h \ +>>>>>>> add future classes contains(DEFINES, INPUT_TEST) { From 62c3eef93f1777716325b1603ea0a8db4ba8ad95 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Fri, 19 Feb 2021 12:42:11 +0100 Subject: [PATCH 02/53] address comments --- app/merginprojectmodel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/merginprojectmodel.h b/app/merginprojectmodel.h index 0c6cc9b59..e45eee127 100644 --- a/app/merginprojectmodel.h +++ b/app/merginprojectmodel.h @@ -84,7 +84,7 @@ class MerginProjectModel: public QAbstractListModel int filterWriter() const; // TODO: remove, no use void setFilterWriter( int filterWriter ); - QString searchExpression() const; + QString searchExpression() const; // TODO: remove, search will be in proxy model void setSearchExpression( const QString &searchExpression ); int lastPage() const; From b07c2d5a1e695da5fc9dff50eabfd2478955544a Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Thu, 18 Feb 2021 16:09:45 +0100 Subject: [PATCH 03/53] add API to list by names and requestId --- app/merginapi.cpp | 170 ++++++++++++++++++++++++++++++++++------------ app/merginapi.h | 23 +++++-- 2 files changed, 144 insertions(+), 49 deletions(-) diff --git a/app/merginapi.cpp b/app/merginapi.cpp index eb1622bc1..a95b2d68d 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -67,12 +67,12 @@ MerginUserInfo *MerginApi::userInfo() const return mUserInfo; } -void MerginApi::listProjects( const QString &searchExpression, const QString &flag, const QString &filterTag, const int page ) +QString MerginApi::listProjects( const QString &searchExpression, const QString &flag, const QString &filterTag, const int page ) { bool authorize = !flag.isEmpty(); if ( ( authorize && !validateAuthAndContinute() ) || mApiVersionStatus != MerginApiStatus::OK ) { - return; + return QString(); } QUrlQuery query; @@ -100,11 +100,42 @@ void MerginApi::listProjects( const QString &searchExpression, const QString &fl QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() ); request.setUrl( url ); + QString requestId = InputUtils::uuidWithoutBraces( QUuid::createUuid() ); + QNetworkReply *reply = mManager.get( request ); InputUtils::log( "list projects", QStringLiteral( "Requesting: " ) + url.toString() ); - connect( reply, &QNetworkReply::finished, this, &MerginApi::listProjectsReplyFinished ); + connect( reply, &QNetworkReply::finished, this, [this, requestId]() {this->listProjectsReplyFinished( requestId );} ); + + return requestId; } +QString MerginApi::listProjectsByName( const QStringList &projectNames ) +{ + setApiRoot( defaultApiRoot() ); + // construct JSON body + QJsonDocument body; + QJsonObject projects; + QJsonArray projectsArr = QJsonArray::fromStringList( projectNames ); + + projects.insert( "projects", projectsArr ); + body.setObject( projects ); + + QUrl url( mApiRoot + QStringLiteral( "/v1/project/by_names" ) ); + + QNetworkRequest request = getDefaultRequest( true ); + request.setUrl( url ); + request.setRawHeader( "Content-type", "application/json" ); + + QString requestId = InputUtils::uuidWithoutBraces( QUuid::createUuid() ); + + QNetworkReply *reply = mManager.post( request, body.toJson() ); + InputUtils::log( "list projects by name", QStringLiteral( "Requesting: " ) + url.toString() ); + connect( reply, &QNetworkReply::finished, this, [this, requestId]() {this->listProjectsByNameReplyFinished( requestId );} ); + + return requestId; +} + + void MerginApi::downloadNextItem( const QString &projectFullName ) { Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); @@ -1178,7 +1209,7 @@ QList MerginApi::getLocalProjectFiles( const QString &projectPath ) return merginFiles; } -void MerginApi::listProjectsReplyFinished() +void MerginApi::listProjectsReplyFinished( QString requestId ) { QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); @@ -1193,12 +1224,11 @@ void MerginApi::listProjectsReplyFinished() QByteArray data = r->readAll(); QJsonDocument doc = QJsonDocument::fromJson( data ); + if ( doc.isObject() ) { - QJsonObject obj = doc.object(); - QJsonArray rawProjects = obj.value( "projects" ).toArray(); - projectCount = obj.value( "count" ).toInt(); - mRemoteProjects = parseProjectJsonArray( rawProjects ); + projectCount = doc.object().value( "count" ).toInt(); + mRemoteProjects = parseProjectsFromJson( doc ); } else { @@ -1230,9 +1260,48 @@ void MerginApi::listProjectsReplyFinished() } r->deleteLater(); + + Q_UNUSED( requestId ) + //TODO: add requestId to signal emit listProjectsFinished( mRemoteProjects, mTransactionalStatus, projectCount, requestedPage ); } +void MerginApi::listProjectsByNameReplyFinished( QString requestId ) +{ + QNetworkReply *r = qobject_cast( sender() ); + Q_ASSERT( r ); + + /* TODO: Detect orphaned project? Project that was considered Mergin but did not get info back */ + + if ( r->error() == QNetworkReply::NoError ) + { + MerginProjectList projectList; + QByteArray data = r->readAll(); + QJsonDocument json = QJsonDocument::fromJson( data ); + + projectList = parseProjectsFromJson( json ); + + for ( MerginProjectListEntry project : qAsConst( projectList ) ) + { + qDebug() << "Project: " << project.projectName; + } + + InputUtils::log( "list projects by name", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) ); + } + else + { + QString serverMsg = extractServerErrorMsg( r->readAll() ); + QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjectsByName" ), r->errorString(), serverMsg ); + emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjectsByName" ) ); + InputUtils::log( "list projects by name", QStringLiteral( "FAILED - %1" ).arg( message ) ); + } + + r->deleteLater(); + + Q_UNUSED( requestId ) + // TODO: emit signal with projects and requestId +} + void MerginApi::finalizeProjectUpdateCopy( const QString &projectFullName, const QString &projectDir, const QString &tempDir, const QString &filePath, const QList &items ) { @@ -2225,55 +2294,72 @@ ProjectDiff MerginApi::compareProjectFiles( const QList &oldServerFi return diff; } - -MerginProjectList MerginApi::parseProjectJsonArray( const QJsonArray &vArray ) +MerginProjectListEntry MerginApi::parseProjectMetadata( const QJsonObject &proj ) { + MerginProjectListEntry project; - MerginProjectList result; - for ( auto it = vArray.constBegin(); it != vArray.constEnd(); ++it ) + if ( proj.isEmpty() ) + { + return project; + } + if ( proj.contains( QStringLiteral( "error" ) ) ) { - QJsonObject projectMap = it->toObject(); - MerginProjectListEntry project; + // TODO: handle project error (might be orphaned project) - project.projectName = projectMap.value( QStringLiteral( "name" ) ).toString(); - project.projectNamespace = projectMap.value( QStringLiteral( "namespace" ) ).toString(); + proj.value( QStringLiteral( "error" ) ).toInt( 0 ); // error code + return project; + } - QString versionStr = projectMap.value( QStringLiteral( "version" ) ).toString(); - if ( versionStr.isEmpty() ) - { - project.version = 0; - } - else if ( versionStr.startsWith( "v" ) ) // cut off 'v' part from v123 - { - versionStr = versionStr.mid( 1 ); - project.version = versionStr.toInt(); - } + project.projectName = proj.value( QStringLiteral( "name" ) ).toString(); + project.projectNamespace = proj.value( QStringLiteral( "namespace" ) ).toString(); - QDateTime updated = QDateTime::fromString( projectMap.value( QStringLiteral( "updated" ) ).toString(), Qt::ISODateWithMs ).toUTC(); - if ( !updated.isValid() ) - { - project.serverUpdated = QDateTime::fromString( projectMap.value( QStringLiteral( "created" ) ).toString(), Qt::ISODateWithMs ).toUTC(); - } - else - { - project.serverUpdated = updated; - } + QString versionStr = proj.value( QStringLiteral( "version" ) ).toString(); + if ( versionStr.isEmpty() ) + { + project.version = 0; + } + else if ( versionStr.startsWith( "v" ) ) // cut off 'v' part from v123 + { + versionStr = versionStr.mid( 1 ); + project.version = versionStr.toInt(); + } - result << project; + QDateTime updated = QDateTime::fromString( proj.value( QStringLiteral( "updated" ) ).toString(), Qt::ISODateWithMs ).toUTC(); + if ( !updated.isValid() ) + { + project.serverUpdated = QDateTime::fromString( proj.value( QStringLiteral( "created" ) ).toString(), Qt::ISODateWithMs ).toUTC(); } - return result; + else + { + project.serverUpdated = updated; + } + return project; } -MerginProjectList MerginApi::parseListProjectsMetadata( const QByteArray &data ) + +MerginProjectList MerginApi::parseProjectsFromJson( const QJsonDocument &doc ) { + if ( !doc.isObject() ) + return MerginProjectList(); + + QJsonObject object = doc.object(); MerginProjectList result; - QJsonDocument doc = QJsonDocument::fromJson( data ); - if ( doc.isArray() ) + if ( object.contains( "projects" ) && object.value( "projects" ).isArray() ) // listProjects API { - QJsonArray vArray = doc.array(); + QJsonArray vArray = object.value( "projects" ).toArray(); - result = parseProjectJsonArray( vArray ); + for ( auto it = vArray.constBegin(); it != vArray.constEnd(); ++it ) + { + result << parseProjectMetadata( it->toObject() ); + } + } + else if ( !object.isEmpty() ) // listProjectsbyName API returns projects as separate objects not in array + { + for ( auto it = object.begin(); it != object.end(); ++it ) + { + result << parseProjectMetadata( it->toObject() ); + } } return result; } diff --git a/app/merginapi.h b/app/merginapi.h index f86fad215..d9148db39 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -166,7 +166,7 @@ struct TransactionStatus }; -struct MerginProjectListEntry +struct MerginProjectListEntry // TODO: replace with RemoteProject from Project_future.h { bool isValid() const { return !projectName.isEmpty() && !projectNamespace.isEmpty(); } @@ -218,9 +218,17 @@ class MerginApi: public QObject * \param flag If defined, it is used to filter out projects tagged as 'created' or 'shared' with a authorized user * \param filterTag Name of tag that fetched projects have to have. * \param page Requested page of projects. + * \returns unique id of a request */ - Q_INVOKABLE void listProjects( const QString &searchExpression = QStringLiteral(), - const QString &flag = QStringLiteral(), const QString &filterTag = QStringLiteral(), const int page = 1 ); + Q_INVOKABLE QString listProjects( const QString &searchExpression = QStringLiteral(), + const QString &flag = QStringLiteral(), const QString &filterTag = QStringLiteral(), const int page = 1 ); + + /** + * + * \param projectnames + * \returns unique id of a request + */ + Q_INVOKABLE QString listProjectsByName( const QStringList &projectNames = QStringList() ); /** * Sends non-blocking POST request to the server to download/update a project with a given name. On downloadProjectReplyFinished, @@ -374,7 +382,7 @@ class MerginApi: public QObject QString merginUserName() const; //! Disk usage of current logged in user in Mergin instance in Bytes - int diskUsage() const; + int diskUsage() const; // TODO: remove (no use) //! Total storage limit of current logged in user in Mergin instance in Bytes int storageLimit() const; @@ -424,7 +432,8 @@ class MerginApi: public QObject void projectDetached(); private slots: - void listProjectsReplyFinished(); + void listProjectsReplyFinished( QString requestId ); + void listProjectsByNameReplyFinished( QString requestId ); // Pull slots void updateInfoReplyFinished(); @@ -446,8 +455,8 @@ class MerginApi: public QObject void pingMerginReplyFinished(); private: - MerginProjectList parseListProjectsMetadata( const QByteArray &data ); - MerginProjectList parseProjectJsonArray( const QJsonArray &vArray ); + MerginProjectListEntry parseProjectMetadata( const QJsonObject &project ); + MerginProjectList parseProjectsFromJson( const QJsonDocument &object ); static QStringList generateChunkIdsForSize( qint64 fileSize ); QJsonArray prepareUploadChangesJSON( const QList &files ); static QString getApiKey( const QString &serverName ); From 3e95dee3db8744b54d99b1d7742aa1f483e90866 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Fri, 19 Feb 2021 12:37:04 +0100 Subject: [PATCH 04/53] address comments --- app/merginapi.cpp | 9 +++------ app/merginapi.h | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/merginapi.cpp b/app/merginapi.cpp index a95b2d68d..78f6194d9 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -1261,9 +1261,7 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) r->deleteLater(); - Q_UNUSED( requestId ) - //TODO: add requestId to signal - emit listProjectsFinished( mRemoteProjects, mTransactionalStatus, projectCount, requestedPage ); + emit listProjectsFinished( mRemoteProjects, mTransactionalStatus, projectCount, requestedPage, requestId ); } void MerginApi::listProjectsByNameReplyFinished( QString requestId ) @@ -1272,10 +1270,10 @@ void MerginApi::listProjectsByNameReplyFinished( QString requestId ) Q_ASSERT( r ); /* TODO: Detect orphaned project? Project that was considered Mergin but did not get info back */ + MerginProjectList projectList; if ( r->error() == QNetworkReply::NoError ) { - MerginProjectList projectList; QByteArray data = r->readAll(); QJsonDocument json = QJsonDocument::fromJson( data ); @@ -1298,8 +1296,7 @@ void MerginApi::listProjectsByNameReplyFinished( QString requestId ) r->deleteLater(); - Q_UNUSED( requestId ) - // TODO: emit signal with projects and requestId + emit listProjectsByNameFinished( projectList, mTransactionalStatus, requestId ); } diff --git a/app/merginapi.h b/app/merginapi.h index d9148db39..ce31e6a81 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -177,7 +177,7 @@ struct MerginProjectListEntry // TODO: replace with RemoteProject from Project_f }; -typedef QList MerginProjectList; +typedef QList MerginProjectList; // TODO: replace with RemoteProject from Project_future.h typedef QHash Transactions; @@ -224,9 +224,13 @@ class MerginApi: public QObject const QString &flag = QStringLiteral(), const QString &filterTag = QStringLiteral(), const int page = 1 ); /** + * Sends non-blocking GET request to the server to listProjectsByName API. Response is handled in listProjectsByNameFinished + * method. Projects are parsed from response JSON. * - * \param projectnames - * \returns unique id of a request + * TODO: add info when error codes will be parsed + * + * \param projectNames QStringList of project full names (namespace/name) + * \returns unique id of a sent request */ Q_INVOKABLE QString listProjectsByName( const QStringList &projectNames = QStringList() ); @@ -379,13 +383,13 @@ class MerginApi: public QObject QString apiRoot() const; void setApiRoot( const QString &apiRoot ); - QString merginUserName() const; + QString merginUserName() const; // TODO: remove (use can be replaced with userInfo->username) //! Disk usage of current logged in user in Mergin instance in Bytes int diskUsage() const; // TODO: remove (no use) //! Total storage limit of current logged in user in Mergin instance in Bytes - int storageLimit() const; + int storageLimit() const; // TODO: remove (no use) MerginApiStatus::VersionStatus apiVersionStatus() const; void setApiVersionStatus( const MerginApiStatus::VersionStatus &apiVersionStatus ); @@ -400,8 +404,9 @@ class MerginApi: public QObject signals: void apiSupportsSubscriptionsChanged(); - void listProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page ); + void listProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ); void listProjectsFailed(); + void listProjectsByNameFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, QString requestId ); void syncProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); /** * Emitted when sync starts/finishes or the progress changes - useful to give a clue in the GUI about the status. @@ -553,7 +558,7 @@ class MerginApi: public QObject QNetworkAccessManager mManager; QString mApiRoot; LocalProjectsManager &mLocalProjects; - MerginProjectList mRemoteProjects; + MerginProjectList mRemoteProjects; // TODO: remove (no use, only in tests - TBD) QString mDataDir; // dir with all projects MerginUserInfo *mUserInfo; //owned by this (qml grouped-properties) From 2dd80244943e537410b13060024f7574d83ccfd9 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Tue, 9 Mar 2021 15:32:13 +0100 Subject: [PATCH 05/53] merge projects onListProjectsFinished --- app/localprojectsmanager.h | 12 +- app/merginapi.cpp | 6 +- app/merginapi.h | 9 ++ app/merginprojectmodel.cpp | 4 +- app/models/projectsmodel_future.cpp | 175 +++++++++++++++++++++-- app/models/projectsmodel_future.h | 23 +-- app/models/projectsproxymodel_future.cpp | 9 ++ app/models/projectsproxymodel_future.h | 9 ++ app/project_future.cpp | 22 ++- app/project_future.h | 67 ++++++--- app/projectsmodel.cpp | 2 +- app/projectsmodel.h | 2 +- app/sources.pri | 3 - 13 files changed, 292 insertions(+), 51 deletions(-) diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index 9c1e9678a..dfb84a674 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -11,7 +11,7 @@ #define LOCALPROJECTSMANAGER_H #include - +#include enum ProjectStatus { @@ -50,6 +50,9 @@ struct LocalProjectInfo ProjectStatus status = NoVersion; + bool operator ==( const LocalProjectInfo &other ) { return ( projectName == other.projectName && projectNamespace == other.projectNamespace );} + bool operator !=( const LocalProjectInfo &other ) { return !( *this == other ); } + // Sync status (e.g. progress) is not kept here because if a project does not exist locally yet // and it is only being downloaded for the first time, it's not in the list of local projects either // and we would need to do some workarounds for that. @@ -114,7 +117,7 @@ class LocalProjectsManager : public QObject QString findQgisProjectFile( const QString &projectDir, QString &err ); - static ProjectStatus currentProjectStatus( const LocalProjectInfo &project ); + static ProjectStatus currentProjectStatus( const LocalProjectInfo &project ); // TODO: local project manager should not have status signals: void projectMetadataChanged( const QString &projectDir ); @@ -123,13 +126,16 @@ class LocalProjectsManager : public QObject void localProjectRemoved( const QString &projectDir ); private: - void updateProjectStatus( LocalProjectInfo &project ); + void updateProjectStatus( LocalProjectInfo &project ); // TODO: local project manager should not have status + //! Should add an entry about newly created project. Emits no signals void addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); private: QString mDataDir; //!< directory with all local projects QList mProjects; + + QList mLocalProjects_future; }; diff --git a/app/merginapi.cpp b/app/merginapi.cpp index 78f6194d9..12dd1bacb 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -30,7 +30,8 @@ #include const QString MerginApi::sMetadataFile = QStringLiteral( "/.mergin/mergin.json" ); -const QString MerginApi::sDefaultApiRoot = QStringLiteral( "https://public.cloudmergin.com/" ); +//const QString MerginApi::sDefaultApiRoot = QStringLiteral( "https://public.cloudmergin.com/" ); +const QString MerginApi::sDefaultApiRoot = QStringLiteral( "https://dev.dev.cloudmergin.com/" ); const QSet MerginApi::sIgnoreExtensions = QSet() << "gpkg-shm" << "gpkg-wal" << "qgs~" << "qgz~" << "pyc" << "swap"; const QSet MerginApi::sIgnoreFiles = QSet() << "mergin.json" << ".DS_Store"; const int MerginApi::UPLOAD_CHUNK_SIZE = 10 * 1024 * 1024; // Should be the same as on Mergin server @@ -106,6 +107,7 @@ QString MerginApi::listProjects( const QString &searchExpression, const QString InputUtils::log( "list projects", QStringLiteral( "Requesting: " ) + url.toString() ); connect( reply, &QNetworkReply::finished, this, [this, requestId]() {this->listProjectsReplyFinished( requestId );} ); + qDebug() << "MerginAPI: ListProjects, returning requestId: " << requestId; return requestId; } @@ -1085,7 +1087,7 @@ QString MerginApi::generateConflictFileName( const QString &path, int version ) return QString( "%1_conflict_%2_v%3" ).arg( path, mUserAuth->username(), QString::number( version ) ); } -QString MerginApi::getFullProjectName( QString projectNamespace, QString projectName ) +QString MerginApi::getFullProjectName( QString projectNamespace, QString projectName ) // TODO: move to inpututils? { return QString( "%1/%2" ).arg( projectNamespace ).arg( projectName ); } diff --git a/app/merginapi.h b/app/merginapi.h index ce31e6a81..761920d86 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -175,6 +175,15 @@ struct MerginProjectListEntry // TODO: replace with RemoteProject from Project_f int version = -1; QDateTime serverUpdated; // available latest version of project files on server + bool operator ==( const MerginProjectListEntry &other ) + { + return ( this->projectName == other.projectName ) && ( this->projectNamespace == other.projectNamespace ); + } + + bool operator !=( const MerginProjectListEntry &other ) + { + return !( *this == other ); + } }; typedef QList MerginProjectList; // TODO: replace with RemoteProject from Project_future.h diff --git a/app/merginprojectmodel.cpp b/app/merginprojectmodel.cpp index 75e6648a4..d7109cb52 100644 --- a/app/merginprojectmodel.cpp +++ b/app/merginprojectmodel.cpp @@ -103,7 +103,7 @@ int MerginProjectModel::rowCount( const QModelIndex &parent ) const void MerginProjectModel::updateModel( const MerginProjectList &merginProjects, QHash pendingProjects, int expectedProjectCount, int page ) { beginResetModel(); - mMerginProjects.removeOne( mAdditionalItem ); + mMerginProjects.removeOne( mAdditionalItem ); // TODO: remove if ( page == 1 ) { @@ -140,7 +140,7 @@ void MerginProjectModel::updateModel( const MerginProjectList &merginProjects, Q mMerginProjects << project; } - if ( mMerginProjects.count() < expectedProjectCount ) + if ( mMerginProjects.count() < expectedProjectCount ) // TODO: remove { mMerginProjects << mAdditionalItem; } diff --git a/app/models/projectsmodel_future.cpp b/app/models/projectsmodel_future.cpp index 7a99b2a88..b97eeb87d 100644 --- a/app/models/projectsmodel_future.cpp +++ b/app/models/projectsmodel_future.cpp @@ -1,5 +1,14 @@ -#include "projectsmodel_future.h" +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#include "projectsmodel_future.h" +#include "localprojectsmanager.h" ProjectsModel_future::ProjectsModel_future( MerginApi *merginApi, @@ -11,7 +20,47 @@ ProjectsModel_future::ProjectsModel_future( mLocalProjectsManager( localProjectsManager ), mModelType( modelType ) { - // TODO: connect to signals from LocalProjectsManager and MerginAPI + QObject::connect( mBackend, &MerginApi::syncProjectStatusChanged, this, &ProjectsModel_future::onProjectSyncProgressChanged ); + QObject::connect( mBackend, &MerginApi::syncProjectFinished, this, &ProjectsModel_future::onProjectSyncFinished ); + + // TODO: connect to signals from LocalProjectsManager +// QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectAdded ) + + if ( mModelType == ProjectModelTypes::LocalProjectsModel ) + { + QObject::connect( mBackend, &MerginApi::listProjectsByNameFinished, this, &ProjectsModel_future::onListProjectsByNameFinished ); + } + else if ( mModelType != ProjectModelTypes::RecentProjectsModel ) + { + QObject::connect( mBackend, &MerginApi::listProjectsFinished, this, &ProjectsModel_future::onListProjectsFinished ); + } + else + { + // TODO: implement RecentProjectsModel type + } +} + +QVariant ProjectsModel_future::data( const QModelIndex &, int ) const +{ + return QVariant( "Test data" ); +} + +QModelIndex ProjectsModel_future::index( int, int, const QModelIndex & ) const +{ + return QModelIndex(); +} + +QHash ProjectsModel_future::roleNames() const +{ + QHash roles; + roles[Roles::Project] = QStringLiteral( "project" ).toLatin1(); + + return roles; +} + +int ProjectsModel_future::rowCount( const QModelIndex & ) const +{ + return mProjects.count(); } void ProjectsModel_future::listProjects() @@ -19,29 +68,139 @@ void ProjectsModel_future::listProjects() mLastRequestId = mBackend->listProjects( "", modelTypeToFlag(), "", mPopulatedPage ); } -void ProjectsModel_future::mergeProjects() +void ProjectsModel_future::mergeProjects( const MerginProjectList &merginProjects, Transactions pendingProjects ) { QList localProjects = mLocalProjectsManager.projects(); + + qDebug() << "ProjectsModel_future: mergeProjects(): # of local projects = " << localProjects.size(); + mProjects.clear(); + + if ( mModelType == ProjectModelTypes::LocalProjectsModel ) + { + // Keep all local projects and ignore all not downloaded remote projects + for ( auto &localProject : localProjects ) + { + std::shared_ptr project = std::shared_ptr( new Project_future() ); + + std::unique_ptr local = std::unique_ptr( new LocalProject_future() ); + + project->local = std::move( local ); + + project->local->projectName = localProject.projectName; + project->local->projectNamespace = localProject.projectNamespace; + project->local->projectDir = localProject.projectDir; + // TODO: later copy data by copy constructor + + MerginProjectListEntry remoteEntry; + remoteEntry.projectName = project->local->projectName; + remoteEntry.projectNamespace = project->local->projectNamespace; + + if ( merginProjects.contains( remoteEntry ) ) + { + int i = merginProjects.indexOf( remoteEntry ); + std::unique_ptr mergin = std::unique_ptr( new MerginProject_future() ); + mergin->projectName = merginProjects[i].projectName; + mergin->projectNamespace = merginProjects[i].projectNamespace; + // TODO: later copy data by copy constructor + // TODO: check for project errors (from ListByName API ~> not authorized / no rights / no version) + + if ( pendingProjects.contains( mergin->projectIdentifier() ) ) + { + TransactionStatus projectTransaction = pendingProjects.value( mergin->projectIdentifier() ); + mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; + mergin->pending = true; + } + + project->mergin = std::move( mergin ); + } + + mProjects << project; + } + } + else if ( mModelType != ProjectModelTypes::RecentProjectsModel ) + { + // Keep all remote projects and ignore all non mergin projects from local projects + for ( const auto &remoteEntry: merginProjects ) + { + std::shared_ptr project = std::shared_ptr( new Project_future() ); + std::unique_ptr mergin = std::unique_ptr( new MerginProject_future() ); + + mergin->projectName = remoteEntry.projectName; + mergin->projectNamespace = remoteEntry.projectNamespace; + // TODO: later copy data by copy constructor + + if ( pendingProjects.contains( mergin->projectIdentifier() ) ) + { + TransactionStatus projectTransaction = pendingProjects.value( mergin->projectIdentifier() ); + mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; + mergin->pending = true; + } + + project->mergin = std::move( mergin ); + + // find downloaded projects + LocalProjectInfo localProject; + localProject.projectName = project->mergin->projectName; + localProject.projectNamespace = project->mergin->projectNamespace; + + if ( localProjects.contains( localProject ) ) + { + int ix = localProjects.indexOf( localProject ); + project->local = std::unique_ptr( new LocalProject_future() ); + + project->local->projectName = localProjects[ix].projectName; + project->local->projectNamespace = localProjects[ix].projectNamespace; + // TODO: later copy data by copy constructor + } + + mProjects << project; + } + } } -void ProjectsModel_future::listProjectsFinished() +void ProjectsModel_future::onListProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ) { - mergeProjects(); + qDebug() << "ProjectsModel_future: onListProjectsFinished(): received response with requestId = " << requestId; + if ( mLastRequestId != requestId ) + { + qDebug() << "ProjectsModel_future: onListProjectsFinished(): should ignore request with id " << requestId << ", disabled for a while"; +// return; + } + Q_UNUSED( pendingProjects ); + Q_UNUSED( projectCount ); + Q_UNUSED( page ); + + qDebug() << "ProjectsModel_future: onListProjectsFinished(): project count = " << projectCount << " but mergin projects emited: " << merginProjects.size(); + + mergeProjects( merginProjects, pendingProjects ); +} + +void ProjectsModel_future::onListProjectsByNameFinished() +{ + } -void ProjectsModel_future::projectSyncFinished() +void ProjectsModel_future::onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully ) { + Q_UNUSED( projectDir ) + Q_UNUSED( projectFullName ) + Q_UNUSED( successfully ) + qDebug() << "PMR: Project " << projectFullName << " finished sync"; } -void ProjectsModel_future::projectSyncProgressChanged() +void ProjectsModel_future::onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ) { + Q_UNUSED( projectFullName ) + Q_UNUSED( progress ) + qDebug() << "PMR: Project " << projectFullName << " changed sync progress to " << progress; } QString ProjectsModel_future::modelTypeToFlag() const { - switch ( mModelType ) { + switch ( mModelType ) + { case MyProjectsModel: return QStringLiteral( "created" ); case SharedProjectsModel: diff --git a/app/models/projectsmodel_future.h b/app/models/projectsmodel_future.h index 57f47cb44..f1579b575 100644 --- a/app/models/projectsmodel_future.h +++ b/app/models/projectsmodel_future.h @@ -13,10 +13,11 @@ #include #include -#include "localprojectsmanager.h" #include "project_future.h" #include "merginapi.h" +class LocalProjectsManager; + /** * \brief The ProjectModelTypes enum */ @@ -40,6 +41,7 @@ class ProjectsModel_future : public QAbstractListModel enum Roles { + // TODO: rewrite to individual roles Project = Qt::UserRole + 1 }; Q_ENUMS( Roles ) @@ -48,21 +50,22 @@ class ProjectsModel_future : public QAbstractListModel ~ProjectsModel_future() override {}; // Needed methods from QAbstractListModel -// Q_INVOKABLE QVariant data( const QModelIndex &index, int role ) const override; -// Q_INVOKABLE QModelIndex index( int row, int column = 0, const QModelIndex &parent = QModelIndex() ) const override; -// QHash roleNames() const override; -// int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + Q_INVOKABLE QVariant data( const QModelIndex &index, int role ) const override; + Q_INVOKABLE QModelIndex index( int row, int column = 0, const QModelIndex &parent = QModelIndex() ) const override; + QHash roleNames() const override; + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; //! Called to list projects, either fetch more or get first Q_INVOKABLE void listProjects(); //! Method detecting local project for remote projects - void mergeProjects(); + void mergeProjects( const MerginProjectList &merginProjects, Transactions pendingProjects ); public slots: - void listProjectsFinished(); - void projectSyncFinished(); - void projectSyncProgressChanged(); + void onListProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ); + void onListProjectsByNameFinished(); + void onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); + void onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ); private: @@ -70,7 +73,7 @@ class ProjectsModel_future : public QAbstractListModel MerginApi *mBackend; LocalProjectsManager &mLocalProjectsManager; - QList mProjects; + QList> mProjects; ProjectModelTypes mModelType; diff --git a/app/models/projectsproxymodel_future.cpp b/app/models/projectsproxymodel_future.cpp index daba71fe4..fcbab74b3 100644 --- a/app/models/projectsproxymodel_future.cpp +++ b/app/models/projectsproxymodel_future.cpp @@ -1,3 +1,12 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + #include "projectsproxymodel_future.h" ProjectsProxyModel_future::ProjectsProxyModel_future( ProjectModelTypes modelType, QObject *parent ) : diff --git a/app/models/projectsproxymodel_future.h b/app/models/projectsproxymodel_future.h index 7f844ca09..db02b5fae 100644 --- a/app/models/projectsproxymodel_future.h +++ b/app/models/projectsproxymodel_future.h @@ -1,3 +1,12 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + #ifndef PROJECTSPROXYMODEL_FUTURE_H #define PROJECTSPROXYMODEL_FUTURE_H diff --git a/app/project_future.cpp b/app/project_future.cpp index 7d2370334..35b35370d 100644 --- a/app/project_future.cpp +++ b/app/project_future.cpp @@ -1,6 +1,24 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + #include "project_future.h" -Project_future::Project_future( QObject *parent ) : QObject( parent ) -{ +#include "merginapi.h" +void LocalProject_future::copyValues( const LocalProject_future &other ) +{ + projectName = other.projectName; + projectNamespace = other.projectNamespace; + projectDir = other.projectDir; + projectError = other.projectError; + qgisProjectFilePath = other.qgisProjectFilePath; + localVersion = other.localVersion; } + +QString MerginProject_future::projectIdentifier() { return MerginApi::getFullProjectName( projectNamespace, projectName ); } diff --git a/app/project_future.h b/app/project_future.h index 511150362..8f302ae15 100644 --- a/app/project_future.h +++ b/app/project_future.h @@ -1,8 +1,19 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + #ifndef PROJECT_FUTURE_H #define PROJECT_FUTURE_H #include +#include #include +#include enum ProjectStatus_future { @@ -16,41 +27,59 @@ enum ProjectStatus_future }; Q_ENUMS( ProjectStatus_future ) -struct RemoteProject +struct LocalProject_future { + LocalProject_future() { qDebug() << "Building LocalProject_future " << this; } + ~LocalProject_future() { qDebug() << "Removing LocalProject_future " << this; } + QString projectName; QString projectNamespace; - QString projectIdentifier() { return QString(); }; + QString projectIdentifier() { return QString(); } - QDateTime serverUpdated; // available latest version of project files on server // TODO: maybe we do not need this at all + QString projectDir; + QString projectError; - bool pending = false; - ProjectStatus_future status = ProjectStatus_future::_NoVersion; - qreal progress = 0; -}; + QString qgisProjectFilePath; + + int localVersion = -1; + void copyValues( const LocalProject_future &other ); +}; -struct MerginProject_deprecated +struct MerginProject_future { + MerginProject_future() { qDebug() << "Building MerginProject_future " << this; } + ~MerginProject_future() { qDebug() << "Removing MerginProject_future " << this; } + QString projectName; QString projectNamespace; - QString projectDir; // full path to the project directory - QDateTime clientUpdated; // client's version of project files - QDateTime serverUpdated; // available latest version of project files on server - bool pending = false; // if there is a pending request for downlaod/update a project + + QString projectIdentifier(); + + QDateTime serverUpdated; // available latest version of project files on server // TODO: maybe we do not need this at all - only as description + int serverVersion; + ProjectStatus_future status = ProjectStatus_future::_NoVersion; - qreal progress = 0; // progress in case of pending download/upload (values [0..1]) -}; + bool pending = false; + qreal progress = 0; + // TODO: Add error code +}; -class Project_future : public QObject +struct Project_future { - Q_OBJECT - public: - explicit Project_future( QObject *parent = nullptr ); - ~Project_future() override {} + Project_future() { qDebug() << "Building Project_future " << this; } + ~Project_future() { qDebug() << "Removing Project_future " << this; } + + std::unique_ptr mergin; + std::unique_ptr local; + + bool isMergin() { return mergin != nullptr; } + bool isLocal() { return local != nullptr; } + //! Attributes that should be there no matter the project type + QString projectInfo; }; #endif // PROJECT_FUTURE_H diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index a1ea49ef3..bc2a2d17c 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -187,7 +187,7 @@ void ProjectModel::syncedProjectFinished( const QString &projectDir, const QStri reloadProjectFiles( projectDir, projectFullName, successfully ); } -void ProjectModel::addLocalProject( const QString &projectDir ) +void ProjectModel::addLocalProject( const QString &projectDir ) // TODO: not needed if localProjectsManager emits added signal { Q_UNUSED( projectDir ); mLocalProjects.reloadProjectDir(); diff --git a/app/projectsmodel.h b/app/projectsmodel.h index 166319e92..6a2905c7b 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -82,7 +82,7 @@ class ProjectModel : public QAbstractListModel QString projectNamespace; //!< mergin project namespace (first part of "namespace/project"). empty for non-mergin project QString folderName; //!< name of the project folder (not the full path) QString path; //!< path to the .qgs/.qgz project file - QString info; + QString info; // Description! bool isValid; /** diff --git a/app/sources.pri b/app/sources.pri index 14560a67a..f200c6a94 100644 --- a/app/sources.pri +++ b/app/sources.pri @@ -77,14 +77,11 @@ variablesmanager.h \ ios/iosimagepicker.h \ ios/iosutils.h \ inputprojutils.h \ -<<<<<<< HEAD codefilter.h \ qrdecoder.h \ -======= models/projectsproxymodel_future.h \ models/projectsmodel_future.h \ project_future.h \ ->>>>>>> add future classes contains(DEFINES, INPUT_TEST) { From c0ede0127864bc3690fb345507aa6892eb7ba367 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Tue, 16 Mar 2021 09:54:19 +0100 Subject: [PATCH 06/53] polis list projects flow --- app/localprojectsmanager.cpp | 3 +- app/models/projectsmodel_future.cpp | 98 ++++++++++++++++++++++++----- app/models/projectsmodel_future.h | 7 ++- app/project_future.cpp | 13 +++- app/project_future.h | 24 ++++--- 5 files changed, 113 insertions(+), 32 deletions(-) diff --git a/app/localprojectsmanager.cpp b/app/localprojectsmanager.cpp index a9beebb93..2a70fa1a9 100644 --- a/app/localprojectsmanager.cpp +++ b/app/localprojectsmanager.cpp @@ -22,7 +22,7 @@ LocalProjectsManager::LocalProjectsManager( const QString &dataDir ) reloadProjectDir(); } -void LocalProjectsManager::reloadProjectDir() +void LocalProjectsManager::reloadProjectDir() // TODO: maybe add function to reload one specific project { mProjects.clear(); QStringList entryList = QDir( mDataDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); @@ -46,6 +46,7 @@ void LocalProjectsManager::reloadProjectDir() mProjects << info; } + qDebug() << "LocalProjectsManager: found" << mProjects.size() << "projects"; } diff --git a/app/models/projectsmodel_future.cpp b/app/models/projectsmodel_future.cpp index b97eeb87d..0168f391b 100644 --- a/app/models/projectsmodel_future.cpp +++ b/app/models/projectsmodel_future.cpp @@ -9,6 +9,7 @@ #include "projectsmodel_future.h" #include "localprojectsmanager.h" +#include "inpututils.h" ProjectsModel_future::ProjectsModel_future( MerginApi *merginApi, @@ -36,7 +37,7 @@ ProjectsModel_future::ProjectsModel_future( } else { - // TODO: implement RecentProjectsModel type + // Implement RecentProjectsModel type } } @@ -65,14 +66,31 @@ int ProjectsModel_future::rowCount( const QModelIndex & ) const void ProjectsModel_future::listProjects() { - mLastRequestId = mBackend->listProjects( "", modelTypeToFlag(), "", mPopulatedPage ); + if ( mModelType == LocalProjectsModel ) + { + InputUtils::log( "Input", "Can not call listProjects API on LocalProjectsModel" ); + return; + } + + mLastRequestId = mBackend->listProjects( "", modelTypeToFlag(), "", 1 ); //TODO: pagination +} + +void ProjectsModel_future::listProjectsByName() +{ + if ( mModelType != LocalProjectsModel ) + { + InputUtils::log( "Input", "Can not call listProjectsByName API on not LocalProjectsModel" ); + return; + } + + mLastRequestId = mBackend->listProjectsByName( projectNames() ); } void ProjectsModel_future::mergeProjects( const MerginProjectList &merginProjects, Transactions pendingProjects ) { QList localProjects = mLocalProjectsManager.projects(); - qDebug() << "ProjectsModel_future: mergeProjects(): # of local projects = " << localProjects.size(); + qDebug() << "PMR: mergeProjects(): # of local projects = " << localProjects.size() << " # of mergin projects = " << merginProjects.size(); mProjects.clear(); if ( mModelType == ProjectModelTypes::LocalProjectsModel ) @@ -81,15 +99,13 @@ void ProjectsModel_future::mergeProjects( const MerginProjectList &merginProject for ( auto &localProject : localProjects ) { std::shared_ptr project = std::shared_ptr( new Project_future() ); - std::unique_ptr local = std::unique_ptr( new LocalProject_future() ); - project->local = std::move( local ); - - project->local->projectName = localProject.projectName; - project->local->projectNamespace = localProject.projectNamespace; - project->local->projectDir = localProject.projectDir; + local->projectName = localProject.projectName; + local->projectNamespace = localProject.projectNamespace; + local->projectDir = localProject.projectDir; // TODO: later copy data by copy constructor + project->local = std::move( local ); MerginProjectListEntry remoteEntry; remoteEntry.projectName = project->local->projectName; @@ -120,7 +136,7 @@ void ProjectsModel_future::mergeProjects( const MerginProjectList &merginProject else if ( mModelType != ProjectModelTypes::RecentProjectsModel ) { // Keep all remote projects and ignore all non mergin projects from local projects - for ( const auto &remoteEntry: merginProjects ) + for ( const auto &remoteEntry : merginProjects ) { std::shared_ptr project = std::shared_ptr( new Project_future() ); std::unique_ptr mergin = std::unique_ptr( new MerginProject_future() ); @@ -160,24 +176,41 @@ void ProjectsModel_future::mergeProjects( const MerginProjectList &merginProject void ProjectsModel_future::onListProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ) { - qDebug() << "ProjectsModel_future: onListProjectsFinished(): received response with requestId = " << requestId; + qDebug() << "PMR: onListProjectsFinished(): received response with requestId = " << requestId; if ( mLastRequestId != requestId ) { - qDebug() << "ProjectsModel_future: onListProjectsFinished(): should ignore request with id " << requestId << ", disabled for a while"; -// return; + qDebug() << "PMR: onListProjectsFinished(): ignoring request with id " << requestId; + return; } - Q_UNUSED( pendingProjects ); + Q_UNUSED( projectCount ); Q_UNUSED( page ); - qDebug() << "ProjectsModel_future: onListProjectsFinished(): project count = " << projectCount << " but mergin projects emited: " << merginProjects.size(); + qDebug() << "PMR: onListProjectsFinished(): project count = " << projectCount << " but mergin projects emited: " << merginProjects.size(); + beginResetModel(); mergeProjects( merginProjects, pendingProjects ); + printProjects(); + endResetModel(); } -void ProjectsModel_future::onListProjectsByNameFinished() +void ProjectsModel_future::onListProjectsByNameFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, QString requestId ) { + qDebug() << "PMR: onListProjectsByNameFinished(): received response with requestId = " << requestId; + if ( mLastRequestId != requestId ) + { + qDebug() << "PMR: onListProjectsByNameFinished(): ignoring request with id " << requestId; + return; + } + + Q_UNUSED( merginProjects ); + Q_UNUSED( pendingProjects ); + Q_UNUSED( requestId ); + beginResetModel(); + mergeProjects( merginProjects, pendingProjects ); + printProjects(); + endResetModel(); } void ProjectsModel_future::onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully ) @@ -209,3 +242,36 @@ QString ProjectsModel_future::modelTypeToFlag() const return QStringLiteral( "" ); } } + +void ProjectsModel_future::printProjects() const // TODO: Helper function, remove after refactoring is done +{ + qDebug() << "Model " << this << " with type " << modelTypeToFlag() << " has projects: "; + for ( const auto &proj : mProjects ) + { + QString lcl = proj->isLocal() ? "local" : ""; + QString mrgn = proj->isMergin() ? "mergin" : ""; + + if ( proj->isLocal() ) + { + qDebug() << " - " << proj->local->projectNamespace << proj->local->projectName << lcl << mrgn; + } + else if ( proj->isMergin() ) + { + qDebug() << " - " << proj->mergin->projectNamespace << proj->mergin->projectName << lcl << mrgn; + } + } +} + +QStringList ProjectsModel_future::projectNames() const // TODO: use local projects instead +{ + QStringList projectNames; + QList projects = mLocalProjectsManager.projects(); + + for ( const auto &proj : projects ) + { + if ( !proj.projectName.isEmpty() && !proj.projectNamespace.isEmpty() ) + projectNames << MerginApi::getFullProjectName( proj.projectNamespace, proj.projectName ); + } + + return projectNames; +} diff --git a/app/models/projectsmodel_future.h b/app/models/projectsmodel_future.h index f1579b575..066b6089b 100644 --- a/app/models/projectsmodel_future.h +++ b/app/models/projectsmodel_future.h @@ -58,18 +58,23 @@ class ProjectsModel_future : public QAbstractListModel //! Called to list projects, either fetch more or get first Q_INVOKABLE void listProjects(); + //! Called to list projects, either fetch more or get first + Q_INVOKABLE void listProjectsByName(); + //! Method detecting local project for remote projects void mergeProjects( const MerginProjectList &merginProjects, Transactions pendingProjects ); public slots: void onListProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ); - void onListProjectsByNameFinished(); + void onListProjectsByNameFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, QString requestId ); void onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); void onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ); private: QString modelTypeToFlag() const; + void printProjects() const; + QStringList projectNames() const; MerginApi *mBackend; LocalProjectsManager &mLocalProjectsManager; diff --git a/app/project_future.cpp b/app/project_future.cpp index 35b35370d..8b6be5f46 100644 --- a/app/project_future.cpp +++ b/app/project_future.cpp @@ -21,4 +21,15 @@ void LocalProject_future::copyValues( const LocalProject_future &other ) localVersion = other.localVersion; } -QString MerginProject_future::projectIdentifier() { return MerginApi::getFullProjectName( projectNamespace, projectName ); } +QString MerginProject_future::projectIdentifier() +{ + return MerginApi::getFullProjectName( projectNamespace, projectName ); +} + +QString LocalProject_future::projectIdentifier() +{ + if ( !projectName.isEmpty() && !projectNamespace.isEmpty() ) + return MerginApi::getFullProjectName( projectNamespace, projectName ); + + return projectDir; +} diff --git a/app/project_future.h b/app/project_future.h index 8f302ae15..6df937e90 100644 --- a/app/project_future.h +++ b/app/project_future.h @@ -21,24 +21,22 @@ enum ProjectStatus_future _UpToDate, //!< both server and local copy are in sync with no extra modifications _OutOfDate, //!< server has newer version than what is available locally (but the project is not modified locally) _Modified, //!< there are some local modifications in the project that need to be pushed (note: also server may have newer version) - _NonProjectItem //!< only for mock projects, acts like a hook to enable extra functionality for models working with projects . - // TODO: replace _NonProjectItem with footer property in ListView - // TODO2: add orphaned state? + // TODO: add orphaned state }; Q_ENUMS( ProjectStatus_future ) struct LocalProject_future { - LocalProject_future() { qDebug() << "Building LocalProject_future " << this; } - ~LocalProject_future() { qDebug() << "Removing LocalProject_future " << this; } + LocalProject_future() {}; /*{ qDebug() << "Building LocalProject_future " << this; }*/ + ~LocalProject_future() {}; /*{ qDebug() << "Removing LocalProject_future " << this; }*/ QString projectName; QString projectNamespace; - QString projectIdentifier() { return QString(); } + QString projectIdentifier(); QString projectDir; - QString projectError; + QString projectError; // Error that leads to project not being able to open in app QString qgisProjectFilePath; @@ -49,8 +47,8 @@ struct LocalProject_future struct MerginProject_future { - MerginProject_future() { qDebug() << "Building MerginProject_future " << this; } - ~MerginProject_future() { qDebug() << "Removing MerginProject_future " << this; } + MerginProject_future() {}; /*{ qDebug() << "Building MerginProject_future " << this; }*/ + ~MerginProject_future() {}; /*{ qDebug() << "Removing MerginProject_future " << this; }*/ QString projectName; QString projectNamespace; @@ -64,13 +62,13 @@ struct MerginProject_future bool pending = false; qreal progress = 0; - // TODO: Add error code + QString remoteError; // Error leading to project not being able to sync }; struct Project_future { - Project_future() { qDebug() << "Building Project_future " << this; } - ~Project_future() { qDebug() << "Removing Project_future " << this; } + Project_future() {}; /*{ qDebug() << "Building Project_future " << this; }*/ + ~Project_future() {}; /*{ qDebug() << "Removing Project_future " << this; }*/ std::unique_ptr mergin; std::unique_ptr local; @@ -79,7 +77,7 @@ struct Project_future bool isLocal() { return local != nullptr; } //! Attributes that should be there no matter the project type - QString projectInfo; + QString projectDescription; // rather on the fly }; #endif // PROJECT_FUTURE_H From bfff3899ecb3854dd348ef9eaf06e387f1192b3b Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Tue, 16 Mar 2021 11:59:29 +0100 Subject: [PATCH 07/53] incorporate changes from previous pr --- app/models/projectsmodel_future.cpp | 8 ++++---- app/project_future.cpp | 4 ++-- app/project_future.h | 12 +++++------- app/qml/ProjectDelegateItem.qml | 2 +- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/models/projectsmodel_future.cpp b/app/models/projectsmodel_future.cpp index 0168f391b..9ee830905 100644 --- a/app/models/projectsmodel_future.cpp +++ b/app/models/projectsmodel_future.cpp @@ -120,9 +120,9 @@ void ProjectsModel_future::mergeProjects( const MerginProjectList &merginProject // TODO: later copy data by copy constructor // TODO: check for project errors (from ListByName API ~> not authorized / no rights / no version) - if ( pendingProjects.contains( mergin->projectIdentifier() ) ) + if ( pendingProjects.contains( mergin->id() ) ) { - TransactionStatus projectTransaction = pendingProjects.value( mergin->projectIdentifier() ); + TransactionStatus projectTransaction = pendingProjects.value( mergin->id() ); mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; mergin->pending = true; } @@ -145,9 +145,9 @@ void ProjectsModel_future::mergeProjects( const MerginProjectList &merginProject mergin->projectNamespace = remoteEntry.projectNamespace; // TODO: later copy data by copy constructor - if ( pendingProjects.contains( mergin->projectIdentifier() ) ) + if ( pendingProjects.contains( mergin->id() ) ) { - TransactionStatus projectTransaction = pendingProjects.value( mergin->projectIdentifier() ); + TransactionStatus projectTransaction = pendingProjects.value( mergin->id() ); mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; mergin->pending = true; } diff --git a/app/project_future.cpp b/app/project_future.cpp index 8b6be5f46..dcb3cabd8 100644 --- a/app/project_future.cpp +++ b/app/project_future.cpp @@ -21,12 +21,12 @@ void LocalProject_future::copyValues( const LocalProject_future &other ) localVersion = other.localVersion; } -QString MerginProject_future::projectIdentifier() +QString MerginProject_future::id() { return MerginApi::getFullProjectName( projectNamespace, projectName ); } -QString LocalProject_future::projectIdentifier() +QString LocalProject_future::id() { if ( !projectName.isEmpty() && !projectNamespace.isEmpty() ) return MerginApi::getFullProjectName( projectNamespace, projectName ); diff --git a/app/project_future.h b/app/project_future.h index 6df937e90..cef3229b6 100644 --- a/app/project_future.h +++ b/app/project_future.h @@ -33,7 +33,7 @@ struct LocalProject_future QString projectName; QString projectNamespace; - QString projectIdentifier(); + QString id(); QString projectDir; QString projectError; // Error that leads to project not being able to open in app @@ -53,15 +53,16 @@ struct MerginProject_future QString projectName; QString projectNamespace; - QString projectIdentifier(); - - QDateTime serverUpdated; // available latest version of project files on server // TODO: maybe we do not need this at all - only as description + QString id(); + QDateTime serverUpdated; // available latest version of project files on server int serverVersion; ProjectStatus_future status = ProjectStatus_future::_NoVersion; bool pending = false; qreal progress = 0; + + // Maybe better use enum or int for error code QString remoteError; // Error leading to project not being able to sync }; @@ -75,9 +76,6 @@ struct Project_future bool isMergin() { return mergin != nullptr; } bool isLocal() { return local != nullptr; } - - //! Attributes that should be there no matter the project type - QString projectDescription; // rather on the fly }; #endif // PROJECT_FUTURE_H diff --git a/app/qml/ProjectDelegateItem.qml b/app/qml/ProjectDelegateItem.qml index 6c1167743..dcfef51ef 100644 --- a/app/qml/ProjectDelegateItem.qml +++ b/app/qml/ProjectDelegateItem.qml @@ -167,7 +167,7 @@ Rectangle { } // Additional item - DelegateButton { + DelegateButton { // TODO: replace with footer property on projects listview visible: itemContainer.isAdditional width: itemContainer.width height: itemContainer.height From 086de4ea29623571a80908b6485b1d092fdc8c18 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Tue, 16 Mar 2021 12:20:23 +0100 Subject: [PATCH 08/53] remove models subfolder --- app/{models => }/projectsmodel_future.cpp | 2 ++ app/{models => }/projectsmodel_future.h | 0 app/{models => }/projectsproxymodel_future.cpp | 0 app/{models => }/projectsproxymodel_future.h | 0 app/sources.pri | 8 ++++---- 5 files changed, 6 insertions(+), 4 deletions(-) rename app/{models => }/projectsmodel_future.cpp (98%) rename app/{models => }/projectsmodel_future.h (100%) rename app/{models => }/projectsproxymodel_future.cpp (100%) rename app/{models => }/projectsproxymodel_future.h (100%) diff --git a/app/models/projectsmodel_future.cpp b/app/projectsmodel_future.cpp similarity index 98% rename from app/models/projectsmodel_future.cpp rename to app/projectsmodel_future.cpp index 9ee830905..589979c5e 100644 --- a/app/models/projectsmodel_future.cpp +++ b/app/projectsmodel_future.cpp @@ -117,6 +117,8 @@ void ProjectsModel_future::mergeProjects( const MerginProjectList &merginProject std::unique_ptr mergin = std::unique_ptr( new MerginProject_future() ); mergin->projectName = merginProjects[i].projectName; mergin->projectNamespace = merginProjects[i].projectNamespace; + mergin->serverVersion = merginProjects[i].version; + mergin->serverUpdated = merginProjects[i].serverUpdated; // TODO: later copy data by copy constructor // TODO: check for project errors (from ListByName API ~> not authorized / no rights / no version) diff --git a/app/models/projectsmodel_future.h b/app/projectsmodel_future.h similarity index 100% rename from app/models/projectsmodel_future.h rename to app/projectsmodel_future.h diff --git a/app/models/projectsproxymodel_future.cpp b/app/projectsproxymodel_future.cpp similarity index 100% rename from app/models/projectsproxymodel_future.cpp rename to app/projectsproxymodel_future.cpp diff --git a/app/models/projectsproxymodel_future.h b/app/projectsproxymodel_future.h similarity index 100% rename from app/models/projectsproxymodel_future.h rename to app/projectsproxymodel_future.h diff --git a/app/sources.pri b/app/sources.pri index f200c6a94..3b28b1786 100644 --- a/app/sources.pri +++ b/app/sources.pri @@ -33,8 +33,8 @@ ios/iosutils.cpp \ inputprojutils.cpp \ codefilter.cpp \ qrdecoder.cpp \ -models/projectsproxymodel_future.cpp \ -models/projectsmodel_future.cpp \ +projectsproxymodel_future.cpp \ +projectsmodel_future.cpp \ project_future.cpp \ exists(merginsecrets.cpp) { @@ -79,8 +79,8 @@ ios/iosutils.h \ inputprojutils.h \ codefilter.h \ qrdecoder.h \ -models/projectsproxymodel_future.h \ -models/projectsmodel_future.h \ +projectsproxymodel_future.h \ +projectsmodel_future.h \ project_future.h \ contains(DEFINES, INPUT_TEST) { From 169e9c35e1bc246bbd556ccd4139c4e77327b669 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Wed, 17 Mar 2021 13:47:56 +0100 Subject: [PATCH 09/53] move sync logic to cpp --- app/merginapi.cpp | 2 + app/project_future.cpp | 6 +- app/project_future.h | 39 +++++++--- app/projectsmodel_future.cpp | 137 ++++++++++++++++++++++++++++++++--- app/projectsmodel_future.h | 20 ++++- 5 files changed, 179 insertions(+), 25 deletions(-) diff --git a/app/merginapi.cpp b/app/merginapi.cpp index 12dd1bacb..300190f70 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -121,6 +121,7 @@ QString MerginApi::listProjectsByName( const QStringList &projectNames ) projects.insert( "projects", projectsArr ); body.setObject( projects ); + qDebug() << "PMR: listProjectsByName(): requesting projects " << projectNames; QUrl url( mApiRoot + QStringLiteral( "/v1/project/by_names" ) ); @@ -1238,6 +1239,7 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) } // for any local projects we can update the latest server version + // TODO: this should now be done inside model so no need to do it here (LocalProjects do not have server version anymore) for ( MerginProjectListEntry project : mRemoteProjects ) { QString fullProjectName = getFullProjectName( project.projectNamespace, project.projectName ); diff --git a/app/project_future.cpp b/app/project_future.cpp index dcb3cabd8..b937b568d 100644 --- a/app/project_future.cpp +++ b/app/project_future.cpp @@ -8,9 +8,10 @@ ***************************************************************************/ #include "project_future.h" - #include "merginapi.h" +#include + void LocalProject_future::copyValues( const LocalProject_future &other ) { projectName = other.projectName; @@ -31,5 +32,6 @@ QString LocalProject_future::id() if ( !projectName.isEmpty() && !projectNamespace.isEmpty() ) return MerginApi::getFullProjectName( projectNamespace, projectName ); - return projectDir; + QDir dir( projectDir ); + return dir.dirName(); } diff --git a/app/project_future.h b/app/project_future.h index cef3229b6..e4a359c82 100644 --- a/app/project_future.h +++ b/app/project_future.h @@ -27,13 +27,13 @@ Q_ENUMS( ProjectStatus_future ) struct LocalProject_future { - LocalProject_future() {}; /*{ qDebug() << "Building LocalProject_future " << this; }*/ - ~LocalProject_future() {}; /*{ qDebug() << "Removing LocalProject_future " << this; }*/ + LocalProject_future() {}; + ~LocalProject_future() {}; QString projectName; QString projectNamespace; - QString id(); + QString id(); //! projectFullName for time being QString projectDir; QString projectError; // Error that leads to project not being able to open in app @@ -47,13 +47,14 @@ struct LocalProject_future struct MerginProject_future { - MerginProject_future() {}; /*{ qDebug() << "Building MerginProject_future " << this; }*/ - ~MerginProject_future() {}; /*{ qDebug() << "Removing MerginProject_future " << this; }*/ + MerginProject_future() {}; + ~MerginProject_future() {}; QString projectName; QString projectNamespace; - QString id(); + QString id(); //! projectFullName for time being + QDateTime serverUpdated; // available latest version of project files on server int serverVersion; @@ -68,14 +69,32 @@ struct MerginProject_future struct Project_future { - Project_future() {}; /*{ qDebug() << "Building Project_future " << this; }*/ - ~Project_future() {}; /*{ qDebug() << "Removing Project_future " << this; }*/ + Project_future() {}; + ~Project_future() {}; std::unique_ptr mergin; std::unique_ptr local; - bool isMergin() { return mergin != nullptr; } - bool isLocal() { return local != nullptr; } + bool isMergin() const { return mergin != nullptr; } + bool isLocal() const { return local != nullptr; } + + bool operator ==( const Project_future &other ) + { + if ( this->isLocal() && other.isLocal() ) + { + return this->local->id() == other.local->id(); + } + else if ( this->isMergin() && other.isMergin() ) + { + return this->mergin->id() == other.mergin->id(); + } + return false; + } + + bool operator !=( const Project_future &other ) + { + return !( *this == other ); + } }; #endif // PROJECT_FUTURE_H diff --git a/app/projectsmodel_future.cpp b/app/projectsmodel_future.cpp index 589979c5e..6476f64e0 100644 --- a/app/projectsmodel_future.cpp +++ b/app/projectsmodel_future.cpp @@ -10,6 +10,7 @@ #include "projectsmodel_future.h" #include "localprojectsmanager.h" #include "inpututils.h" +#include "merginuserauth.h" ProjectsModel_future::ProjectsModel_future( MerginApi *merginApi, @@ -41,21 +42,29 @@ ProjectsModel_future::ProjectsModel_future( } } -QVariant ProjectsModel_future::data( const QModelIndex &, int ) const +QVariant ProjectsModel_future::data( const QModelIndex &index, int role ) const { - return QVariant( "Test data" ); + if ( !index.isValid() ) + return QVariant(); + + std::shared_ptr project = mProjects.at( index.row() ); + + switch ( role ) { + default: return QVariant("TestData"); + } } -QModelIndex ProjectsModel_future::index( int, int, const QModelIndex & ) const +QModelIndex ProjectsModel_future::index( int row, int col, const QModelIndex &parent ) const { - return QModelIndex(); + Q_UNUSED( col ) + Q_UNUSED( parent ) + return createIndex( row, 0, nullptr ); } QHash ProjectsModel_future::roleNames() const { QHash roles; - roles[Roles::Project] = QStringLiteral( "project" ).toLatin1(); - + roles[Roles::ProjectName] = QStringLiteral( "ProjectName" ).toLatin1(); return roles; } @@ -104,6 +113,8 @@ void ProjectsModel_future::mergeProjects( const MerginProjectList &merginProject local->projectName = localProject.projectName; local->projectNamespace = localProject.projectNamespace; local->projectDir = localProject.projectDir; + local->projectError = localProject.qgisProjectError; + local->qgisProjectFilePath = localProject.qgisProjectFilePath; // TODO: later copy data by copy constructor project->local = std::move( local ); @@ -215,19 +226,104 @@ void ProjectsModel_future::onListProjectsByNameFinished( const MerginProjectList endResetModel(); } +void ProjectsModel_future::syncProject( QString projectNamespace, QString projectName ) +{ + std::shared_ptr project = projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + + if ( project == nullptr ) + { + qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "not in projects list"; + return; + } + + if ( !project->isMergin() ) + { + qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "is not a mergin project"; + return; + } + + if ( project->mergin->pending ) + { + qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "is already "; + return; + } + + if ( project->mergin->status == _NoVersion || project->mergin->status == _OutOfDate ) + { + qDebug() << "PMR: updating project:" << project->mergin->id(); + + bool useAuth = !mBackend->userAuth()->hasAuthData() && mModelType == ProjectModelTypes::ExploreProjectsModel; + mBackend->updateProject( projectNamespace, projectName, useAuth ); + } + else if ( project->mergin->status == _Modified ) + { + qDebug() << "PMR: uploading project:" << project->mergin->id(); + mBackend->uploadProject( projectNamespace, projectName ); + } +} + +void ProjectsModel_future::stopProjectSync( QString projectNamespace, QString projectName ) +{ + std::shared_ptr project = projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + + if ( project == nullptr ) + { + qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "not in projects list"; + return; + } + + if ( !project->isMergin() ) + { + qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "is not a mergin project"; + return; + } + + if ( !project->mergin->pending ) + { + qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "is not pending"; + return; + } + + if ( project->mergin->status == _NoVersion || project->mergin->status == _OutOfDate ) + { + qDebug() << "PMR: cancelling update of project:" << project->mergin->id(); + mBackend->updateCancel( project->mergin->id() ); + } + else if ( project->mergin->status == _Modified ) + { + qDebug() << "PMR: cancelling upload of project:" << project->mergin->id(); + mBackend->uploadCancel( project->mergin->id() ); + } +} + void ProjectsModel_future::onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully ) { Q_UNUSED( projectDir ) - Q_UNUSED( projectFullName ) - Q_UNUSED( successfully ) + + std::shared_ptr project = projectFromId( projectFullName ); + if ( !project || !project->isMergin() || !successfully ) + return; + + project->mergin->pending = false; + project->mergin->progress = 0; + + QModelIndex ix = index( mProjects.indexOf( project ) ); + emit dataChanged( ix, ix ); qDebug() << "PMR: Project " << projectFullName << " finished sync"; } void ProjectsModel_future::onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ) { - Q_UNUSED( projectFullName ) - Q_UNUSED( progress ) + std::shared_ptr project = projectFromId( projectFullName ); + if ( !project || !project->isMergin() ) + return; + + project->mergin->pending = progress >= 0; + project->mergin->progress = progress >= 0 ? progress : 0; + + QModelIndex ix = index( mProjects.indexOf( project ) ); + emit dataChanged( ix, ix ); qDebug() << "PMR: Project " << projectFullName << " changed sync progress to " << progress; } @@ -247,7 +343,7 @@ QString ProjectsModel_future::modelTypeToFlag() const void ProjectsModel_future::printProjects() const // TODO: Helper function, remove after refactoring is done { - qDebug() << "Model " << this << " with type " << modelTypeToFlag() << " has projects: "; + qDebug() << "PMR: Model " << this << " with type " << modelTypeToFlag() << " has projects: "; for ( const auto &proj : mProjects ) { QString lcl = proj->isLocal() ? "local" : ""; @@ -277,3 +373,22 @@ QStringList ProjectsModel_future::projectNames() const // TODO: use local projec return projectNames; } + +bool ProjectsModel_future::containsProject( QString projectId ) const +{ + std::shared_ptr proj = projectFromId( projectId ); + return proj != nullptr; +} + +std::shared_ptr ProjectsModel_future::projectFromId( QString projectId ) const +{ + for ( int ix = 0; ix < mProjects.size(); ++ix ) + { + if ( mProjects[ix]->isMergin() && mProjects[ix]->mergin->id() == projectId ) + return mProjects[ix]; + else if ( mProjects[ix]->isLocal() && mProjects[ix]->local->id() == projectId ) + return mProjects[ix]; + } + + return nullptr; +} diff --git a/app/projectsmodel_future.h b/app/projectsmodel_future.h index 066b6089b..6a5a9224e 100644 --- a/app/projectsmodel_future.h +++ b/app/projectsmodel_future.h @@ -41,8 +41,15 @@ class ProjectsModel_future : public QAbstractListModel enum Roles { - // TODO: rewrite to individual roles - Project = Qt::UserRole + 1 + ProjectName = Qt::UserRole + 1, + ProjectNamespace, + ProjectFullName, // or ProjectId, filled with folderName if project is not + ProjectDescription, + ProjectPending, + ProjectIsMergin, + ProjectIsLocal, + ProjectStatus, + ProjectProgress }; Q_ENUMS( Roles ) @@ -61,6 +68,12 @@ class ProjectsModel_future : public QAbstractListModel //! Called to list projects, either fetch more or get first Q_INVOKABLE void listProjectsByName(); + //! Syncs specified project - upload or update + Q_INVOKABLE void syncProject( QString projectNamespace, QString projectName ); + + //! Stops running project upload or update + Q_INVOKABLE void stopProjectSync( QString projectNamespace, QString projectName ); + //! Method detecting local project for remote projects void mergeProjects( const MerginProjectList &merginProjects, Transactions pendingProjects ); @@ -76,6 +89,9 @@ class ProjectsModel_future : public QAbstractListModel void printProjects() const; QStringList projectNames() const; + bool containsProject( QString projectId ) const; + std::shared_ptr projectFromId( QString projectId ) const; + MerginApi *mBackend; LocalProjectsManager &mLocalProjectsManager; QList> mProjects; From 859bd030aa03599ed571bb01c1063edba3944e4b Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Thu, 18 Mar 2021 08:31:42 +0100 Subject: [PATCH 10/53] add several signals/slots --- app/merginapi.cpp | 3 ++- app/merginapi.h | 2 +- app/project_future.h | 2 +- app/projectsmodel_future.cpp | 19 +++++++++++++++++++ app/projectsmodel_future.h | 10 ++++++++-- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/app/merginapi.cpp b/app/merginapi.cpp index 300190f70..ffd92b79d 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -1150,7 +1150,7 @@ void MerginApi::detachProjectFromMergin( const QString &projectNamespace, const mLocalProjects.reloadProjectDir(); emit notify( tr( "Project detached from Mergin" ) ); - emit projectDetached(); + emit projectDetached( projectFullName ); } QString MerginApi::apiRoot() const @@ -1518,6 +1518,7 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) // add the local project if not there yet if ( !mLocalProjects.projectFromMerginName( projectFullName ).isValid() ) { + qDebug() << "PMR: Downloaded project" << projectFullName; QString projectNamespace, projectName; extractProjectName( projectFullName, projectNamespace, projectName ); diff --git a/app/merginapi.h b/app/merginapi.h index 761920d86..6870fca70 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -443,7 +443,7 @@ class MerginApi: public QObject //! Emitted when upload cancellation request has finished void uploadCanceled( const QString &projectFullName, bool result ); void projectDataChanged( const QString &projectFullName ); - void projectDetached(); + void projectDetached( const QString &projectFullName ); private slots: void listProjectsReplyFinished( QString requestId ); diff --git a/app/project_future.h b/app/project_future.h index e4a359c82..1f0b17668 100644 --- a/app/project_future.h +++ b/app/project_future.h @@ -27,7 +27,7 @@ Q_ENUMS( ProjectStatus_future ) struct LocalProject_future { - LocalProject_future() {}; + LocalProject_future() {}; // TODO: define copy constructor ~LocalProject_future() {}; QString projectName; diff --git a/app/projectsmodel_future.cpp b/app/projectsmodel_future.cpp index 6476f64e0..c3b200078 100644 --- a/app/projectsmodel_future.cpp +++ b/app/projectsmodel_future.cpp @@ -24,6 +24,7 @@ ProjectsModel_future::ProjectsModel_future( { QObject::connect( mBackend, &MerginApi::syncProjectStatusChanged, this, &ProjectsModel_future::onProjectSyncProgressChanged ); QObject::connect( mBackend, &MerginApi::syncProjectFinished, this, &ProjectsModel_future::onProjectSyncFinished ); + QObject::connect( mBackend, &MerginApi::projectDetached, this, &ProjectsModel_future::onProjectDetachedFromMergin ); // TODO: connect to signals from LocalProjectsManager // QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectAdded ) @@ -40,6 +41,8 @@ ProjectsModel_future::ProjectsModel_future( { // Implement RecentProjectsModel type } + +// QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectAdded, this, &ProjectsModel_future::onProjectAdded ); } QVariant ProjectsModel_future::data( const QModelIndex &index, int role ) const @@ -328,6 +331,22 @@ void ProjectsModel_future::onProjectSyncProgressChanged( const QString &projectF qDebug() << "PMR: Project " << projectFullName << " changed sync progress to " << progress; } +void ProjectsModel_future::onProjectAdded( const LocalProject_future &project ) +{ + std::shared_ptr newProject = std::shared_ptr( new Project_future() ); + newProject->local = std::unique_ptr( new LocalProject_future( project ) ); +} + +void ProjectsModel_future::onProjectDeleted( const QString &projectFullName ) +{ + Q_UNUSED( projectFullName ) +} + +void ProjectsModel_future::onProjectDetachedFromMergin( const QString &projectFullName ) +{ + Q_UNUSED( projectFullName ) +} + QString ProjectsModel_future::modelTypeToFlag() const { switch ( mModelType ) diff --git a/app/projectsmodel_future.h b/app/projectsmodel_future.h index 6a5a9224e..19ffd583f 100644 --- a/app/projectsmodel_future.h +++ b/app/projectsmodel_future.h @@ -74,7 +74,7 @@ class ProjectsModel_future : public QAbstractListModel //! Stops running project upload or update Q_INVOKABLE void stopProjectSync( QString projectNamespace, QString projectName ); - //! Method detecting local project for remote projects + //! Method merging local and remote projects based on the model type void mergeProjects( const MerginProjectList &merginProjects, Transactions pendingProjects ); public slots: @@ -83,6 +83,12 @@ class ProjectsModel_future : public QAbstractListModel void onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); void onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ); + void onProjectAdded( const LocalProject_future &project ); + void onProjectDeleted( const QString &projectFullName ); + + void onProjectDetachedFromMergin( const QString &projectFullName ); + void onProjectAttachedToMergin() {}; + private: QString modelTypeToFlag() const; @@ -99,7 +105,7 @@ class ProjectsModel_future : public QAbstractListModel ProjectModelTypes mModelType; //! For pagination - int mPopulatedPage = -1; + int mPopulatedPage = -1; // -> on the fly in QML:: QML should pass this to model //! For processing only my requests QString mLastRequestId; From 4613b93e5117e7374b724163f121d46d70f696eb Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Thu, 18 Mar 2021 10:34:22 +0100 Subject: [PATCH 11/53] LocalProjectManager revamp --- app/inpututils.cpp | 8 + app/inpututils.h | 1 + app/localprojectsmanager.cpp | 266 +++++++++++++++---------------- app/localprojectsmanager.h | 116 +++++++------- app/merginapi.cpp | 24 +-- app/merginapi.h | 1 + app/merginprojectstatusmodel.cpp | 2 +- app/project_future.cpp | 4 +- app/project_future.h | 6 +- app/projectsmodel_future.cpp | 21 ++- app/projectsmodel_future.h | 5 +- app/test/testmerginapi.cpp | 12 +- 12 files changed, 246 insertions(+), 220 deletions(-) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index 8216a73b7..2903add45 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -488,6 +488,14 @@ QString InputUtils::createUniqueProjectDirectory( const QString &baseDataDir, co return projectDirPath; } +bool InputUtils::removeDir( const QString &dir ) +{ + if( dir.isEmpty() || dir == "/" ) + return false; + + return QDir( dir ).removeRecursively(); +} + void InputUtils::onQgsLogMessageReceived( const QString &message, const QString &tag, Qgis::MessageLevel level ) { QString levelStr; diff --git a/app/inpututils.h b/app/inpututils.h index 6744a65f6..6a0e489b7 100644 --- a/app/inpututils.h +++ b/app/inpututils.h @@ -169,6 +169,7 @@ class InputUtils: public QObject //! Creates and registers custom expression functions to Input, so they can be used in default value definitions. static void registerInputExpressionFunctions(); + static bool removeDir( const QString &projectDir ); signals: Q_INVOKABLE void showNotificationRequested( const QString &message ); diff --git a/app/localprojectsmanager.cpp b/app/localprojectsmanager.cpp index 2a70fa1a9..fece7d997 100644 --- a/app/localprojectsmanager.cpp +++ b/app/localprojectsmanager.cpp @@ -19,18 +19,18 @@ LocalProjectsManager::LocalProjectsManager( const QString &dataDir ) : mDataDir( dataDir ) { - reloadProjectDir(); + reloadDataDir(); } -void LocalProjectsManager::reloadProjectDir() // TODO: maybe add function to reload one specific project +void LocalProjectsManager::reloadDataDir() // TODO: maybe add function to reload one specific project { mProjects.clear(); QStringList entryList = QDir( mDataDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); for ( QString folderName : entryList ) { - LocalProjectInfo info; + LocalProject_future info; info.projectDir = mDataDir + "/" + folderName; - info.qgisProjectFilePath = findQgisProjectFile( info.projectDir, info.qgisProjectError ); + info.qgisProjectFilePath = findQgisProjectFile( info.projectDir, info.projectError ); MerginProjectMetadata metadata = MerginProjectMetadata::fromCachedJson( info.projectDir + "/" + MerginApi::sMetadataFile ); if ( metadata.isValid() ) @@ -39,10 +39,10 @@ void LocalProjectsManager::reloadProjectDir() // TODO: maybe add function to rel info.projectNamespace = metadata.projectNamespace; info.localVersion = metadata.version; } - else - { - info.projectName = folderName; - } +// else +// { +// info.projectName = folderName; +// } mProjects << info; } @@ -50,180 +50,171 @@ void LocalProjectsManager::reloadProjectDir() // TODO: maybe add function to rel qDebug() << "LocalProjectsManager: found" << mProjects.size() << "projects"; } -LocalProjectInfo LocalProjectsManager::projectFromDirectory( const QString &projectDir ) const +LocalProject_future LocalProjectsManager::projectFromDirectory( const QString &projectDir ) const { - for ( const LocalProjectInfo &info : mProjects ) + for ( const LocalProject_future &info : mProjects ) { if ( info.projectDir == projectDir ) return info; } - return LocalProjectInfo(); + return LocalProject_future(); } -LocalProjectInfo LocalProjectsManager::projectFromProjectFilePath( const QString &projectFilePath ) const +LocalProject_future LocalProjectsManager::projectFromProjectFilePath( const QString &projectFilePath ) const { - for ( const LocalProjectInfo &info : mProjects ) + for ( const LocalProject_future &info : mProjects ) { if ( info.qgisProjectFilePath == projectFilePath ) return info; } - return LocalProjectInfo(); + return LocalProject_future(); } -LocalProjectInfo LocalProjectsManager::projectFromMerginName( const QString &projectFullName ) const +LocalProject_future LocalProjectsManager::projectFromMerginName( const QString &projectFullName ) const { - for ( const LocalProjectInfo &info : mProjects ) + for ( const LocalProject_future &info : mProjects ) { - if ( MerginApi::getFullProjectName( info.projectNamespace, info.projectName ) == projectFullName ) + if ( info.id() == projectFullName ) return info; } - return LocalProjectInfo(); + return LocalProject_future(); } -LocalProjectInfo LocalProjectsManager::projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const +LocalProject_future LocalProjectsManager::projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const { return projectFromMerginName( MerginApi::getFullProjectName( projectNamespace, projectName ) ); } -bool LocalProjectsManager::hasMerginProject( const QString &projectFullName ) const -{ - return projectFromMerginName( projectFullName ).isValid(); -} - -bool LocalProjectsManager::hasMerginProject( const QString &projectNamespace, const QString &projectName ) const -{ - return hasMerginProject( MerginApi::getFullProjectName( projectNamespace, projectName ) ); -} - -void LocalProjectsManager::updateProjectStatus( const QString &projectDir ) -{ - for ( LocalProjectInfo &info : mProjects ) - { - if ( info.projectDir == projectDir ) - { - updateProjectStatus( info ); - return; - } - } - Q_ASSERT( false ); // should not happen -} - -void LocalProjectsManager::addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ) +//bool LocalProjectsManager::hasMerginProject( const QString &projectFullName ) const +//{ +// return projectFromMerginName( projectFullName ).isValid(); +//} + +//bool LocalProjectsManager::hasMerginProject( const QString &projectNamespace, const QString &projectName ) const +//{ +// return hasMerginProject( MerginApi::getFullProjectName( projectNamespace, projectName ) ); +//} + +//void LocalProjectsManager::updateProjectStatus( const QString &projectDir ) +//{ +// for ( LocalProject_future &info : mProjects ) +// { +// if ( info.projectDir == projectDir ) +// { +// updateProjectStatus( info ); +// return; +// } +// } +// Q_ASSERT( false ); // should not happen +//} + +void LocalProjectsManager::addLocalProject( const QString &projectDir, const QString &projectName, const QString &projectNamespace ) { - LocalProjectInfo project; + LocalProject_future project; project.projectDir = projectDir; - project.qgisProjectFilePath = findQgisProjectFile( projectDir, project.qgisProjectError ); + project.qgisProjectFilePath = findQgisProjectFile( projectDir, project.projectError ); project.projectNamespace = projectNamespace; project.projectName = projectName; mProjects << project; -} - -void LocalProjectsManager::addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ) -{ - addProject( projectDir, projectNamespace, projectName ); - // version info and status should be updated afterwards - emit localMerginProjectAdded( projectDir ); -} - -void LocalProjectsManager::addLocalProject( const QString &projectDir, const QString &projectName ) -{ - addProject( projectDir, QString(), projectName ); emit localProjectAdded( projectDir ); } -void LocalProjectsManager::removeProject( const QString &projectDir ) -{ - for ( int i = 0; i < mProjects.count(); ++i ) - { - if ( mProjects[i].projectDir == projectDir ) - { - mProjects.removeAt( i ); - emit localProjectRemoved( projectDir ); - return; - } - } -} +//void LocalProjectsManager::addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ) +//{ +// addProject( projectDir, projectNamespace, projectName ); +// // version info and status should be updated afterwards +// emit localMerginProjectAdded( projectDir ); +//} -void LocalProjectsManager::resetMerginInfo( const QString &projectNamespace, const QString &projectName ) -{ - for ( int i = 0; i < mProjects.count(); ++i ) - { - if ( mProjects[i].projectNamespace == projectNamespace && mProjects[i].projectName == projectName ) - { - mProjects[i].localVersion = -1; - mProjects[i].serverVersion = -1; - mProjects[i].projectNamespace.clear(); - updateProjectStatus( mProjects[i] ); - emit projectMetadataChanged( mProjects[i].projectDir ); - return; - } - } -} +//void LocalProjectsManager::addLocalProject( const QString &projectDir, const QString &projectName ) +//{ +// addProject( projectDir, QString(), projectName ); +// emit localProjectAdded( projectDir ); +//} -void LocalProjectsManager::deleteProjectDirectory( const QString &projectDir ) +void LocalProjectsManager::removeLocalProject( const QString &projectDir ) { for ( int i = 0; i < mProjects.count(); ++i ) { if ( mProjects[i].projectDir == projectDir ) { - Q_ASSERT( !projectDir.isEmpty() && projectDir != "/" ); - QDir( projectDir ).removeRecursively(); + InputUtils::removeDir( mProjects[i].projectDir ); mProjects.removeAt( i ); + + emit localProjectRemoved( projectDir ); return; } } } -void LocalProjectsManager::updateMerginLocalVersion( const QString &projectDir, int version ) +//void LocalProjectsManager::removeMerginInfo( const QString &projectFullName ) +//{ +// for ( int i = 0; i < mProjects.count(); ++i ) +// { +// if ( mProjects[i].id() == projectFullName ) +// { +// mProjects[i].localVersion = -1; +// mProjects[i].projectNamespace.clear(); +// InputUtils::removeDir( mProjects[i].projectDir + "/.mergin" ); + +// emit localProjectDataChanged( mProjects[i].projectDir ); +// return; +// } +// } +//} + +void LocalProjectsManager::updateLocalVersion( const QString &projectDir, int version ) { for ( int i = 0; i < mProjects.count(); ++i ) { if ( mProjects[i].projectDir == projectDir ) { mProjects[i].localVersion = version; - updateProjectStatus( mProjects[i] ); - return; - } - } - Q_ASSERT( false ); // should not happen -} -void LocalProjectsManager::updateMerginServerVersion( const QString &projectDir, int version ) -{ - for ( int i = 0; i < mProjects.count(); ++i ) - { - if ( mProjects[i].projectDir == projectDir ) - { - mProjects[i].serverVersion = version; - updateProjectStatus( mProjects[i] ); + emit localProjectDataChanged( mProjects[i].projectDir ); return; } } Q_ASSERT( false ); // should not happen } -void LocalProjectsManager::updateProjectErrors( const QString &projectDir, const QString &errMsg ) +//void LocalProjectsManager::updateMerginServerVersion( const QString &projectDir, int version ) +//{ +// for ( int i = 0; i < mProjects.count(); ++i ) +// { +// if ( mProjects[i].projectDir == projectDir ) +// { +// mProjects[i].serverVersion = version; +// updateProjectStatus( mProjects[i] ); +// return; +// } +// } +// Q_ASSERT( false ); // should not happen +//} + +//void LocalProjectsManager::updateProjectErrors( const QString &projectDir, const QString &errMsg ) +//{ +// for ( int i = 0; i < mProjects.count(); ++i ) +// { +// if ( mProjects[i].projectDir == projectDir ) +// { +// // Effects only local project list, no need to send projectMetadataChanged +// mProjects[i].qgisProjectError = errMsg; +// return; +// } +// } +//} + +void LocalProjectsManager::updateNamespace( const QString &projectDir, const QString &projectNamespace ) { for ( int i = 0; i < mProjects.count(); ++i ) { if ( mProjects[i].projectDir == projectDir ) { - // Effects only local project list, no need to send projectMetadataChanged - mProjects[i].qgisProjectError = errMsg; - return; - } - } -} - -void LocalProjectsManager::updateMerginNamespace( const QString &projectDir, const QString &projectNamespace ) -{ - for ( int i = 0; i < mProjects.count(); ++i ) - { - if ( mProjects[i].projectDir == projectDir ) - { - // Effects only local project list, no need to send projectMetadataChanged mProjects[i].projectNamespace = projectNamespace; + + emit localProjectDataChanged( mProjects[i].projectDir ); return; } } @@ -301,12 +292,15 @@ static int _getProjectFilesCount( const QString &path ) return count; } -ProjectStatus LocalProjectsManager::currentProjectStatus( const LocalProjectInfo &project ) +ProjectStatus_future LocalProjectsManager::currentProjectStatus( const Project_future &project ) { + if ( !project.isMergin() || !project.isLocal() ) // This is not a Mergin project or not downloaded project + return ProjectStatus_future::_NoVersion; + // There was no sync yet - if ( project.localVersion < 0 ) + if ( project.local->localVersion < 0 ) { - return ProjectStatus::NoVersion; + return ProjectStatus_future::_NoVersion; } // @@ -314,31 +308,31 @@ ProjectStatus LocalProjectsManager::currentProjectStatus( const LocalProjectInfo // // Something has locally changed after last sync with server - QString metadataFilePath = project.projectDir + "/" + MerginApi::sMetadataFile; - QDateTime lastModified = _getLastModifiedFileDateTime( project.projectDir ); + QString metadataFilePath = project.local->projectDir + "/" + MerginApi::sMetadataFile; + QDateTime lastModified = _getLastModifiedFileDateTime( project.local->projectDir ); QDateTime lastSync = QFileInfo( metadataFilePath ).lastModified(); MerginProjectMetadata meta = MerginProjectMetadata::fromCachedJson( metadataFilePath ); - int filesCount = _getProjectFilesCount( project.projectDir ); + int filesCount = _getProjectFilesCount( project.local->projectDir ); if ( lastSync < lastModified || meta.files.count() != filesCount ) { - return ProjectStatus::Modified; + return ProjectStatus_future::_Modified; } // Version is lower than latest one, last sync also before updated - if ( project.localVersion < project.serverVersion ) + if ( project.local->localVersion < project.mergin->serverVersion ) { - return ProjectStatus::OutOfDate; + return ProjectStatus_future::_OutOfDate; } - return ProjectStatus::UpToDate; + return ProjectStatus_future::_UpToDate; } -void LocalProjectsManager::updateProjectStatus( LocalProjectInfo &project ) -{ - ProjectStatus newStatus = currentProjectStatus( project ); - if ( newStatus != project.status ) - { - project.status = newStatus; - emit projectMetadataChanged( project.projectDir ); - } -} +//void LocalProjectsManager::updateProjectStatus( LocalProject_future &project ) +//{ +// ProjectStatus newStatus = currentProjectStatus( project ); +// if ( newStatus != project.status ) +// { +// project.status = newStatus; +// emit projectMetadataChanged( project.projectDir ); +// } +//} diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index dfb84a674..23f86c36c 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -13,50 +13,50 @@ #include #include -enum ProjectStatus -{ - NoVersion, //!< the project is not available locally - UpToDate, //!< both server and local copy are in sync with no extra modifications - OutOfDate, //!< server has newer version than what is available locally (but the project is not modified locally) - Modified, //!< there are some local modifications in the project that need to be pushed (note: also server may have newer version) - NonProjectItem //!< only for mock projects, acts like a hook to enable extra functionality for models working with projects . -}; -Q_ENUMS( ProjectStatus ) +//enum ProjectStatus +//{ +// NoVersion, //!< the project is not available locally +// UpToDate, //!< both server and local copy are in sync with no extra modifications +// OutOfDate, //!< server has newer version than what is available locally (but the project is not modified locally) +// Modified, //!< there are some local modifications in the project that need to be pushed (note: also server may have newer version) +// NonProjectItem //!< only for mock projects, acts like a hook to enable extra functionality for models working with projects . +//}; +//Q_ENUMS( ProjectStatus ) //! Summary information about a local project -struct LocalProjectInfo -{ - bool isValid() const { return !projectDir.isEmpty(); } +//struct LocalProjectInfo +//{ +// bool isValid() const { return !projectDir.isEmpty(); } - bool isShowable() const { return qgisProjectError.isEmpty(); } +// bool isShowable() const { return qgisProjectError.isEmpty(); } - QString projectDir; //!< full path to the project directory +// QString projectDir; //!< full path to the project directory - QString qgisProjectFilePath; //!< path to the .qgs/.qgz file (or empty if not have exactly one such file) +// QString qgisProjectFilePath; //!< path to the .qgs/.qgz file (or empty if not have exactly one such file) - QString qgisProjectError; //!< If project is invalid, projectError carry more information why - // TODO: reset when project is synchronized +// QString qgisProjectError; //!< If project is invalid, projectError carry more information why +// // TODO: reset when project is synchronized - // - // mergin-specific project info (may be empty) - // +// // +// // mergin-specific project info (may be empty) +// // - QString projectName; - QString projectNamespace; +// QString projectName; +// QString projectNamespace; - int localVersion = -1; //!< the project version that is currently available locally - int serverVersion = -1; //!< the project version most recently seen on server (may be -1 if no info from server is available) +// int localVersion = -1; //!< the project version that is currently available locally +// int serverVersion = -1; //!< the project version most recently seen on server (may be -1 if no info from server is available) - ProjectStatus status = NoVersion; +// ProjectStatus status = NoVersion; - bool operator ==( const LocalProjectInfo &other ) { return ( projectName == other.projectName && projectNamespace == other.projectNamespace );} - bool operator !=( const LocalProjectInfo &other ) { return !( *this == other ); } +// bool operator ==( const LocalProjectInfo &other ) { return ( projectName == other.projectName && projectNamespace == other.projectNamespace );} +// bool operator !=( const LocalProjectInfo &other ) { return !( *this == other ); } - // Sync status (e.g. progress) is not kept here because if a project does not exist locally yet - // and it is only being downloaded for the first time, it's not in the list of local projects either - // and we would need to do some workarounds for that. -}; +// // Sync status (e.g. progress) is not kept here because if a project does not exist locally yet +// // and it is only being downloaded for the first time, it's not in the list of local projects either +// // and we would need to do some workarounds for that. +//}; class LocalProjectsManager : public QObject @@ -67,75 +67,75 @@ class LocalProjectsManager : public QObject QString dataDir() const { return mDataDir; } - QList projects() const { return mProjects; } + QList projects() const { return mProjects; } - void reloadProjectDir(); + //! Loads all projects from mDataDir, removes all old projects + void reloadDataDir(); - LocalProjectInfo projectFromDirectory( const QString &projectDir ) const; - LocalProjectInfo projectFromProjectFilePath( const QString &projectDir ) const; + LocalProject_future projectFromDirectory( const QString &projectDir ) const; + LocalProject_future projectFromProjectFilePath( const QString &projectFilePath ) const; - LocalProjectInfo projectFromMerginName( const QString &projectFullName ) const; - LocalProjectInfo projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const; + LocalProject_future projectFromMerginName( const QString &projectFullName ) const; + LocalProject_future projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const; - bool hasMerginProject( const QString &projectFullName ) const; - bool hasMerginProject( const QString &projectNamespace, const QString &projectName ) const; + //! Adds entry about newly created project + void addLocalProject( const QString &projectDir, const QString &projectName, const QString &projectNamespace = QString() ); - void updateProjectStatus( const QString &projectDir ); +// bool hasMerginProject( const QString &projectFullName ) const; +// bool hasMerginProject( const QString &projectNamespace, const QString &projectName ) const; + +// void updateProjectStatus( const QString &projectDir ); //! Should add an entry about newly created Mergin project - void addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); +// void addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); //! Should add an entry about newly created local project - void addLocalProject( const QString &projectDir, const QString &projectName ); +// void addLocalProject( const QString &projectDir, const QString &projectName ); //! Should forget about that project (it has been removed already) - void removeProject( const QString &projectDir ); + void removeLocalProject( const QString &projectDir ); //! Resets mergin related info for given project. - void resetMerginInfo( const QString &projectNamespace, const QString &projectName ); +// void removeMerginInfo( const QString &projectFullName ); //! Recursively removes project's directory (only when it exists in the list) - void deleteProjectDirectory( const QString &projectDir ); +// void deleteProjectDirectory( const QString &projectDir ); // // updates of mergin info // //! after successful update/upload - both server and local version are the same - void updateMerginLocalVersion( const QString &projectDir, int version ); + void updateLocalVersion( const QString &projectDir, int version ); //! after receiving project info with server version (local version stays the same - void updateMerginServerVersion( const QString &projectDir, int version ); +// void updateMerginServerVersion( const QString &projectDir, int version ); //! Updates qgisProjectError (after successful project synced) - void updateProjectErrors( const QString &projectDir, const QString &errMsg ); +// void updateProjectErrors( const QString &projectDir, const QString &errMsg ); //! Updates proejct's namespace - void updateMerginNamespace( const QString &projectDir, const QString &projectNamespace ); + void updateNamespace( const QString &projectDir, const QString &projectNamespace ); //! Finds all QGIS project files and set the err variable if any occured. QString findQgisProjectFile( const QString &projectDir, QString &err ); - static ProjectStatus currentProjectStatus( const LocalProjectInfo &project ); // TODO: local project manager should not have status + static ProjectStatus_future currentProjectStatus( const Project_future &project ); // TODO: maybe move somewhere else? signals: void projectMetadataChanged( const QString &projectDir ); void localMerginProjectAdded( const QString &projectDir ); void localProjectAdded( const QString &projectDir ); void localProjectRemoved( const QString &projectDir ); + void localProjectDataChanged( const QString &projectDir ); - private: - void updateProjectStatus( LocalProjectInfo &project ); // TODO: local project manager should not have status - - //! Should add an entry about newly created project. Emits no signals - void addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); +// private: +// void updateProjectStatus( LocalProject_future &project ); // TODO: local project manager should not have status private: QString mDataDir; //!< directory with all local projects - QList mProjects; - - QList mLocalProjects_future; + QList mProjects; }; diff --git a/app/merginapi.cpp b/app/merginapi.cpp index ffd92b79d..84084f928 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -761,11 +761,12 @@ void MerginApi::createProjectFinished() extractProjectName( projectFullName, projectNamespace, projectName ); // Upload data if createProject has been called for a local project with empty namespace (case of migrating a project) - for ( const LocalProjectInfo &info : mLocalProjects.projects() ) + for ( const LocalProject_future &info : mLocalProjects.projects() ) { if ( info.projectName == projectName && info.projectNamespace.isEmpty() ) { - mLocalProjects.updateMerginNamespace( info.projectDir, projectNamespace ); + mLocalProjects.updateNamespace( info.projectDir, projectNamespace ); + emit projectAttachedToMergin( projectFullName ); QDir projectDir( info.projectDir ); if ( projectDir.exists() && !projectDir.isEmpty() ) @@ -1137,17 +1138,18 @@ void MerginApi::migrateProjectToMergin( const QString &projectName, const QStrin void MerginApi::detachProjectFromMergin( const QString &projectNamespace, const QString &projectName ) { - // remove mergin folder + // Remove mergin folder QString projectFullName = getFullProjectName( projectNamespace, projectName ); - LocalProjectInfo projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject_future projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + if ( projectInfo.isValid() ) { - QDir merginProjectDir( projectInfo.projectDir + "/.mergin" ); - merginProjectDir.removeRecursively(); + InputUtils::removeDir( projectInfo.projectDir + "/.mergin" ); } - // Update localProjects (updating mMerginProjects can be omitted since it is updated on listing projects) - mLocalProjects.resetMerginInfo( projectNamespace, projectName ); - mLocalProjects.reloadProjectDir(); + + // Update localProject + mLocalProjects.updateNamespace( projectInfo.projectDir, "" ); + mLocalProjects.updateLocalVersion( projectInfo.projectDir, -1 ); emit notify( tr( "Project detached from Mergin" ) ); emit projectDetached( projectFullName ); @@ -2438,7 +2440,9 @@ void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSucc writeData( transaction.projectMetadata, transaction.projectDir + "/" + MerginApi::sMetadataFile ); // update info of local projects - mLocalProjects.updateMerginLocalVersion( transaction.projectDir, transaction.version ); + mLocalProjects.updateLocalVersion( transaction.projectDir, transaction.version ); + + // TODO: emit server version mLocalProjects.updateMerginServerVersion( transaction.projectDir, transaction.version ); InputUtils::log( "sync " + projectFullName, QStringLiteral( "### Finished ### New project version: %1\n" ).arg( transaction.version ) ); diff --git a/app/merginapi.h b/app/merginapi.h index 6870fca70..6c99c31fc 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -444,6 +444,7 @@ class MerginApi: public QObject void uploadCanceled( const QString &projectFullName, bool result ); void projectDataChanged( const QString &projectFullName ); void projectDetached( const QString &projectFullName ); + void projectAttachedToMergin( const QString &projectFullName ); private slots: void listProjectsReplyFinished( QString requestId ); diff --git a/app/merginprojectstatusmodel.cpp b/app/merginprojectstatusmodel.cpp index 3c1d81742..335ec99a1 100644 --- a/app/merginprojectstatusmodel.cpp +++ b/app/merginprojectstatusmodel.cpp @@ -125,7 +125,7 @@ void MerginProjectStatusModel::infoProjectUpdated( const ProjectDiff &projectDif bool MerginProjectStatusModel::loadProjectInfo( const QString &projectFullName ) { - LocalProjectInfo projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject_future projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); if ( !projectInfo.projectDir.isEmpty() ) { ProjectDiff diff = MerginApi::localProjectChanges( projectInfo.projectDir ); diff --git a/app/project_future.cpp b/app/project_future.cpp index b937b568d..6734b3553 100644 --- a/app/project_future.cpp +++ b/app/project_future.cpp @@ -22,12 +22,12 @@ void LocalProject_future::copyValues( const LocalProject_future &other ) localVersion = other.localVersion; } -QString MerginProject_future::id() +QString MerginProject_future::id() const { return MerginApi::getFullProjectName( projectNamespace, projectName ); } -QString LocalProject_future::id() +QString LocalProject_future::id() const { if ( !projectName.isEmpty() && !projectNamespace.isEmpty() ) return MerginApi::getFullProjectName( projectNamespace, projectName ); diff --git a/app/project_future.h b/app/project_future.h index 1f0b17668..f76c1f879 100644 --- a/app/project_future.h +++ b/app/project_future.h @@ -33,7 +33,7 @@ struct LocalProject_future QString projectName; QString projectNamespace; - QString id(); //! projectFullName for time being + QString id() const; //! projectFullName for time being QString projectDir; QString projectError; // Error that leads to project not being able to open in app @@ -43,6 +43,8 @@ struct LocalProject_future int localVersion = -1; void copyValues( const LocalProject_future &other ); + + bool isValid() { return !projectDir.isEmpty(); } }; struct MerginProject_future @@ -53,7 +55,7 @@ struct MerginProject_future QString projectName; QString projectNamespace; - QString id(); //! projectFullName for time being + QString id() const; //! projectFullName for time being QDateTime serverUpdated; // available latest version of project files on server int serverVersion; diff --git a/app/projectsmodel_future.cpp b/app/projectsmodel_future.cpp index c3b200078..52e732be5 100644 --- a/app/projectsmodel_future.cpp +++ b/app/projectsmodel_future.cpp @@ -43,6 +43,7 @@ ProjectsModel_future::ProjectsModel_future( } // QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectAdded, this, &ProjectsModel_future::onProjectAdded ); + QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectDataChanged, this, &ProjectsModel_future::onProjectDataChanged ); } QVariant ProjectsModel_future::data( const QModelIndex &index, int role ) const @@ -331,20 +332,34 @@ void ProjectsModel_future::onProjectSyncProgressChanged( const QString &projectF qDebug() << "PMR: Project " << projectFullName << " changed sync progress to " << progress; } -void ProjectsModel_future::onProjectAdded( const LocalProject_future &project ) +void ProjectsModel_future::onProjectAdded( const QString &projectDir ) { - std::shared_ptr newProject = std::shared_ptr( new Project_future() ); - newProject->local = std::unique_ptr( new LocalProject_future( project ) ); + Q_UNUSED( projectDir ) + qDebug() << "PMR: Added project" << projectDir; } void ProjectsModel_future::onProjectDeleted( const QString &projectFullName ) { Q_UNUSED( projectFullName ) + qDebug() << "PMR: Deleted project" << projectFullName; +} + +void ProjectsModel_future::onProjectDataChanged( const QString &projectDir ) +{ + Q_UNUSED( projectDir ) + qDebug() << "PMR: Data changed in project" << projectDir; } void ProjectsModel_future::onProjectDetachedFromMergin( const QString &projectFullName ) { Q_UNUSED( projectFullName ) + qDebug() << "PMR: Project detached from mergin " << projectFullName; +} + +void ProjectsModel_future::onProjectAttachedToMergin(const QString &projectFullName) +{ + Q_UNUSED( projectFullName ) + qDebug() << "PMR: Project attached to mergin " << projectFullName; } QString ProjectsModel_future::modelTypeToFlag() const diff --git a/app/projectsmodel_future.h b/app/projectsmodel_future.h index 19ffd583f..4e8dc89fa 100644 --- a/app/projectsmodel_future.h +++ b/app/projectsmodel_future.h @@ -83,11 +83,12 @@ class ProjectsModel_future : public QAbstractListModel void onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); void onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ); - void onProjectAdded( const LocalProject_future &project ); + void onProjectAdded( const QString &projectDir ); void onProjectDeleted( const QString &projectFullName ); + void onProjectDataChanged( const QString &projectDir ); void onProjectDetachedFromMergin( const QString &projectFullName ); - void onProjectAttachedToMergin() {}; + void onProjectAttachedToMergin( const QString &projectFullName ); private: diff --git a/app/test/testmerginapi.cpp b/app/test/testmerginapi.cpp index 928cf5903..539ebc258 100644 --- a/app/test/testmerginapi.cpp +++ b/app/test/testmerginapi.cpp @@ -160,7 +160,7 @@ void TestMerginApi::testDownloadProject() // check that the local projects are updated QVERIFY( mApi->localProjectsManager().hasMerginProject( mUsername, projectName ) ); - LocalProjectInfo project = mApi->localProjectsManager().projectFromMerginName( projectNamespace, projectName ); + LocalProjectInfo project = mApi->localProjectsManager().projectFromFullName( projectNamespace, projectName ); QVERIFY( project.isValid() ); QCOMPARE( project.projectDir, mApi->projectsPath() + "/" + projectName ); QCOMPARE( project.serverVersion, 1 ); @@ -253,7 +253,7 @@ void TestMerginApi::createRemoteProject( MerginApi *api, const QString &projectN QCOMPARE( info.size(), 0 ); QVERIFY( dir.isEmpty() ); - api->localProjectsManager().removeProject( projectDir ); + api->localProjectsManager().removeLocalProject( projectDir ); QCOMPARE( QFileInfo( projectDir ).size(), 0 ); QVERIFY( QDir( projectDir ).isEmpty() ); @@ -1347,7 +1347,7 @@ void TestMerginApi::testMigrateProject() createLocalProject( projectDir ); // reload localmanager after copying the project - mApi->mLocalProjects.reloadProjectDir(); + mApi->mLocalProjects.reloadDataDir(); QStringList entryList = QDir( projectDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); // migrate project @@ -1393,7 +1393,7 @@ void TestMerginApi::testMigrateProjectAndSync() // step 1 createLocalProject( projectDir ); - mApi->mLocalProjects.reloadProjectDir(); + mApi->mLocalProjects.reloadDataDir(); // step 2 QSignalSpy spy( mApi, &MerginApi::projectCreated ); QSignalSpy spy2( mApi, &MerginApi::syncProjectFinished ); @@ -1446,7 +1446,7 @@ void TestMerginApi::testMigrateDetachProject() createLocalProject( projectDir ); // reload localmanager after copying the project - mApi->mLocalProjects.reloadProjectDir(); + mApi->mLocalProjects.reloadDataDir(); // migrate project QSignalSpy spy( mApi, &MerginApi::projectCreated ); @@ -1516,7 +1516,7 @@ void TestMerginApi::deleteLocalProject( MerginApi *api, const QString &projectNa QDir projectDir( project.projectDir ); projectDir.removeRecursively(); - api->localProjectsManager().removeProject( project.projectDir ); + api->localProjectsManager().removeLocalProject( project.projectDir ); } void TestMerginApi::downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ) From e6e4221f3c5c3e52068c9a264a8e997fcee83a26 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Thu, 18 Mar 2021 11:09:04 +0100 Subject: [PATCH 12/53] MerginApi revamp --- app/merginapi.cpp | 50 +++++++++++++++++++++++------------------------ app/merginapi.h | 4 ++-- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/app/merginapi.cpp b/app/merginapi.cpp index 84084f928..d97ce241e 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -941,7 +941,7 @@ QNetworkReply *MerginApi::getProjectInfo( const QString &projectFullName, bool w } int sinceVersion = -1; - LocalProjectInfo projectInfo = getLocalProject( projectFullName ); + LocalProject_future projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); if ( projectInfo.isValid() ) { // let's also fetch the recent history of diffable files @@ -1066,7 +1066,7 @@ QString MerginApi::extractServerErrorMsg( const QByteArray &data ) } -LocalProjectInfo MerginApi::getLocalProject( const QString &projectFullName ) +LocalProject_future MerginApi::getLocalProject( const QString &projectFullName ) { return mLocalProjects.projectFromMerginName( projectFullName ); } @@ -1242,15 +1242,15 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) // for any local projects we can update the latest server version // TODO: this should now be done inside model so no need to do it here (LocalProjects do not have server version anymore) - for ( MerginProjectListEntry project : mRemoteProjects ) - { - QString fullProjectName = getFullProjectName( project.projectNamespace, project.projectName ); - LocalProjectInfo localProject = mLocalProjects.projectFromMerginName( fullProjectName ); - if ( localProject.isValid() ) - { - mLocalProjects.updateMerginServerVersion( localProject.projectDir, project.version ); - } - } +// for ( MerginProjectListEntry project : mRemoteProjects ) +// { +// QString fullProjectName = getFullProjectName( project.projectNamespace, project.projectName ); +// LocalProjectInfo localProject = mLocalProjects.projectFromMerginName( fullProjectName ); +// if ( localProject.isValid() ) +// { +// mLocalProjects.updateMerginServerVersion( localProject.projectDir, project.version ); +// } +// } InputUtils::log( "list projects", QStringLiteral( "Success - got %1 projects" ).arg( mRemoteProjects.count() ) ); } @@ -1417,7 +1417,7 @@ void MerginApi::finalizeProjectUpdateApplyDiff( const QString &projectFullName, // not good... something went wrong in rebase - we need to save the local changes // let's put them into a conflict file and use the server version - LocalProjectInfo info = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject_future info = mLocalProjects.projectFromMerginName( projectFullName ); QString newDest = InputUtils::findUniquePath( generateConflictFileName( dest, info.localVersion ), false ); if ( !QFile::rename( dest, newDest ) ) { @@ -1471,7 +1471,7 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) { // move local file to conflict file QString origPath = projectDir + "/" + finalizationItem.filePath; - LocalProjectInfo info = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject_future info = mLocalProjects.projectFromMerginName( projectFullName ); QString newPath = InputUtils::findUniquePath( generateConflictFileName( origPath, info.localVersion ), false ); if ( !QFile::rename( origPath, newPath ) ) { @@ -1528,7 +1528,7 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) if ( !QFile::remove( InputUtils::downloadInProgressFilePath( transaction.projectDir ) ) ) InputUtils::log( QStringLiteral( "sync %1" ).arg( projectFullName ), QStringLiteral( "Failed to remove download in progress file for project name %1" ).arg( projectName ) ); - mLocalProjects.addMerginProject( projectDir, projectNamespace, projectName ); + mLocalProjects.addLocalProject( projectDir, projectName, projectNamespace ); } finishProjectSync( projectFullName, true ); @@ -1700,8 +1700,8 @@ void MerginApi::startProjectUpdate( const QString &projectFullName, const QByteA Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); TransactionStatus &transaction = mTransactionalStatus[projectFullName]; - LocalProjectInfo projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); - if ( projectInfo.isValid() ) + LocalProject_future projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + if ( projectInfo.isValid() ) // If project is already downloaded { transaction.projectDir = projectInfo.projectDir; } @@ -1877,20 +1877,20 @@ void MerginApi::uploadInfoReplyFinished() transaction.replyUploadProjectInfo->deleteLater(); transaction.replyUploadProjectInfo = nullptr; - LocalProjectInfo projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject_future projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); transaction.projectDir = projectInfo.projectDir; Q_ASSERT( !transaction.projectDir.isEmpty() ); MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data ); // get the latest server version from our reply (we do not update it in LocalProjectsManager though... I guess we don't need to) - projectInfo.serverVersion = serverProject.version; +// projectInfo.serverVersion = serverProject.version; // now let's figure a key question: are we on the most recent version of the project // if we're about to do upload? because if not, we need to do local update first - if ( projectInfo.isValid() && projectInfo.localVersion != -1 && projectInfo.localVersion < projectInfo.serverVersion ) + if ( projectInfo.isValid() && projectInfo.localVersion != -1 && projectInfo.localVersion < serverProject.version ) { InputUtils::log( "push " + projectFullName, QStringLiteral( "Need pull first: local version %1 | server version %2" ) - .arg( projectInfo.localVersion ).arg( projectInfo.serverVersion ) ); + .arg( projectInfo.localVersion ).arg( serverProject.version ) ); transaction.updateBeforeUpload = true; startProjectUpdate( projectFullName, data ); return; @@ -1899,7 +1899,7 @@ void MerginApi::uploadInfoReplyFinished() QList localFiles = getLocalProjectFiles( transaction.projectDir + "/" ); MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile ); - mLocalProjects.updateMerginServerVersion( transaction.projectDir, serverProject.version ); +// mLocalProjects.updateMerginServerVersion( transaction.projectDir, serverProject.version ); transaction.diff = compareProjectFiles( oldServerProject.files, serverProject.files, localFiles, transaction.projectDir ); InputUtils::log( "push " + projectFullName, transaction.diff.dump() ); @@ -2308,7 +2308,7 @@ MerginProjectListEntry MerginApi::parseProjectMetadata( const QJsonObject &proj } if ( proj.contains( QStringLiteral( "error" ) ) ) { - // TODO: handle project error (might be orphaned project) + // TODO: handle project error (user might be logged out / do not have write rights / project is on different server / project is orphaned) proj.value( QStringLiteral( "error" ) ).toInt( 0 ); // error code return project; @@ -2442,8 +2442,9 @@ void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSucc // update info of local projects mLocalProjects.updateLocalVersion( transaction.projectDir, transaction.version ); - // TODO: emit server version - mLocalProjects.updateMerginServerVersion( transaction.projectDir, transaction.version ); +// mLocalProjects.updateMerginServerVersion( transaction.projectDir, transaction.version ); +// TODO: Is it neccessary to update server version at all? +// emit updateServerVersion( transaction.projectDir, transaction.version ); InputUtils::log( "sync " + projectFullName, QStringLiteral( "### Finished ### New project version: %1\n" ).arg( transaction.version ) ); } @@ -2481,7 +2482,6 @@ void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSucc } } } - } bool MerginApi::writeData( const QByteArray &data, const QString &path ) diff --git a/app/merginapi.h b/app/merginapi.h index 6c99c31fc..ad46984e6 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -331,7 +331,7 @@ class MerginApi: public QObject Q_INVOKABLE void detachProjectFromMergin( const QString &projectNamespace, const QString &projectName ); - LocalProjectInfo getLocalProject( const QString &projectFullName ); + LocalProject_future getLocalProject( const QString &projectFullName ); // Test function static const int MERGIN_API_VERSION_MAJOR = 2020; static const int MERGIN_API_VERSION_MINOR = 4; @@ -502,7 +502,7 @@ class MerginApi: public QObject void sendUploadCancelRequest( const QString &projectFullName, const QString &transactionUUID ); bool writeData( const QByteArray &data, const QString &path ); - void createPathIfNotExists( const QString &filePath ); + void createPathIfNotExists( const QString &filePath ); // TODO: make static and move to InputUtils static QByteArray getChecksum( const QString &filePath ); static QSet listFiles( const QString &projectPath ); From c402d8c63fb06f7a5610610bed0c210c87c7ccb0 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Mon, 22 Mar 2021 10:08:56 +0100 Subject: [PATCH 13/53] add local/remote projects --- app/localprojectsmanager.cpp | 40 ++++++++++++++++-------------------- app/localprojectsmanager.h | 8 +++++--- app/merginapi.cpp | 2 +- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/app/localprojectsmanager.cpp b/app/localprojectsmanager.cpp index fece7d997..e678556da 100644 --- a/app/localprojectsmanager.cpp +++ b/app/localprojectsmanager.cpp @@ -26,7 +26,7 @@ void LocalProjectsManager::reloadDataDir() // TODO: maybe add function to reload { mProjects.clear(); QStringList entryList = QDir( mDataDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); - for ( QString folderName : entryList ) + for ( const QString &folderName : entryList ) { LocalProject_future info; info.projectDir = mDataDir + "/" + folderName; @@ -108,30 +108,15 @@ LocalProject_future LocalProjectsManager::projectFromMerginName( const QString & // Q_ASSERT( false ); // should not happen //} -void LocalProjectsManager::addLocalProject( const QString &projectDir, const QString &projectName, const QString &projectNamespace ) +void LocalProjectsManager::addLocalProject( const QString &projectDir, const QString &projectName ) { - LocalProject_future project; - project.projectDir = projectDir; - project.qgisProjectFilePath = findQgisProjectFile( projectDir, project.projectError ); - project.projectNamespace = projectNamespace; - project.projectName = projectName; - - mProjects << project; - emit localProjectAdded( projectDir ); + addProject( projectDir, QString(), projectName ); } -//void LocalProjectsManager::addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ) -//{ -// addProject( projectDir, projectNamespace, projectName ); -// // version info and status should be updated afterwards -// emit localMerginProjectAdded( projectDir ); -//} - -//void LocalProjectsManager::addLocalProject( const QString &projectDir, const QString &projectName ) -//{ -// addProject( projectDir, QString(), projectName ); -// emit localProjectAdded( projectDir ); -//} +void LocalProjectsManager::addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ) +{ + addProject( projectDir, projectNamespace, projectName ); +} void LocalProjectsManager::removeLocalProject( const QString &projectDir ) { @@ -258,6 +243,17 @@ QString LocalProjectsManager::findQgisProjectFile( const QString &projectDir, QS return QString(); } +void LocalProjectsManager::addProject(const QString &projectDir, const QString &projectNamespace, const QString &projectName) +{ + LocalProject_future project; + project.projectDir = projectDir; + project.qgisProjectFilePath = findQgisProjectFile( projectDir, project.projectError ); + project.projectName = projectName; + project.projectNamespace = projectNamespace; + + mProjects << project; + emit localProjectAdded( projectDir ); +} static QDateTime _getLastModifiedFileDateTime( const QString &path ) { diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index 23f86c36c..c1b08bd98 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -79,15 +79,15 @@ class LocalProjectsManager : public QObject LocalProject_future projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const; //! Adds entry about newly created project - void addLocalProject( const QString &projectDir, const QString &projectName, const QString &projectNamespace = QString() ); + void addLocalProject( const QString &projectDir, const QString &projectName ); // bool hasMerginProject( const QString &projectFullName ) const; // bool hasMerginProject( const QString &projectNamespace, const QString &projectName ) const; // void updateProjectStatus( const QString &projectDir ); - //! Should add an entry about newly created Mergin project -// void addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); + //! Adds entry for downloaded project + void addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); //! Should add an entry about newly created local project // void addLocalProject( const QString &projectDir, const QString &projectName ); @@ -134,6 +134,8 @@ class LocalProjectsManager : public QObject // void updateProjectStatus( LocalProject_future &project ); // TODO: local project manager should not have status private: + void addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); + QString mDataDir; //!< directory with all local projects QList mProjects; }; diff --git a/app/merginapi.cpp b/app/merginapi.cpp index d97ce241e..2996ed93d 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -1528,7 +1528,7 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) if ( !QFile::remove( InputUtils::downloadInProgressFilePath( transaction.projectDir ) ) ) InputUtils::log( QStringLiteral( "sync %1" ).arg( projectFullName ), QStringLiteral( "Failed to remove download in progress file for project name %1" ).arg( projectName ) ); - mLocalProjects.addLocalProject( projectDir, projectName, projectNamespace ); + mLocalProjects.addMerginProject( projectDir, projectNamespace, projectName ); } finishProjectSync( projectFullName, true ); From 8442621a56ce7c0fc1739e3d6098ceee0c12b9db Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Mon, 22 Mar 2021 10:11:54 +0100 Subject: [PATCH 14/53] replace MerginProjectEntry with new structure --- app/localprojectsmanager.h | 4 +- app/merginapi.cpp | 54 ++++++++++----------- app/merginapi.h | 47 +++++++++--------- app/project_future.cpp | 10 ---- app/project_future.h | 28 +++++++++-- app/projectsmodel_future.cpp | 93 +++++++++++++----------------------- app/projectsmodel_future.h | 8 ++-- 7 files changed, 112 insertions(+), 132 deletions(-) diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index c1b08bd98..19e16f206 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -67,7 +67,7 @@ class LocalProjectsManager : public QObject QString dataDir() const { return mDataDir; } - QList projects() const { return mProjects; } + LocalProjectsList projects() const { return mProjects; } //! Loads all projects from mDataDir, removes all old projects void reloadDataDir(); @@ -137,7 +137,7 @@ class LocalProjectsManager : public QObject void addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); QString mDataDir; //!< directory with all local projects - QList mProjects; + LocalProjectsList mProjects; }; diff --git a/app/merginapi.cpp b/app/merginapi.cpp index 2996ed93d..fb9092121 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -1189,10 +1189,10 @@ QString MerginApi::merginUserName() const return userAuth()->username(); } -MerginProjectList MerginApi::projects() -{ - return mRemoteProjects; -} +//MerginProjectList MerginApi::projects() +//{ +// return mRemoteProjects; +//} QList MerginApi::getLocalProjectFiles( const QString &projectPath ) { @@ -1221,6 +1221,7 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) int projectCount = -1; int requestedPage = 1; + MerginProjectsList projectList; if ( r->error() == QNetworkReply::NoError ) { @@ -1233,12 +1234,12 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) if ( doc.isObject() ) { projectCount = doc.object().value( "count" ).toInt(); - mRemoteProjects = parseProjectsFromJson( doc ); - } - else - { - mRemoteProjects.clear(); + projectList = parseProjectsFromJson( doc ); } +// else +// { +// mRemoteProjects.clear(); +// } // for any local projects we can update the latest server version // TODO: this should now be done inside model so no need to do it here (LocalProjects do not have server version anymore) @@ -1252,7 +1253,7 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) // } // } - InputUtils::log( "list projects", QStringLiteral( "Success - got %1 projects" ).arg( mRemoteProjects.count() ) ); + InputUtils::log( "list projects", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) ); } else { @@ -1260,14 +1261,14 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjects" ), r->errorString(), serverMsg ); emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjects" ) ); InputUtils::log( "list projects", QStringLiteral( "FAILED - %1" ).arg( message ) ); - mRemoteProjects.clear(); +// mRemoteProjects.clear(); emit listProjectsFailed(); } r->deleteLater(); - emit listProjectsFinished( mRemoteProjects, mTransactionalStatus, projectCount, requestedPage, requestId ); + emit listProjectsFinished( projectList, mTransactionalStatus, projectCount, requestedPage, requestId ); } void MerginApi::listProjectsByNameReplyFinished( QString requestId ) @@ -1276,20 +1277,13 @@ void MerginApi::listProjectsByNameReplyFinished( QString requestId ) Q_ASSERT( r ); /* TODO: Detect orphaned project? Project that was considered Mergin but did not get info back */ - MerginProjectList projectList; + MerginProjectsList projectList; if ( r->error() == QNetworkReply::NoError ) { QByteArray data = r->readAll(); QJsonDocument json = QJsonDocument::fromJson( data ); - projectList = parseProjectsFromJson( json ); - - for ( MerginProjectListEntry project : qAsConst( projectList ) ) - { - qDebug() << "Project: " << project.projectName; - } - InputUtils::log( "list projects by name", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) ); } else @@ -2298,19 +2292,19 @@ ProjectDiff MerginApi::compareProjectFiles( const QList &oldServerFi return diff; } -MerginProjectListEntry MerginApi::parseProjectMetadata( const QJsonObject &proj ) +MerginProject_future MerginApi::parseProjectMetadata( const QJsonObject &proj ) { - MerginProjectListEntry project; + MerginProject_future project; if ( proj.isEmpty() ) { return project; } + if ( proj.contains( QStringLiteral( "error" ) ) ) { - // TODO: handle project error (user might be logged out / do not have write rights / project is on different server / project is orphaned) - - proj.value( QStringLiteral( "error" ) ).toInt( 0 ); // error code + // handle project error (user might be logged out / do not have write rights / project is on different server / project is orphaned) + project.remoteError = proj.value( QStringLiteral( "error" ) ).toInt( 0 ); // error code return project; } @@ -2320,12 +2314,12 @@ MerginProjectListEntry MerginApi::parseProjectMetadata( const QJsonObject &proj QString versionStr = proj.value( QStringLiteral( "version" ) ).toString(); if ( versionStr.isEmpty() ) { - project.version = 0; + project.serverVersion = 0; } else if ( versionStr.startsWith( "v" ) ) // cut off 'v' part from v123 { versionStr = versionStr.mid( 1 ); - project.version = versionStr.toInt(); + project.serverVersion = versionStr.toInt(); } QDateTime updated = QDateTime::fromString( proj.value( QStringLiteral( "updated" ) ).toString(), Qt::ISODateWithMs ).toUTC(); @@ -2341,13 +2335,13 @@ MerginProjectListEntry MerginApi::parseProjectMetadata( const QJsonObject &proj } -MerginProjectList MerginApi::parseProjectsFromJson( const QJsonDocument &doc ) +MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc ) { if ( !doc.isObject() ) - return MerginProjectList(); + return MerginProjectsList(); QJsonObject object = doc.object(); - MerginProjectList result; + MerginProjectsList result; if ( object.contains( "projects" ) && object.value( "projects" ).isArray() ) // listProjects API { diff --git a/app/merginapi.h b/app/merginapi.h index ad46984e6..620e3cfeb 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -27,6 +27,7 @@ #include "merginsubscriptionstatus.h" #include "merginprojectmetadata.h" #include "localprojectsmanager.h" +#include "project_future.h" class MerginUserAuth; class MerginUserInfo; @@ -166,27 +167,27 @@ struct TransactionStatus }; -struct MerginProjectListEntry // TODO: replace with RemoteProject from Project_future.h -{ - bool isValid() const { return !projectName.isEmpty() && !projectNamespace.isEmpty(); } +//struct MerginProjectListEntry // TODO: replace with RemoteProject from Project_future.h +//{ +// bool isValid() const { return !projectName.isEmpty() && !projectNamespace.isEmpty(); } - QString projectName; - QString projectNamespace; - int version = -1; - QDateTime serverUpdated; // available latest version of project files on server +// QString projectName; +// QString projectNamespace; +// int version = -1; +// QDateTime serverUpdated; // available latest version of project files on server - bool operator ==( const MerginProjectListEntry &other ) - { - return ( this->projectName == other.projectName ) && ( this->projectNamespace == other.projectNamespace ); - } +// bool operator ==( const MerginProjectListEntry &other ) +// { +// return ( this->projectName == other.projectName ) && ( this->projectNamespace == other.projectNamespace ); +// } - bool operator !=( const MerginProjectListEntry &other ) - { - return !( *this == other ); - } -}; +// bool operator !=( const MerginProjectListEntry &other ) +// { +// return !( *this == other ); +// } +//}; -typedef QList MerginProjectList; // TODO: replace with RemoteProject from Project_future.h +//typedef QList MerginProjectList; // TODO: replace with RemoteProject from Project_future.h typedef QHash Transactions; @@ -385,7 +386,7 @@ class MerginApi: public QObject static ProjectDiff compareProjectFiles( const QList &oldServerFiles, const QList &newServerFiles, const QList &localFiles, const QString &projectDir ); //! Returns the most recent list of projects fetched from the server - MerginProjectList projects(); +// MerginProjectList projects(); static QList getLocalProjectFiles( const QString &projectPath ); @@ -413,9 +414,9 @@ class MerginApi: public QObject signals: void apiSupportsSubscriptionsChanged(); - void listProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ); + void listProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ); void listProjectsFailed(); - void listProjectsByNameFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, QString requestId ); + void listProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ); void syncProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); /** * Emitted when sync starts/finishes or the progress changes - useful to give a clue in the GUI about the status. @@ -470,8 +471,8 @@ class MerginApi: public QObject void pingMerginReplyFinished(); private: - MerginProjectListEntry parseProjectMetadata( const QJsonObject &project ); - MerginProjectList parseProjectsFromJson( const QJsonDocument &object ); + MerginProject_future parseProjectMetadata( const QJsonObject &project ); + MerginProjectsList parseProjectsFromJson( const QJsonDocument &object ); static QStringList generateChunkIdsForSize( qint64 fileSize ); QJsonArray prepareUploadChangesJSON( const QList &files ); static QString getApiKey( const QString &serverName ); @@ -568,7 +569,7 @@ class MerginApi: public QObject QNetworkAccessManager mManager; QString mApiRoot; LocalProjectsManager &mLocalProjects; - MerginProjectList mRemoteProjects; // TODO: remove (no use, only in tests - TBD) +// MerginProjectList mRemoteProjects; // TODO: remove (no use, only in tests - TBD) QString mDataDir; // dir with all projects MerginUserInfo *mUserInfo; //owned by this (qml grouped-properties) diff --git a/app/project_future.cpp b/app/project_future.cpp index 6734b3553..c4dccbecf 100644 --- a/app/project_future.cpp +++ b/app/project_future.cpp @@ -12,16 +12,6 @@ #include -void LocalProject_future::copyValues( const LocalProject_future &other ) -{ - projectName = other.projectName; - projectNamespace = other.projectNamespace; - projectDir = other.projectDir; - projectError = other.projectError; - qgisProjectFilePath = other.qgisProjectFilePath; - localVersion = other.localVersion; -} - QString MerginProject_future::id() const { return MerginApi::getFullProjectName( projectNamespace, projectName ); diff --git a/app/project_future.h b/app/project_future.h index f76c1f879..43fc74e3c 100644 --- a/app/project_future.h +++ b/app/project_future.h @@ -21,7 +21,8 @@ enum ProjectStatus_future _UpToDate, //!< both server and local copy are in sync with no extra modifications _OutOfDate, //!< server has newer version than what is available locally (but the project is not modified locally) _Modified, //!< there are some local modifications in the project that need to be pushed (note: also server may have newer version) - // TODO: add orphaned state + + // Maybe orphaned state in future }; Q_ENUMS( ProjectStatus_future ) @@ -42,9 +43,17 @@ struct LocalProject_future int localVersion = -1; - void copyValues( const LocalProject_future &other ); - bool isValid() { return !projectDir.isEmpty(); } + + bool operator ==( const LocalProject_future &other ) + { + return ( this->id() == other.id() ) && ( this->projectDir == other.projectDir ); + } + + bool operator !=( const LocalProject_future &other ) + { + return !( *this == other ); + } }; struct MerginProject_future @@ -67,6 +76,16 @@ struct MerginProject_future // Maybe better use enum or int for error code QString remoteError; // Error leading to project not being able to sync + + bool operator ==( const MerginProject_future &other ) + { + return ( this->id() == other.id() ); + } + + bool operator !=( const MerginProject_future &other ) + { + return !( *this == other ); + } }; struct Project_future @@ -99,4 +118,7 @@ struct Project_future } }; +typedef QList MerginProjectsList; +typedef QList LocalProjectsList; + #endif // PROJECT_FUTURE_H diff --git a/app/projectsmodel_future.cpp b/app/projectsmodel_future.cpp index 52e732be5..3e1b04ee9 100644 --- a/app/projectsmodel_future.cpp +++ b/app/projectsmodel_future.cpp @@ -26,9 +26,6 @@ ProjectsModel_future::ProjectsModel_future( QObject::connect( mBackend, &MerginApi::syncProjectFinished, this, &ProjectsModel_future::onProjectSyncFinished ); QObject::connect( mBackend, &MerginApi::projectDetached, this, &ProjectsModel_future::onProjectDetachedFromMergin ); - // TODO: connect to signals from LocalProjectsManager -// QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectAdded ) - if ( mModelType == ProjectModelTypes::LocalProjectsModel ) { QObject::connect( mBackend, &MerginApi::listProjectsByNameFinished, this, &ProjectsModel_future::onListProjectsByNameFinished ); @@ -42,7 +39,8 @@ ProjectsModel_future::ProjectsModel_future( // Implement RecentProjectsModel type } -// QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectAdded, this, &ProjectsModel_future::onProjectAdded ); + QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectAdded, this, &ProjectsModel_future::onProjectAdded ); + QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectRemoved, this, &ProjectsModel_future::onProjectRemoved ); QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectDataChanged, this, &ProjectsModel_future::onProjectDataChanged ); } @@ -99,52 +97,40 @@ void ProjectsModel_future::listProjectsByName() mLastRequestId = mBackend->listProjectsByName( projectNames() ); } -void ProjectsModel_future::mergeProjects( const MerginProjectList &merginProjects, Transactions pendingProjects ) +void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects ) { - QList localProjects = mLocalProjectsManager.projects(); + LocalProjectsList localProjects = mLocalProjectsManager.projects(); qDebug() << "PMR: mergeProjects(): # of local projects = " << localProjects.size() << " # of mergin projects = " << merginProjects.size(); + + // TODO: clear projects only if this is local project or paginated page == 1 mProjects.clear(); if ( mModelType == ProjectModelTypes::LocalProjectsModel ) { // Keep all local projects and ignore all not downloaded remote projects - for ( auto &localProject : localProjects ) + for ( const auto &localProject : localProjects ) { std::shared_ptr project = std::shared_ptr( new Project_future() ); - std::unique_ptr local = std::unique_ptr( new LocalProject_future() ); - - local->projectName = localProject.projectName; - local->projectNamespace = localProject.projectNamespace; - local->projectDir = localProject.projectDir; - local->projectError = localProject.qgisProjectError; - local->qgisProjectFilePath = localProject.qgisProjectFilePath; - // TODO: later copy data by copy constructor + std::unique_ptr local = std::unique_ptr( new LocalProject_future( localProject ) ); + project->local = std::move( local ); - MerginProjectListEntry remoteEntry; + MerginProject_future remoteEntry; remoteEntry.projectName = project->local->projectName; remoteEntry.projectNamespace = project->local->projectNamespace; if ( merginProjects.contains( remoteEntry ) ) { int i = merginProjects.indexOf( remoteEntry ); - std::unique_ptr mergin = std::unique_ptr( new MerginProject_future() ); - mergin->projectName = merginProjects[i].projectName; - mergin->projectNamespace = merginProjects[i].projectNamespace; - mergin->serverVersion = merginProjects[i].version; - mergin->serverUpdated = merginProjects[i].serverUpdated; - // TODO: later copy data by copy constructor - // TODO: check for project errors (from ListByName API ~> not authorized / no rights / no version) - - if ( pendingProjects.contains( mergin->id() ) ) + project->mergin = std::unique_ptr( new MerginProject_future( merginProjects[i] ) ); + + if ( pendingProjects.contains( project->mergin->id() ) ) { - TransactionStatus projectTransaction = pendingProjects.value( mergin->id() ); - mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; - mergin->pending = true; + TransactionStatus projectTransaction = pendingProjects.value( project->mergin->id() ); + project->mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; + project->mergin->pending = true; } - - project->mergin = std::move( mergin ); } mProjects << project; @@ -156,34 +142,24 @@ void ProjectsModel_future::mergeProjects( const MerginProjectList &merginProject for ( const auto &remoteEntry : merginProjects ) { std::shared_ptr project = std::shared_ptr( new Project_future() ); - std::unique_ptr mergin = std::unique_ptr( new MerginProject_future() ); + project->mergin = std::unique_ptr( new MerginProject_future( remoteEntry ) ); - mergin->projectName = remoteEntry.projectName; - mergin->projectNamespace = remoteEntry.projectNamespace; - // TODO: later copy data by copy constructor - - if ( pendingProjects.contains( mergin->id() ) ) + if ( pendingProjects.contains( project->mergin->id() ) ) { - TransactionStatus projectTransaction = pendingProjects.value( mergin->id() ); - mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; - mergin->pending = true; + TransactionStatus projectTransaction = pendingProjects.value( project->mergin->id() ); + project->mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; + project->mergin->pending = true; } - project->mergin = std::move( mergin ); - // find downloaded projects - LocalProjectInfo localProject; + LocalProject_future localProject; localProject.projectName = project->mergin->projectName; localProject.projectNamespace = project->mergin->projectNamespace; if ( localProjects.contains( localProject ) ) { int ix = localProjects.indexOf( localProject ); - project->local = std::unique_ptr( new LocalProject_future() ); - - project->local->projectName = localProjects[ix].projectName; - project->local->projectNamespace = localProjects[ix].projectNamespace; - // TODO: later copy data by copy constructor + project->local = std::unique_ptr( new LocalProject_future( localProjects[ix] ) ); } mProjects << project; @@ -191,7 +167,7 @@ void ProjectsModel_future::mergeProjects( const MerginProjectList &merginProject } } -void ProjectsModel_future::onListProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ) +void ProjectsModel_future::onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ) { qDebug() << "PMR: onListProjectsFinished(): received response with requestId = " << requestId; if ( mLastRequestId != requestId ) @@ -200,10 +176,11 @@ void ProjectsModel_future::onListProjectsFinished( const MerginProjectList &merg return; } - Q_UNUSED( projectCount ); + // TODO: save projectsCount and paginatedPage to model so that QML can respond accordingly -> show "fetch more" + Q_UNUSED( projectsCount ); Q_UNUSED( page ); - qDebug() << "PMR: onListProjectsFinished(): project count = " << projectCount << " but mergin projects emited: " << merginProjects.size(); + qDebug() << "PMR: onListProjectsFinished(): project count = " << projectsCount << " but mergin projects emited: " << merginProjects.size(); beginResetModel(); mergeProjects( merginProjects, pendingProjects ); @@ -211,7 +188,7 @@ void ProjectsModel_future::onListProjectsFinished( const MerginProjectList &merg endResetModel(); } -void ProjectsModel_future::onListProjectsByNameFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, QString requestId ) +void ProjectsModel_future::onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ) { qDebug() << "PMR: onListProjectsByNameFinished(): received response with requestId = " << requestId; if ( mLastRequestId != requestId ) @@ -220,10 +197,6 @@ void ProjectsModel_future::onListProjectsByNameFinished( const MerginProjectList return; } - Q_UNUSED( merginProjects ); - Q_UNUSED( pendingProjects ); - Q_UNUSED( requestId ); - beginResetModel(); mergeProjects( merginProjects, pendingProjects ); printProjects(); @@ -248,7 +221,7 @@ void ProjectsModel_future::syncProject( QString projectNamespace, QString projec if ( project->mergin->pending ) { - qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "is already "; + qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "is already syncing"; return; } @@ -338,7 +311,7 @@ void ProjectsModel_future::onProjectAdded( const QString &projectDir ) qDebug() << "PMR: Added project" << projectDir; } -void ProjectsModel_future::onProjectDeleted( const QString &projectFullName ) +void ProjectsModel_future::onProjectRemoved( const QString &projectFullName ) { Q_UNUSED( projectFullName ) qDebug() << "PMR: Deleted project" << projectFullName; @@ -394,15 +367,15 @@ void ProjectsModel_future::printProjects() const // TODO: Helper function, remov } } -QStringList ProjectsModel_future::projectNames() const // TODO: use local projects instead +QStringList ProjectsModel_future::projectNames() const { QStringList projectNames; - QList projects = mLocalProjectsManager.projects(); + LocalProjectsList projects = mLocalProjectsManager.projects(); for ( const auto &proj : projects ) { if ( !proj.projectName.isEmpty() && !proj.projectNamespace.isEmpty() ) - projectNames << MerginApi::getFullProjectName( proj.projectNamespace, proj.projectName ); + projectNames << proj.id(); } return projectNames; diff --git a/app/projectsmodel_future.h b/app/projectsmodel_future.h index 4e8dc89fa..915025af1 100644 --- a/app/projectsmodel_future.h +++ b/app/projectsmodel_future.h @@ -75,16 +75,16 @@ class ProjectsModel_future : public QAbstractListModel Q_INVOKABLE void stopProjectSync( QString projectNamespace, QString projectName ); //! Method merging local and remote projects based on the model type - void mergeProjects( const MerginProjectList &merginProjects, Transactions pendingProjects ); + void mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects ); public slots: - void onListProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ); - void onListProjectsByNameFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, QString requestId ); + void onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ); + void onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ); void onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); void onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ); void onProjectAdded( const QString &projectDir ); - void onProjectDeleted( const QString &projectFullName ); + void onProjectRemoved( const QString &projectFullName ); void onProjectDataChanged( const QString &projectDir ); void onProjectDetachedFromMergin( const QString &projectFullName ); From 71ef485d1c316b42083a482042e5642d3bf57871 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Mon, 22 Mar 2021 15:18:42 +0100 Subject: [PATCH 15/53] implement model functions and slots --- app/localprojectsmanager.cpp | 10 +-- app/localprojectsmanager.h | 7 ++- app/project_future.cpp | 12 ++-- app/project_future.h | 24 +++++++- app/projectsmodel_future.cpp | 115 +++++++++++++++++++++++++++++------ app/projectsmodel_future.h | 8 +-- 6 files changed, 140 insertions(+), 36 deletions(-) diff --git a/app/localprojectsmanager.cpp b/app/localprojectsmanager.cpp index e678556da..d07fd58e2 100644 --- a/app/localprojectsmanager.cpp +++ b/app/localprojectsmanager.cpp @@ -46,6 +46,7 @@ void LocalProjectsManager::reloadDataDir() // TODO: maybe add function to reload mProjects << info; } + emit dataDirReloaded(); qDebug() << "LocalProjectsManager: found" << mProjects.size() << "projects"; } @@ -124,10 +125,11 @@ void LocalProjectsManager::removeLocalProject( const QString &projectDir ) { if ( mProjects[i].projectDir == projectDir ) { + emit aboutToRemoveLocalProject( mProjects[i] ); + InputUtils::removeDir( mProjects[i].projectDir ); mProjects.removeAt( i ); - emit localProjectRemoved( projectDir ); return; } } @@ -157,7 +159,7 @@ void LocalProjectsManager::updateLocalVersion( const QString &projectDir, int ve { mProjects[i].localVersion = version; - emit localProjectDataChanged( mProjects[i].projectDir ); + emit localProjectDataChanged( mProjects[i] ); return; } } @@ -199,7 +201,7 @@ void LocalProjectsManager::updateNamespace( const QString &projectDir, const QSt { mProjects[i].projectNamespace = projectNamespace; - emit localProjectDataChanged( mProjects[i].projectDir ); + emit localProjectDataChanged( mProjects[i] ); return; } } @@ -252,7 +254,7 @@ void LocalProjectsManager::addProject(const QString &projectDir, const QString & project.projectNamespace = projectNamespace; mProjects << project; - emit localProjectAdded( projectDir ); + emit localProjectAdded( project ); } static QDateTime _getLastModifiedFileDateTime( const QString &path ) diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index 19e16f206..8fbd44760 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -126,9 +126,10 @@ class LocalProjectsManager : public QObject signals: void projectMetadataChanged( const QString &projectDir ); void localMerginProjectAdded( const QString &projectDir ); - void localProjectAdded( const QString &projectDir ); - void localProjectRemoved( const QString &projectDir ); - void localProjectDataChanged( const QString &projectDir ); + void localProjectAdded( const LocalProject_future &project ); + void aboutToRemoveLocalProject( const LocalProject_future project ); + void localProjectDataChanged( const LocalProject_future &project ); + void dataDirReloaded(); // private: // void updateProjectStatus( LocalProject_future &project ); // TODO: local project manager should not have status diff --git a/app/project_future.cpp b/app/project_future.cpp index c4dccbecf..c278f0272 100644 --- a/app/project_future.cpp +++ b/app/project_future.cpp @@ -10,13 +10,6 @@ #include "project_future.h" #include "merginapi.h" -#include - -QString MerginProject_future::id() const -{ - return MerginApi::getFullProjectName( projectNamespace, projectName ); -} - QString LocalProject_future::id() const { if ( !projectName.isEmpty() && !projectNamespace.isEmpty() ) @@ -25,3 +18,8 @@ QString LocalProject_future::id() const QDir dir( projectDir ); return dir.dirName(); } + +QString MerginProject_future::id() const +{ + return MerginApi::getFullProjectName( projectNamespace, projectName ); +} diff --git a/app/project_future.h b/app/project_future.h index 43fc74e3c..1b4946ac9 100644 --- a/app/project_future.h +++ b/app/project_future.h @@ -11,9 +11,10 @@ #define PROJECT_FUTURE_H #include -#include #include +#include #include +#include enum ProjectStatus_future { @@ -99,6 +100,27 @@ struct Project_future bool isMergin() const { return mergin != nullptr; } bool isLocal() const { return local != nullptr; } + QString projectName() + { + if ( isLocal() ) return local->projectName; + else if ( isMergin() ) return mergin->projectName; + return QString(); + } + + QString projectNamespace() + { + if ( isLocal() ) return local->projectNamespace; + else if ( isMergin() ) return mergin->projectNamespace; + return QString(); + } + + QString projectId() + { + if ( isLocal() ) return local->id(); + else if ( isMergin() ) return mergin->id(); + return QString(); + } + bool operator ==( const Project_future &other ) { if ( this->isLocal() && other.isLocal() ) diff --git a/app/projectsmodel_future.cpp b/app/projectsmodel_future.cpp index 3e1b04ee9..c4cc7a296 100644 --- a/app/projectsmodel_future.cpp +++ b/app/projectsmodel_future.cpp @@ -25,6 +25,8 @@ ProjectsModel_future::ProjectsModel_future( QObject::connect( mBackend, &MerginApi::syncProjectStatusChanged, this, &ProjectsModel_future::onProjectSyncProgressChanged ); QObject::connect( mBackend, &MerginApi::syncProjectFinished, this, &ProjectsModel_future::onProjectSyncFinished ); QObject::connect( mBackend, &MerginApi::projectDetached, this, &ProjectsModel_future::onProjectDetachedFromMergin ); + QObject::connect( mBackend, &MerginApi::projectAttachedToMergin, this, &ProjectsModel_future::onProjectAttachedToMergin ); + if ( mModelType == ProjectModelTypes::LocalProjectsModel ) { @@ -40,7 +42,7 @@ ProjectsModel_future::ProjectsModel_future( } QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectAdded, this, &ProjectsModel_future::onProjectAdded ); - QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectRemoved, this, &ProjectsModel_future::onProjectRemoved ); + QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::aboutToRemoveLocalProject, this, &ProjectsModel_future::onAboutToRemoveProject ); QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectDataChanged, this, &ProjectsModel_future::onProjectDataChanged ); } @@ -49,10 +51,29 @@ QVariant ProjectsModel_future::data( const QModelIndex &index, int role ) const if ( !index.isValid() ) return QVariant(); - std::shared_ptr project = mProjects.at( index.row() ); + std::shared_ptr project = mProjects.at( index.row() ); switch ( role ) { - default: return QVariant("TestData"); + case ProjectName: return QVariant( project->projectName() ); + case ProjectNamespace: return QVariant( project->projectNamespace() ); + case ProjectFullName: return QVariant( project->projectId() ); + case ProjectIsLocal: return QVariant( project->isLocal() ); + case ProjectIsMergin: return QVariant( project->isMergin() ); + case ProjectDescription: { + if ( project->isLocal() && !project->local->projectError.isEmpty() ) + return project->local->projectError; + + QFileInfo fi( project->local->projectDir ); + return fi.lastModified(); // TODO: Better project info + } + default: { + if ( !project->isMergin() ) return QVariant(); + + // Roles only for projects that has mergin part + if ( role == ProjectPending ) return QVariant( project->mergin->pending ); + else if ( role == ProjectSyncProgress ) return QVariant( project->mergin->progress ); + return QVariant(); + } } } @@ -66,7 +87,14 @@ QModelIndex ProjectsModel_future::index( int row, int col, const QModelIndex &pa QHash ProjectsModel_future::roleNames() const { QHash roles; - roles[Roles::ProjectName] = QStringLiteral( "ProjectName" ).toLatin1(); + roles[Roles::ProjectName] = QStringLiteral( "ProjectName" ).toLatin1(); + roles[Roles::ProjectNamespace] = QStringLiteral( "ProjectNamespace" ).toLatin1(); + roles[Roles::ProjectFullName] = QStringLiteral( "ProjectFullName" ).toLatin1(); + roles[Roles::ProjectIsLocal] = QStringLiteral( "ProjectIsLocal" ).toLatin1(); + roles[Roles::ProjectIsMergin] = QStringLiteral( "ProjectIsMergin" ).toLatin1(); + roles[Roles::ProjectDescription] = QStringLiteral( "ProjectDescription" ).toLatin1(); + roles[Roles::ProjectPending] = QStringLiteral( "ProjectPending" ).toLatin1(); + roles[Roles::ProjectSyncProgress] = QStringLiteral( "ProjectSyncProgress" ).toLatin1(); return roles; } @@ -305,33 +333,86 @@ void ProjectsModel_future::onProjectSyncProgressChanged( const QString &projectF qDebug() << "PMR: Project " << projectFullName << " changed sync progress to " << progress; } -void ProjectsModel_future::onProjectAdded( const QString &projectDir ) +void ProjectsModel_future::onProjectAdded( const LocalProject_future &project ) { - Q_UNUSED( projectDir ) - qDebug() << "PMR: Added project" << projectDir; + // Check if such project is already in project list + std::shared_ptr proj = projectFromId( project.id() ); + if ( proj ) + { + // add local information ~ project downloaded + proj->local = std::unique_ptr( new LocalProject_future( project ) ); + + QModelIndex ix = index( mProjects.indexOf( proj ) ); + emit dataChanged( ix, ix ); + } + else if ( mModelType == LocalProjectsModel ) + { + // add project to project list ~ project created + std::shared_ptr newProject = std::shared_ptr( new Project_future() ); + newProject->local = std::unique_ptr( new LocalProject_future( project ) ); + + int insertIndex = mProjects.size() == 0 ? 0 : mProjects.size() - 1; + beginInsertRows( QModelIndex(), insertIndex, insertIndex + 1 ); + mProjects << newProject; + endInsertRows(); + } + + qDebug() << "PMR: Added project" << project.id(); } -void ProjectsModel_future::onProjectRemoved( const QString &projectFullName ) +void ProjectsModel_future::onAboutToRemoveProject( const LocalProject_future project ) { - Q_UNUSED( projectFullName ) - qDebug() << "PMR: Deleted project" << projectFullName; + std::shared_ptr proj = projectFromId( project.id() ); + + if ( proj ) + { + int removeIndex = mProjects.indexOf( proj ); + + beginRemoveRows( QModelIndex(), removeIndex, removeIndex ); + mProjects.removeOne( proj ); + endRemoveRows(); + + qDebug() << "PMR: Deleted project" << project.id(); + } } -void ProjectsModel_future::onProjectDataChanged( const QString &projectDir ) +void ProjectsModel_future::onProjectDataChanged( const LocalProject_future &project ) { - Q_UNUSED( projectDir ) - qDebug() << "PMR: Data changed in project" << projectDir; + std::shared_ptr proj = projectFromId( project.id() ); + + if ( proj ) + { + proj->local = std::unique_ptr( new LocalProject_future( project ) ); + QModelIndex editIndex = index( mProjects.indexOf( proj ) ); + + emit dataChanged( editIndex, editIndex ); + } + qDebug() << "PMR: Data changed in project" << project.id(); } void ProjectsModel_future::onProjectDetachedFromMergin( const QString &projectFullName ) { - Q_UNUSED( projectFullName ) - qDebug() << "PMR: Project detached from mergin " << projectFullName; + std::shared_ptr proj = projectFromId( projectFullName ); + + if ( proj ) + { + proj->mergin = nullptr; + QModelIndex editIndex = index( mProjects.indexOf( proj ) ); + + emit dataChanged( editIndex, editIndex ); + + // This project should also be removed from project list for remote project model types, + // however, currently one needs to click on "My projects/Shared/Explore" and that sends + // another listProjects request. In new list this project will not be shown. + } } -void ProjectsModel_future::onProjectAttachedToMergin(const QString &projectFullName) +void ProjectsModel_future::onProjectAttachedToMergin( const QString &projectFullName ) { - Q_UNUSED( projectFullName ) + // To ensure project will be in sync with server, send listProjectByName request. + // In theory we could send that request only for this one project. + listProjectsByName(); + qDebug() << "PMR: Project attached to mergin " << projectFullName; } diff --git a/app/projectsmodel_future.h b/app/projectsmodel_future.h index 915025af1..d31bed8fa 100644 --- a/app/projectsmodel_future.h +++ b/app/projectsmodel_future.h @@ -49,7 +49,7 @@ class ProjectsModel_future : public QAbstractListModel ProjectIsMergin, ProjectIsLocal, ProjectStatus, - ProjectProgress + ProjectSyncProgress }; Q_ENUMS( Roles ) @@ -83,9 +83,9 @@ class ProjectsModel_future : public QAbstractListModel void onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); void onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ); - void onProjectAdded( const QString &projectDir ); - void onProjectRemoved( const QString &projectFullName ); - void onProjectDataChanged( const QString &projectDir ); + void onProjectAdded( const LocalProject_future &project ); + void onAboutToRemoveProject( const LocalProject_future project ); + void onProjectDataChanged( const LocalProject_future &project ); void onProjectDetachedFromMergin( const QString &projectFullName ); void onProjectAttachedToMergin( const QString &projectFullName ); From a881f39a6227aa8c42ba0e663e1d6e1894c4da5d Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Mon, 22 Mar 2021 16:13:46 +0100 Subject: [PATCH 16/53] add pagination --- app/projectsmodel_future.cpp | 43 ++++++++++++++++++++++++++++-------- app/projectsmodel_future.h | 25 +++++++++++++++------ 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/app/projectsmodel_future.cpp b/app/projectsmodel_future.cpp index c4cc7a296..02fdbd6f1 100644 --- a/app/projectsmodel_future.cpp +++ b/app/projectsmodel_future.cpp @@ -31,6 +31,7 @@ ProjectsModel_future::ProjectsModel_future( if ( mModelType == ProjectModelTypes::LocalProjectsModel ) { QObject::connect( mBackend, &MerginApi::listProjectsByNameFinished, this, &ProjectsModel_future::onListProjectsByNameFinished ); + loadLocalProjects(); // at app start, we need to fill model with local projects } else if ( mModelType != ProjectModelTypes::RecentProjectsModel ) { @@ -44,6 +45,7 @@ ProjectsModel_future::ProjectsModel_future( QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectAdded, this, &ProjectsModel_future::onProjectAdded ); QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::aboutToRemoveLocalProject, this, &ProjectsModel_future::onAboutToRemoveProject ); QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectDataChanged, this, &ProjectsModel_future::onProjectDataChanged ); + QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::dataDirReloaded, this, &ProjectsModel_future::loadLocalProjects ); } QVariant ProjectsModel_future::data( const QModelIndex &index, int role ) const @@ -103,15 +105,16 @@ int ProjectsModel_future::rowCount( const QModelIndex & ) const return mProjects.count(); } -void ProjectsModel_future::listProjects() +void ProjectsModel_future::listProjects( int page, QString searchExpression ) { if ( mModelType == LocalProjectsModel ) { InputUtils::log( "Input", "Can not call listProjects API on LocalProjectsModel" ); + // maybe call listProjectsByName(); return; } - mLastRequestId = mBackend->listProjects( "", modelTypeToFlag(), "", 1 ); //TODO: pagination + mLastRequestId = mBackend->listProjects( "", "" /*modelTypeToFlag()*/, searchExpression, page ); } void ProjectsModel_future::listProjectsByName() @@ -125,14 +128,14 @@ void ProjectsModel_future::listProjectsByName() mLastRequestId = mBackend->listProjectsByName( projectNames() ); } -void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects ) +void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious ) { LocalProjectsList localProjects = mLocalProjectsManager.projects(); qDebug() << "PMR: mergeProjects(): # of local projects = " << localProjects.size() << " # of mergin projects = " << merginProjects.size(); - // TODO: clear projects only if this is local project or paginated page == 1 - mProjects.clear(); + if ( !keepPrevious ) + mProjects.clear(); if ( mModelType == ProjectModelTypes::LocalProjectsModel ) { @@ -195,6 +198,11 @@ void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjec } } +int ProjectsModel_future::serverProjectsCount() const +{ + return mServerProjectsCount; +} + void ProjectsModel_future::onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ) { qDebug() << "PMR: onListProjectsFinished(): received response with requestId = " << requestId; @@ -204,14 +212,12 @@ void ProjectsModel_future::onListProjectsFinished( const MerginProjectsList &mer return; } - // TODO: save projectsCount and paginatedPage to model so that QML can respond accordingly -> show "fetch more" - Q_UNUSED( projectsCount ); - Q_UNUSED( page ); + setServerProjectsCount( projectsCount ); qDebug() << "PMR: onListProjectsFinished(): project count = " << projectsCount << " but mergin projects emited: " << merginProjects.size(); beginResetModel(); - mergeProjects( merginProjects, pendingProjects ); + mergeProjects( merginProjects, pendingProjects, page != 1 ); // throw projects only if paginating first page printProjects(); endResetModel(); } @@ -416,6 +422,15 @@ void ProjectsModel_future::onProjectAttachedToMergin( const QString &projectFull qDebug() << "PMR: Project attached to mergin " << projectFullName; } +void ProjectsModel_future::setServerProjectsCount( int serverProjectsCount ) +{ + if ( mServerProjectsCount == serverProjectsCount ) + return; + + mServerProjectsCount = serverProjectsCount; + emit serverProjectsCountChanged( mServerProjectsCount ); +} + QString ProjectsModel_future::modelTypeToFlag() const { switch ( mModelType ) @@ -462,6 +477,16 @@ QStringList ProjectsModel_future::projectNames() const return projectNames; } +void ProjectsModel_future::loadLocalProjects() +{ + if ( mModelType == LocalProjectsModel ) + { + beginResetModel(); + mergeProjects( MerginProjectsList(), Transactions() ); // Fills model with local projects + endResetModel(); + } +} + bool ProjectsModel_future::containsProject( QString projectId ) const { std::shared_ptr proj = projectFromId( projectId ); diff --git a/app/projectsmodel_future.h b/app/projectsmodel_future.h index d31bed8fa..161e67692 100644 --- a/app/projectsmodel_future.h +++ b/app/projectsmodel_future.h @@ -56,6 +56,8 @@ class ProjectsModel_future : public QAbstractListModel ProjectsModel_future( MerginApi *merginApi, ProjectModelTypes modelType, LocalProjectsManager &localProjectsManager, QObject *parent = nullptr ); ~ProjectsModel_future() override {}; + Q_PROPERTY( int serverProjectsCount READ serverProjectsCount WRITE setServerProjectsCount NOTIFY serverProjectsCountChanged ); + // Needed methods from QAbstractListModel Q_INVOKABLE QVariant data( const QModelIndex &index, int role ) const override; Q_INVOKABLE QModelIndex index( int row, int column = 0, const QModelIndex &parent = QModelIndex() ) const override; @@ -63,7 +65,7 @@ class ProjectsModel_future : public QAbstractListModel int rowCount( const QModelIndex &parent = QModelIndex() ) const override; //! Called to list projects, either fetch more or get first - Q_INVOKABLE void listProjects(); + Q_INVOKABLE void listProjects( int page = 1, const QString searchExpression = QString() ); //! Called to list projects, either fetch more or get first Q_INVOKABLE void listProjectsByName(); @@ -75,26 +77,35 @@ class ProjectsModel_future : public QAbstractListModel Q_INVOKABLE void stopProjectSync( QString projectNamespace, QString projectName ); //! Method merging local and remote projects based on the model type - void mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects ); + void mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious = false ); + + int serverProjectsCount() const; - public slots: +public slots: + // MerginAPI - backend signals void onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ); void onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ); void onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); void onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ); + void onProjectDetachedFromMergin( const QString &projectFullName ); + void onProjectAttachedToMergin( const QString &projectFullName ); + // LocalProjectsManager signals void onProjectAdded( const LocalProject_future &project ); void onAboutToRemoveProject( const LocalProject_future project ); void onProjectDataChanged( const LocalProject_future &project ); - void onProjectDetachedFromMergin( const QString &projectFullName ); - void onProjectAttachedToMergin( const QString &projectFullName ); + void setServerProjectsCount( int serverProjectsCount ); + +signals: + void serverProjectsCountChanged( int serverProjectsCount ); - private: +private: QString modelTypeToFlag() const; void printProjects() const; QStringList projectNames() const; + void loadLocalProjects(); bool containsProject( QString projectId ) const; std::shared_ptr projectFromId( QString projectId ) const; @@ -106,7 +117,7 @@ class ProjectsModel_future : public QAbstractListModel ProjectModelTypes mModelType; //! For pagination - int mPopulatedPage = -1; // -> on the fly in QML:: QML should pass this to model + int mServerProjectsCount = -1; //! For processing only my requests QString mLastRequestId; From c43d72d31d6dc1db0754e40abb95dfe65d05e07a Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Mon, 22 Mar 2021 17:11:09 +0100 Subject: [PATCH 17/53] project status and remote error --- app/localprojectsmanager.cpp | 14 +++++++------- app/localprojectsmanager.h | 2 +- app/projectsmodel_future.cpp | 7 +++++-- app/projectsmodel_future.h | 4 +++- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/localprojectsmanager.cpp b/app/localprojectsmanager.cpp index d07fd58e2..f8b9ad5aa 100644 --- a/app/localprojectsmanager.cpp +++ b/app/localprojectsmanager.cpp @@ -290,13 +290,13 @@ static int _getProjectFilesCount( const QString &path ) return count; } -ProjectStatus_future LocalProjectsManager::currentProjectStatus( const Project_future &project ) +ProjectStatus_future LocalProjectsManager::currentProjectStatus( const std::shared_ptr project ) { - if ( !project.isMergin() || !project.isLocal() ) // This is not a Mergin project or not downloaded project + if ( !project || !project->isMergin() || !project->isLocal() ) // This is not a Mergin project or not downloaded project return ProjectStatus_future::_NoVersion; // There was no sync yet - if ( project.local->localVersion < 0 ) + if ( project->local->localVersion < 0 ) { return ProjectStatus_future::_NoVersion; } @@ -306,18 +306,18 @@ ProjectStatus_future LocalProjectsManager::currentProjectStatus( const Project_f // // Something has locally changed after last sync with server - QString metadataFilePath = project.local->projectDir + "/" + MerginApi::sMetadataFile; - QDateTime lastModified = _getLastModifiedFileDateTime( project.local->projectDir ); + QString metadataFilePath = project->local->projectDir + "/" + MerginApi::sMetadataFile; + QDateTime lastModified = _getLastModifiedFileDateTime( project->local->projectDir ); QDateTime lastSync = QFileInfo( metadataFilePath ).lastModified(); MerginProjectMetadata meta = MerginProjectMetadata::fromCachedJson( metadataFilePath ); - int filesCount = _getProjectFilesCount( project.local->projectDir ); + int filesCount = _getProjectFilesCount( project->local->projectDir ); if ( lastSync < lastModified || meta.files.count() != filesCount ) { return ProjectStatus_future::_Modified; } // Version is lower than latest one, last sync also before updated - if ( project.local->localVersion < project.mergin->serverVersion ) + if ( project->local->localVersion < project->mergin->serverVersion ) { return ProjectStatus_future::_OutOfDate; } diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index 8fbd44760..4a601aae5 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -121,7 +121,7 @@ class LocalProjectsManager : public QObject QString findQgisProjectFile( const QString &projectDir, QString &err ); - static ProjectStatus_future currentProjectStatus( const Project_future &project ); // TODO: maybe move somewhere else? + static ProjectStatus_future currentProjectStatus( const std::shared_ptr project ); // TODO: maybe move somewhere else? signals: void projectMetadataChanged( const QString &projectDir ); diff --git a/app/projectsmodel_future.cpp b/app/projectsmodel_future.cpp index 02fdbd6f1..000bf1ad9 100644 --- a/app/projectsmodel_future.cpp +++ b/app/projectsmodel_future.cpp @@ -61,12 +61,14 @@ QVariant ProjectsModel_future::data( const QModelIndex &index, int role ) const case ProjectFullName: return QVariant( project->projectId() ); case ProjectIsLocal: return QVariant( project->isLocal() ); case ProjectIsMergin: return QVariant( project->isMergin() ); + case ProjectStatus: return QVariant( LocalProjectsManager::currentProjectStatus( project ) ); + case ProjectIsValid: return QVariant( !project->isLocal() || ( project->isLocal() && project->local->projectError.isEmpty() ) ); case ProjectDescription: { if ( project->isLocal() && !project->local->projectError.isEmpty() ) return project->local->projectError; QFileInfo fi( project->local->projectDir ); - return fi.lastModified(); // TODO: Better project info + return fi.lastModified(); } default: { if ( !project->isMergin() ) return QVariant(); @@ -74,6 +76,7 @@ QVariant ProjectsModel_future::data( const QModelIndex &index, int role ) const // Roles only for projects that has mergin part if ( role == ProjectPending ) return QVariant( project->mergin->pending ); else if ( role == ProjectSyncProgress ) return QVariant( project->mergin->progress ); + else if ( role == ProjectRemoteError ) return QVariant( project->mergin->remoteError ); return QVariant(); } } @@ -114,7 +117,7 @@ void ProjectsModel_future::listProjects( int page, QString searchExpression ) return; } - mLastRequestId = mBackend->listProjects( "", "" /*modelTypeToFlag()*/, searchExpression, page ); + mLastRequestId = mBackend->listProjects( "", modelTypeToFlag(), searchExpression, page ); } void ProjectsModel_future::listProjectsByName() diff --git a/app/projectsmodel_future.h b/app/projectsmodel_future.h index 161e67692..456f80298 100644 --- a/app/projectsmodel_future.h +++ b/app/projectsmodel_future.h @@ -48,8 +48,10 @@ class ProjectsModel_future : public QAbstractListModel ProjectPending, ProjectIsMergin, ProjectIsLocal, + ProjectIsValid, ProjectStatus, - ProjectSyncProgress + ProjectSyncProgress, + ProjectRemoteError }; Q_ENUMS( Roles ) From 0187537ac38299b66957a6a2eb7855961280157a Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Tue, 23 Mar 2021 11:54:29 +0100 Subject: [PATCH 18/53] Proxy models --- app/main.cpp | 30 ++++++++++-- app/projectsmodel_future.cpp | 5 ++ app/projectsmodel_future.h | 2 + app/projectsproxymodel_future.cpp | 77 +++++++++++++++++++++++++++++-- app/projectsproxymodel_future.h | 18 ++++++-- 5 files changed, 121 insertions(+), 11 deletions(-) diff --git a/app/main.cpp b/app/main.cpp index 13d58cf9f..c85e09b3a 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -55,6 +55,9 @@ #include "codefilter.h" #include "inputexpressionfunctions.h" +#include "projectsmodel_future.h" +#include "projectsproxymodel_future.h" + #ifdef INPUT_TEST #include "test/testmerginapi.h" #include "test/testlinks.h" @@ -231,6 +234,9 @@ void initDeclarative() qmlRegisterType( "lc", 1, 0, "PositionDirection" ); qmlRegisterType( "lc", 1, 0, "FieldsModel" ); qmlRegisterType( "lc", 1, 0, "CodeFilter" ); + + qmlRegisterUncreatableType( "lc", 1, 0, "ProjectsModelF", "" ); + qmlRegisterUncreatableType( "lc", 1, 0, "ProjectsProxyModelF", "" ); } #ifdef INPUT_TEST @@ -368,6 +374,17 @@ int main( int argc, char *argv[] ) InputHelp help( ma.get(), &iu ); ProjectWizard pw( projectDir ); + // project models - instance for each category + ProjectsModel_future myProjectsModel( ma.get(), ProjectModelTypes::MyProjectsModel, localProjects ); + ProjectsModel_future localProjectsModel( ma.get(), ProjectModelTypes::LocalProjectsModel, localProjects ); + ProjectsModel_future sharedProjectsModel( ma.get(), ProjectModelTypes::SharedProjectsModel, localProjects ); + ProjectsModel_future exploreProjectsModel( ma.get(), ProjectModelTypes::ExploreProjectsModel, localProjects ); + + ProjectsProxyModel_future myProjectsProxyModel( &myProjectsModel ); + ProjectsProxyModel_future localProjectsProxyModel( &localProjectsModel ); + ProjectsProxyModel_future sharedProjectsProxyModel( &sharedProjectsModel ); + ProjectsProxyModel_future exploreProjectsProxyModel( &exploreProjectsModel ); + // layer models LayersModel lm; LayersProxyModel browseLpm( &lm, LayerModelTypes::BrowseDataLayerSelection ); @@ -383,11 +400,11 @@ int main( int argc, char *argv[] ) // Connections QObject::connect( &app, &QGuiApplication::applicationStateChanged, &loader, &Loader::appStateChanged ); QObject::connect( &app, &QCoreApplication::aboutToQuit, &loader, &Loader::appAboutToQuit ); - QObject::connect( ma.get(), &MerginApi::syncProjectFinished, &pm, &ProjectModel::syncedProjectFinished ); - QObject::connect( ma.get(), &MerginApi::projectDetached, &pm, &ProjectModel::findProjectFiles ); +// QObject::connect( ma.get(), &MerginApi::syncProjectFinished, &pm, &ProjectModel::syncedProjectFinished ); +// QObject::connect( ma.get(), &MerginApi::projectDetached, &pm, &ProjectModel::findProjectFiles ); QObject::connect( &pw, &ProjectWizard::projectCreated, &localProjects, &LocalProjectsManager::addLocalProject ); - QObject::connect( ma.get(), &MerginApi::listProjectsFinished, &mpm, &MerginProjectModel::updateModel ); - QObject::connect( ma.get(), &MerginApi::syncProjectStatusChanged, &mpm, &MerginProjectModel::syncProjectStatusChanged ); +// QObject::connect( ma.get(), &MerginApi::listProjectsFinished, &mpm, &MerginProjectModel::updateModel ); +// QObject::connect( ma.get(), &MerginApi::syncProjectStatusChanged, &mpm, &MerginProjectModel::syncProjectStatusChanged ); QObject::connect( ma.get(), &MerginApi::reloadProject, &loader, &Loader::reloadProject ); QObject::connect( &mtm, &MapThemesModel::mapThemeChanged, &recordingLpm, &LayersProxyModel::onMapThemeChanged ); QObject::connect( &loader, &Loader::projectReloaded, vm.get(), &VariablesManager::merginProjectChanged ); @@ -488,6 +505,11 @@ int main( int argc, char *argv[] ) engine.rootContext()->setContextProperty( "__purchasing", purchasing.get() ); engine.rootContext()->setContextProperty( "__projectWizard", &pw ); + engine.rootContext()->setContextProperty( "__myProjectsModel", &myProjectsModel ); // TODO: maybe project models do not need to be exposed? + engine.rootContext()->setContextProperty( "__localProjectsModel", &localProjectsModel ); + engine.rootContext()->setContextProperty( "__myProjectsProxyModel", &myProjectsProxyModel ); + engine.rootContext()->setContextProperty( "__localProjectsProxyModel", &localProjectsProxyModel ); + #ifdef MOBILE_OS engine.rootContext()->setContextProperty( "__appwindowvisibility", QWindow::Maximized ); engine.rootContext()->setContextProperty( "__appwindowwidth", QVariant( 0 ) ); diff --git a/app/projectsmodel_future.cpp b/app/projectsmodel_future.cpp index 000bf1ad9..046010619 100644 --- a/app/projectsmodel_future.cpp +++ b/app/projectsmodel_future.cpp @@ -508,3 +508,8 @@ std::shared_ptr ProjectsModel_future::projectFromId( QString pro return nullptr; } + +ProjectModelTypes ProjectsModel_future::modelType() const +{ + return mModelType; +} diff --git a/app/projectsmodel_future.h b/app/projectsmodel_future.h index 456f80298..9000409e9 100644 --- a/app/projectsmodel_future.h +++ b/app/projectsmodel_future.h @@ -83,6 +83,8 @@ class ProjectsModel_future : public QAbstractListModel int serverProjectsCount() const; + ProjectModelTypes modelType() const; + public slots: // MerginAPI - backend signals void onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ); diff --git a/app/projectsproxymodel_future.cpp b/app/projectsproxymodel_future.cpp index fcbab74b3..398c704c4 100644 --- a/app/projectsproxymodel_future.cpp +++ b/app/projectsproxymodel_future.cpp @@ -9,15 +9,84 @@ #include "projectsproxymodel_future.h" -ProjectsProxyModel_future::ProjectsProxyModel_future( ProjectModelTypes modelType, QObject *parent ) : +ProjectsProxyModel_future::ProjectsProxyModel_future( ProjectsModel_future *projectsSourceModel, QObject *parent ) : QSortFilterProxyModel( parent ), - mModelType( modelType ) + mModel( projectsSourceModel ) { + setSourceModel( mModel ); + mModelType = mModel->modelType(); + setSortRole( ProjectsModel_future::Roles::ProjectFullName ); + setSortCaseSensitivity( Qt::CaseSensitivity::CaseInsensitive ); } -bool ProjectsProxyModel_future::filterAcceptsRow( int, const QModelIndex & ) const +QString ProjectsProxyModel_future::searchExpression() const +{ + return mSearchExpression; +} + +void ProjectsProxyModel_future::setSearchExpression( QString searchExpression ) +{ + if ( mSearchExpression == searchExpression ) + return; + + mSearchExpression = searchExpression; + + if ( mSearchExpression.isEmpty() ) + invalidate(); + else + setFilterRegularExpression( QRegularExpression( mSearchExpression ) ); + + emit searchExpressionChanged( mSearchExpression ); +} + +bool ProjectsProxyModel_future::filterAcceptsRow( int, const QModelIndex &sourceParent ) const { // return true if it passes search filter - return true; + QString projectName = sourceModel()->data( sourceParent, ProjectsModel_future::Roles::ProjectName ).toString(); + QString projectNamespace = sourceModel()->data( sourceParent, ProjectsModel_future::Roles::ProjectNamespace ).toString(); + + QRegExp filter = filterRegExp(); + if ( filter.isEmpty() ) + return true; + + return ( projectName.contains( filter ) || projectNamespace.contains( filter ) ); } + +//bool ProjectsProxyModel_future::lessThan( const QModelIndex &left, const QModelIndex &right ) const +//{ +// TODO: Maybe simply setting sort role as projectFullName would work the same + +// if ( mModelType == LocalProjectsModel ) +// { + /** + * Ordering of local projects: first non-mergin projects (using folder name), + * then mergin projects (sorted first by namespace, then project name) + */ + +// if ( projectNamespace.isEmpty() && other.projectNamespace.isEmpty() ) +// { +// return folderName.compare( other.folderName, Qt::CaseInsensitive ) < 0; +// } +// if ( !projectNamespace.isEmpty() && other.projectNamespace.isEmpty() ) +// { +// return false; +// } +// if ( projectNamespace.isEmpty() && !other.projectNamespace.isEmpty() ) +// { +// return true; +// } + +// if ( projectNamespace.compare( other.projectNamespace, Qt::CaseInsensitive ) == 0 ) +// { +// return projectName.compare( other.projectName, Qt::CaseInsensitive ) < 0; +// } +// if ( projectNamespace.compare( other.projectNamespace, Qt::CaseInsensitive ) < 0 ) +// { +// return true; +// } +// else +// return false; +// return true; +// } +//} diff --git a/app/projectsproxymodel_future.h b/app/projectsproxymodel_future.h index db02b5fae..66b4539fb 100644 --- a/app/projectsproxymodel_future.h +++ b/app/projectsproxymodel_future.h @@ -22,15 +22,27 @@ class ProjectsProxyModel_future : public QSortFilterProxyModel { Q_OBJECT public: - explicit ProjectsProxyModel_future( ProjectModelTypes modelType, QObject *parent = nullptr ); + explicit ProjectsProxyModel_future( ProjectsModel_future *projectSourceModel, QObject *parent = nullptr ); ~ProjectsProxyModel_future() override {}; - protected: + Q_PROPERTY( QString searchExpression READ searchExpression WRITE setSearchExpression NOTIFY searchExpressionChanged ) + + QString searchExpression() const; + +public slots: + void setSearchExpression(QString SearchExpression); + +signals: + void searchExpressionChanged(QString SearchExpression); + +protected: bool filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent ) const override; -// bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; +// bool lessThan( const QModelIndex &left, const QModelIndex &right ) const override; private: + ProjectsModel_future *mModel; ProjectModelTypes mModelType; + QString mSearchExpression; }; #endif // PROJECTSPROXYMODEL_FUTURE_H From 5348e76f6c93e9b58599b5f168927071b738937f Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Wed, 24 Mar 2021 22:16:44 +0100 Subject: [PATCH 19/53] Make models instantiable from QML --- app/localprojectsmanager.cpp | 12 +-- app/localprojectsmanager.h | 4 +- app/main.cpp | 6 +- app/project_future.h | 37 ++++---- app/projectsmodel_future.cpp | 153 ++++++++++++++++++++++-------- app/projectsmodel_future.h | 70 +++++++++----- app/projectsproxymodel_future.cpp | 109 +++++++++++---------- app/projectsproxymodel_future.h | 23 +++-- 8 files changed, 267 insertions(+), 147 deletions(-) diff --git a/app/localprojectsmanager.cpp b/app/localprojectsmanager.cpp index f8b9ad5aa..b06b6950f 100644 --- a/app/localprojectsmanager.cpp +++ b/app/localprojectsmanager.cpp @@ -290,15 +290,15 @@ static int _getProjectFilesCount( const QString &path ) return count; } -ProjectStatus_future LocalProjectsManager::currentProjectStatus( const std::shared_ptr project ) +ProjectStatus::Status LocalProjectsManager::currentProjectStatus( const std::shared_ptr project ) { if ( !project || !project->isMergin() || !project->isLocal() ) // This is not a Mergin project or not downloaded project - return ProjectStatus_future::_NoVersion; + return ProjectStatus::NoVersion; // There was no sync yet if ( project->local->localVersion < 0 ) { - return ProjectStatus_future::_NoVersion; + return ProjectStatus::NoVersion; } // @@ -313,16 +313,16 @@ ProjectStatus_future LocalProjectsManager::currentProjectStatus( const std::shar int filesCount = _getProjectFilesCount( project->local->projectDir ); if ( lastSync < lastModified || meta.files.count() != filesCount ) { - return ProjectStatus_future::_Modified; + return ProjectStatus::Modified; } // Version is lower than latest one, last sync also before updated if ( project->local->localVersion < project->mergin->serverVersion ) { - return ProjectStatus_future::_OutOfDate; + return ProjectStatus::OutOfDate; } - return ProjectStatus_future::_UpToDate; + return ProjectStatus::UpToDate; } //void LocalProjectsManager::updateProjectStatus( LocalProject_future &project ) diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index 4a601aae5..0b5a0652f 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -93,7 +93,7 @@ class LocalProjectsManager : public QObject // void addLocalProject( const QString &projectDir, const QString &projectName ); //! Should forget about that project (it has been removed already) - void removeLocalProject( const QString &projectDir ); + Q_INVOKABLE void removeLocalProject( const QString &projectDir ); //! Resets mergin related info for given project. // void removeMerginInfo( const QString &projectFullName ); @@ -121,7 +121,7 @@ class LocalProjectsManager : public QObject QString findQgisProjectFile( const QString &projectDir, QString &err ); - static ProjectStatus_future currentProjectStatus( const std::shared_ptr project ); // TODO: maybe move somewhere else? + static ProjectStatus::Status currentProjectStatus( const std::shared_ptr project ); // TODO: maybe move somewhere else? signals: void projectMetadataChanged( const QString &projectDir ); diff --git a/app/main.cpp b/app/main.cpp index c85e09b3a..8c41aab12 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -57,6 +57,7 @@ #include "projectsmodel_future.h" #include "projectsproxymodel_future.h" +#include "project_future.h" #ifdef INPUT_TEST #include "test/testmerginapi.h" @@ -235,8 +236,9 @@ void initDeclarative() qmlRegisterType( "lc", 1, 0, "FieldsModel" ); qmlRegisterType( "lc", 1, 0, "CodeFilter" ); - qmlRegisterUncreatableType( "lc", 1, 0, "ProjectsModelF", "" ); - qmlRegisterUncreatableType( "lc", 1, 0, "ProjectsProxyModelF", "" ); + qmlRegisterType( "lc", 1, 0, "ProjectsModel" ); + qmlRegisterType( "lc", 1, 0, "ProjectsProxyModel" ); + qmlRegisterUncreatableMetaObject( ProjectStatus::staticMetaObject, "lc", 1, 0, "ProjectStatus", "ProjectStatus Enum" ); } #ifdef INPUT_TEST diff --git a/app/project_future.h b/app/project_future.h index 1b4946ac9..b6ffc43b5 100644 --- a/app/project_future.h +++ b/app/project_future.h @@ -16,16 +16,19 @@ #include #include -enum ProjectStatus_future -{ - _NoVersion, //!< the project is not available locally - _UpToDate, //!< both server and local copy are in sync with no extra modifications - _OutOfDate, //!< server has newer version than what is available locally (but the project is not modified locally) - _Modified, //!< there are some local modifications in the project that need to be pushed (note: also server may have newer version) +namespace ProjectStatus { + Q_NAMESPACE + enum Status + { + NoVersion, //!< the project is not available locally + UpToDate, //!< both server and local copy are in sync with no extra modifications + OutOfDate, //!< server has newer version than what is available locally (but the project is not modified locally) + Modified, //!< there are some local modifications in the project that need to be pushed (note: also server may have newer version) - // Maybe orphaned state in future -}; -Q_ENUMS( ProjectStatus_future ) + // Maybe orphaned state in future + }; + Q_ENUM_NS( Status ) +} struct LocalProject_future { @@ -48,7 +51,7 @@ struct LocalProject_future bool operator ==( const LocalProject_future &other ) { - return ( this->id() == other.id() ) && ( this->projectDir == other.projectDir ); + return ( this->id() == other.id() ); } bool operator !=( const LocalProject_future &other ) @@ -70,7 +73,7 @@ struct MerginProject_future QDateTime serverUpdated; // available latest version of project files on server int serverVersion; - ProjectStatus_future status = ProjectStatus_future::_NoVersion; + ProjectStatus::Status status = ProjectStatus::NoVersion; bool pending = false; qreal progress = 0; @@ -102,22 +105,22 @@ struct Project_future QString projectName() { - if ( isLocal() ) return local->projectName; - else if ( isMergin() ) return mergin->projectName; + if ( isMergin() ) return mergin->projectName; + else if ( isLocal() ) return local->projectName; return QString(); } QString projectNamespace() { - if ( isLocal() ) return local->projectNamespace; - else if ( isMergin() ) return mergin->projectNamespace; + if ( isMergin() ) return mergin->projectNamespace; + else if ( isLocal() ) return local->projectNamespace; return QString(); } QString projectId() { - if ( isLocal() ) return local->id(); - else if ( isMergin() ) return mergin->id(); + if ( isMergin() ) return mergin->id(); + else if ( isLocal() ) return local->id(); return QString(); } diff --git a/app/projectsmodel_future.cpp b/app/projectsmodel_future.cpp index 046010619..8e99f552c 100644 --- a/app/projectsmodel_future.cpp +++ b/app/projectsmodel_future.cpp @@ -12,26 +12,27 @@ #include "inpututils.h" #include "merginuserauth.h" -ProjectsModel_future::ProjectsModel_future( - MerginApi *merginApi, - ProjectModelTypes modelType, - LocalProjectsManager &localProjectsManager, - QObject *parent ) : - QAbstractListModel( parent ), - mBackend( merginApi ), - mLocalProjectsManager( localProjectsManager ), - mModelType( modelType ) +ProjectsModel_future::ProjectsModel_future( QObject *parent ) : QAbstractListModel( parent ) { + qDebug() << "PMR: Instantiated ProjectsModel! " << this << "MerginAPI: " << mBackend << "LPM:" << mLocalProjectsManager << "Type: " << mModelType; +} + +void ProjectsModel_future::initializeProjectsModel() +{ + if ( !mBackend || !mLocalProjectsManager || mModelType == EmptyProjectsModel ) // Model is not set up properly yet + return; + + qDebug() << "PMR: initializing projects model " << this; + QObject::connect( mBackend, &MerginApi::syncProjectStatusChanged, this, &ProjectsModel_future::onProjectSyncProgressChanged ); QObject::connect( mBackend, &MerginApi::syncProjectFinished, this, &ProjectsModel_future::onProjectSyncFinished ); QObject::connect( mBackend, &MerginApi::projectDetached, this, &ProjectsModel_future::onProjectDetachedFromMergin ); QObject::connect( mBackend, &MerginApi::projectAttachedToMergin, this, &ProjectsModel_future::onProjectAttachedToMergin ); - if ( mModelType == ProjectModelTypes::LocalProjectsModel ) { QObject::connect( mBackend, &MerginApi::listProjectsByNameFinished, this, &ProjectsModel_future::onListProjectsByNameFinished ); - loadLocalProjects(); // at app start, we need to fill model with local projects + loadLocalProjects(); } else if ( mModelType != ProjectModelTypes::RecentProjectsModel ) { @@ -42,10 +43,12 @@ ProjectsModel_future::ProjectsModel_future( // Implement RecentProjectsModel type } - QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectAdded, this, &ProjectsModel_future::onProjectAdded ); - QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::aboutToRemoveLocalProject, this, &ProjectsModel_future::onAboutToRemoveProject ); - QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::localProjectDataChanged, this, &ProjectsModel_future::onProjectDataChanged ); - QObject::connect( &mLocalProjectsManager, &LocalProjectsManager::dataDirReloaded, this, &ProjectsModel_future::loadLocalProjects ); + QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectAdded, this, &ProjectsModel_future::onProjectAdded ); + QObject::connect( mLocalProjectsManager, &LocalProjectsManager::aboutToRemoveLocalProject, this, &ProjectsModel_future::onAboutToRemoveProject ); + QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectDataChanged, this, &ProjectsModel_future::onProjectDataChanged ); + QObject::connect( mLocalProjectsManager, &LocalProjectsManager::dataDirReloaded, this, &ProjectsModel_future::loadLocalProjects ); + + emit modelInitialized(); } QVariant ProjectsModel_future::data( const QModelIndex &index, int role ) const @@ -53,22 +56,41 @@ QVariant ProjectsModel_future::data( const QModelIndex &index, int role ) const if ( !index.isValid() ) return QVariant(); + if ( index.row() < 0 || index.row() >= mProjects.size() ) + return QVariant(); + std::shared_ptr project = mProjects.at( index.row() ); switch ( role ) { case ProjectName: return QVariant( project->projectName() ); case ProjectNamespace: return QVariant( project->projectNamespace() ); - case ProjectFullName: return QVariant( project->projectId() ); + case ProjectFullName: return MerginApi::getFullProjectName( project->projectNamespace(), project->projectName() ); + case ProjectId: return QVariant( project->projectId() ); case ProjectIsLocal: return QVariant( project->isLocal() ); case ProjectIsMergin: return QVariant( project->isMergin() ); - case ProjectStatus: return QVariant( LocalProjectsManager::currentProjectStatus( project ) ); - case ProjectIsValid: return QVariant( !project->isLocal() || ( project->isLocal() && project->local->projectError.isEmpty() ) ); + case ProjectSyncStatus: return QVariant( LocalProjectsManager::currentProjectStatus( project ) ); + case ProjectFilePath: return QVariant( project->isLocal() ? project->local->qgisProjectFilePath : QString() ); + case ProjectDirectory: return QVariant( project->isLocal() ? project->local->projectDir : QString() ); + case ProjectIsValid: { + if ( !project->isLocal() ) + return true; // Mergin projects are by default valid, remote error only affects syncing, not opening of a project + return project->local->projectError.isEmpty(); + } case ProjectDescription: { - if ( project->isLocal() && !project->local->projectError.isEmpty() ) - return project->local->projectError; - - QFileInfo fi( project->local->projectDir ); - return fi.lastModified(); + if ( project->isLocal() ) + { + if ( !project->local->projectError.isEmpty() ) + { + return QVariant( project->local->projectError ); + } + QFileInfo fi( project->local->projectDir ); + return QVariant( fi.lastModified().toLocalTime() ); // Maybe use better timestamp format https://doc.qt.io/qt-5/qdatetime.html#toString-3 + } + else if ( project->isMergin() ) + { + return QVariant( project->mergin->serverUpdated ); + } + return QVariant(); // This should not happen } default: { if ( !project->isMergin() ) return QVariant(); @@ -89,17 +111,33 @@ QModelIndex ProjectsModel_future::index( int row, int col, const QModelIndex &pa return createIndex( row, 0, nullptr ); } +bool ProjectsModel_future::canFetchMore( const QModelIndex & ) const +{ +// return mServerProjectsCount > mProjects.size(); + return true; +} + +void ProjectsModel_future::fetchMore( const QModelIndex & ) +{ + +} + QHash ProjectsModel_future::roleNames() const { QHash roles; roles[Roles::ProjectName] = QStringLiteral( "ProjectName" ).toLatin1(); roles[Roles::ProjectNamespace] = QStringLiteral( "ProjectNamespace" ).toLatin1(); roles[Roles::ProjectFullName] = QStringLiteral( "ProjectFullName" ).toLatin1(); + roles[Roles::ProjectDirectory] = QStringLiteral( "ProjectDirectory" ).toLatin1(); roles[Roles::ProjectIsLocal] = QStringLiteral( "ProjectIsLocal" ).toLatin1(); roles[Roles::ProjectIsMergin] = QStringLiteral( "ProjectIsMergin" ).toLatin1(); + roles[Roles::ProjectSyncStatus] = QStringLiteral( "ProjectSyncStatus" ).toLatin1(); + roles[Roles::ProjectIsValid] = QStringLiteral( "ProjectIsValid" ).toLatin1(); + roles[Roles::ProjectFilePath] = QStringLiteral( "ProjectFilePath" ).toLatin1(); roles[Roles::ProjectDescription] = QStringLiteral( "ProjectDescription" ).toLatin1(); roles[Roles::ProjectPending] = QStringLiteral( "ProjectPending" ).toLatin1(); roles[Roles::ProjectSyncProgress] = QStringLiteral( "ProjectSyncProgress" ).toLatin1(); + roles[Roles::ProjectRemoteError] = QStringLiteral( "ProjectRemoteError" ).toLatin1(); return roles; } @@ -112,8 +150,7 @@ void ProjectsModel_future::listProjects( int page, QString searchExpression ) { if ( mModelType == LocalProjectsModel ) { - InputUtils::log( "Input", "Can not call listProjects API on LocalProjectsModel" ); - // maybe call listProjectsByName(); + listProjectsByName(); return; } @@ -124,7 +161,6 @@ void ProjectsModel_future::listProjectsByName() { if ( mModelType != LocalProjectsModel ) { - InputUtils::log( "Input", "Can not call listProjectsByName API on not LocalProjectsModel" ); return; } @@ -133,7 +169,7 @@ void ProjectsModel_future::listProjectsByName() void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious ) { - LocalProjectsList localProjects = mLocalProjectsManager.projects(); + LocalProjectsList localProjects = mLocalProjectsManager->projects(); qDebug() << "PMR: mergeProjects(): # of local projects = " << localProjects.size() << " # of mergin projects = " << merginProjects.size(); @@ -166,6 +202,13 @@ void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjec project->mergin->pending = true; } } + else if ( project->local->localVersion > -1 ) + { + // this is indeed a Mergin project, it has metadata folder in it + project->mergin = std::unique_ptr( new MerginProject_future() ); + project->mergin->projectName = project->local->projectName; + project->mergin->projectNamespace = project->local->projectNamespace; + } mProjects << project; } @@ -240,7 +283,7 @@ void ProjectsModel_future::onListProjectsByNameFinished( const MerginProjectsLis endResetModel(); } -void ProjectsModel_future::syncProject( QString projectNamespace, QString projectName ) +void ProjectsModel_future::syncProject( const QString &projectNamespace, const QString &projectName ) { std::shared_ptr project = projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); @@ -262,21 +305,21 @@ void ProjectsModel_future::syncProject( QString projectNamespace, QString projec return; } - if ( project->mergin->status == _NoVersion || project->mergin->status == _OutOfDate ) + if ( project->mergin->status == ProjectStatus::NoVersion || project->mergin->status == ProjectStatus::OutOfDate ) { qDebug() << "PMR: updating project:" << project->mergin->id(); bool useAuth = !mBackend->userAuth()->hasAuthData() && mModelType == ProjectModelTypes::ExploreProjectsModel; mBackend->updateProject( projectNamespace, projectName, useAuth ); } - else if ( project->mergin->status == _Modified ) + else if ( project->mergin->status == ProjectStatus::Modified ) { qDebug() << "PMR: uploading project:" << project->mergin->id(); mBackend->uploadProject( projectNamespace, projectName ); } } -void ProjectsModel_future::stopProjectSync( QString projectNamespace, QString projectName ) +void ProjectsModel_future::stopProjectSync( const QString &projectNamespace, const QString &projectName ) { std::shared_ptr project = projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); @@ -298,18 +341,23 @@ void ProjectsModel_future::stopProjectSync( QString projectNamespace, QString pr return; } - if ( project->mergin->status == _NoVersion || project->mergin->status == _OutOfDate ) + if ( project->mergin->status == ProjectStatus::NoVersion || project->mergin->status == ProjectStatus::OutOfDate ) { qDebug() << "PMR: cancelling update of project:" << project->mergin->id(); mBackend->updateCancel( project->mergin->id() ); } - else if ( project->mergin->status == _Modified ) + else if ( project->mergin->status == ProjectStatus::Modified ) { qDebug() << "PMR: cancelling upload of project:" << project->mergin->id(); mBackend->uploadCancel( project->mergin->id() ); } } +void ProjectsModel_future::removeLocalProject( const QString &projectDir ) +{ + mLocalProjectsManager->removeLocalProject( projectDir ); +} + void ProjectsModel_future::onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully ) { Q_UNUSED( projectDir ) @@ -360,8 +408,9 @@ void ProjectsModel_future::onProjectAdded( const LocalProject_future &project ) std::shared_ptr newProject = std::shared_ptr( new Project_future() ); newProject->local = std::unique_ptr( new LocalProject_future( project ) ); - int insertIndex = mProjects.size() == 0 ? 0 : mProjects.size() - 1; - beginInsertRows( QModelIndex(), insertIndex, insertIndex + 1 ); + int insertIndex = mProjects.size(); + + beginInsertRows( QModelIndex(), insertIndex, insertIndex ); mProjects << newProject; endInsertRows(); } @@ -434,6 +483,36 @@ void ProjectsModel_future::setServerProjectsCount( int serverProjectsCount ) emit serverProjectsCountChanged( mServerProjectsCount ); } +void ProjectsModel_future::setMerginApi( MerginApi *merginApi ) +{ + if ( !merginApi || mBackend == merginApi ) + return; + + mBackend = merginApi; + qDebug() << "PMR: New MA: " << mBackend; + initializeProjectsModel(); +} + +void ProjectsModel_future::setLocalProjectsManager( LocalProjectsManager *localProjectsManager ) +{ + if ( !localProjectsManager || mLocalProjectsManager == localProjectsManager ) + return; + + mLocalProjectsManager = localProjectsManager; + qDebug() << "PMR: New LPM: " << mLocalProjectsManager; + initializeProjectsModel(); +} + +void ProjectsModel_future::setModelType( ProjectsModel_future::ProjectModelTypes modelType ) +{ + if ( mModelType == modelType ) + return; + + mModelType = modelType; + qDebug() << "PMR: New Type: " << mModelType; + initializeProjectsModel(); +} + QString ProjectsModel_future::modelTypeToFlag() const { switch ( mModelType ) @@ -469,7 +548,7 @@ void ProjectsModel_future::printProjects() const // TODO: Helper function, remov QStringList ProjectsModel_future::projectNames() const { QStringList projectNames; - LocalProjectsList projects = mLocalProjectsManager.projects(); + LocalProjectsList projects = mLocalProjectsManager->projects(); for ( const auto &proj : projects ) { @@ -509,7 +588,7 @@ std::shared_ptr ProjectsModel_future::projectFromId( QString pro return nullptr; } -ProjectModelTypes ProjectsModel_future::modelType() const +ProjectsModel_future::ProjectModelTypes ProjectsModel_future::modelType() const { return mModelType; } diff --git a/app/projectsmodel_future.h b/app/projectsmodel_future.h index 9000409e9..e03ba39c0 100644 --- a/app/projectsmodel_future.h +++ b/app/projectsmodel_future.h @@ -18,18 +18,6 @@ class LocalProjectsManager; -/** - * \brief The ProjectModelTypes enum - */ -enum ProjectModelTypes -{ - LocalProjectsModel = 0, - MyProjectsModel, - SharedProjectsModel, - ExploreProjectsModel, - RecentProjectsModel -}; - /** * \brief The ProjectsModel_future class */ @@ -43,26 +31,50 @@ class ProjectsModel_future : public QAbstractListModel { ProjectName = Qt::UserRole + 1, ProjectNamespace, - ProjectFullName, // or ProjectId, filled with folderName if project is not + ProjectFullName, + ProjectId, // Filled with ProjectFullName for time being + ProjectDirectory, ProjectDescription, ProjectPending, ProjectIsMergin, ProjectIsLocal, + ProjectFilePath, ProjectIsValid, - ProjectStatus, + ProjectSyncStatus, ProjectSyncProgress, ProjectRemoteError }; - Q_ENUMS( Roles ) + Q_ENUM( Roles ) + + /** + * \brief The ProjectModelTypes enum + */ + enum ProjectModelTypes + { + EmptyProjectsModel = 0, // default, holding no projects ~ invalid model + LocalProjectsModel, + MyProjectsModel, + SharedProjectsModel, + ExploreProjectsModel, + RecentProjectsModel + }; + Q_ENUM( ProjectModelTypes ) - ProjectsModel_future( MerginApi *merginApi, ProjectModelTypes modelType, LocalProjectsManager &localProjectsManager, QObject *parent = nullptr ); + ProjectsModel_future( QObject *parent = nullptr ); ~ProjectsModel_future() override {}; - Q_PROPERTY( int serverProjectsCount READ serverProjectsCount WRITE setServerProjectsCount NOTIFY serverProjectsCountChanged ); + Q_PROPERTY( int serverProjectsCount READ serverProjectsCount WRITE setServerProjectsCount NOTIFY serverProjectsCountChanged ) // TODO: replace with builtin canFetchMore + + // From Qt 5.15 we can use REQUIRED keyword here that will ensure object will be always instantiated from QML with these mandatory properties + Q_PROPERTY( MerginApi *merginApi READ merginApi WRITE setMerginApi ) + Q_PROPERTY( LocalProjectsManager *localProjectsManager READ localProjectsManager WRITE setLocalProjectsManager ) + Q_PROPERTY( ProjectModelTypes modelType READ modelType WRITE setModelType ) // Needed methods from QAbstractListModel Q_INVOKABLE QVariant data( const QModelIndex &index, int role ) const override; Q_INVOKABLE QModelIndex index( int row, int column = 0, const QModelIndex &parent = QModelIndex() ) const override; + Q_INVOKABLE bool canFetchMore( const QModelIndex &parent ) const override; + Q_INVOKABLE void fetchMore( const QModelIndex &parent ) override; QHash roleNames() const override; int rowCount( const QModelIndex &parent = QModelIndex() ) const override; @@ -73,17 +85,24 @@ class ProjectsModel_future : public QAbstractListModel Q_INVOKABLE void listProjectsByName(); //! Syncs specified project - upload or update - Q_INVOKABLE void syncProject( QString projectNamespace, QString projectName ); + Q_INVOKABLE void syncProject( const QString &projectNamespace, const QString &projectName ); //! Stops running project upload or update - Q_INVOKABLE void stopProjectSync( QString projectNamespace, QString projectName ); + Q_INVOKABLE void stopProjectSync( const QString &projectNamespace, const QString &projectName ); + + //! Forwards call to LocalProjectsManager to remove local project + Q_INVOKABLE void removeLocalProject( const QString &projectDir ); //! Method merging local and remote projects based on the model type void mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious = false ); int serverProjectsCount() const; - ProjectModelTypes modelType() const; + ProjectsModel_future::ProjectModelTypes modelType() const; + + MerginApi *merginApi() const { return mBackend; } + + LocalProjectsManager *localProjectsManager() const { return mLocalProjectsManager; } public slots: // MerginAPI - backend signals @@ -100,9 +119,13 @@ public slots: void onProjectDataChanged( const LocalProject_future &project ); void setServerProjectsCount( int serverProjectsCount ); + void setMerginApi( MerginApi *merginApi ); + void setLocalProjectsManager( LocalProjectsManager *localProjectsManager ); + void setModelType( ProjectModelTypes modelType ); signals: void serverProjectsCountChanged( int serverProjectsCount ); + void modelInitialized(); private: @@ -110,15 +133,16 @@ public slots: void printProjects() const; QStringList projectNames() const; void loadLocalProjects(); + void initializeProjectsModel(); bool containsProject( QString projectId ) const; std::shared_ptr projectFromId( QString projectId ) const; - MerginApi *mBackend; - LocalProjectsManager &mLocalProjectsManager; + MerginApi *mBackend = nullptr; + LocalProjectsManager *mLocalProjectsManager = nullptr; QList> mProjects; - ProjectModelTypes mModelType; + ProjectModelTypes mModelType = EmptyProjectsModel; //! For pagination int mServerProjectsCount = -1; diff --git a/app/projectsproxymodel_future.cpp b/app/projectsproxymodel_future.cpp index 398c704c4..3afb20571 100644 --- a/app/projectsproxymodel_future.cpp +++ b/app/projectsproxymodel_future.cpp @@ -9,15 +9,21 @@ #include "projectsproxymodel_future.h" -ProjectsProxyModel_future::ProjectsProxyModel_future( ProjectsModel_future *projectsSourceModel, QObject *parent ) : - QSortFilterProxyModel( parent ), - mModel( projectsSourceModel ) +ProjectsProxyModel_future::ProjectsProxyModel_future( QObject *parent ) : QSortFilterProxyModel( parent ) { + qDebug() << "PMR: Building proxy model " << this; +} + +void ProjectsProxyModel_future::initialize() +{ + qDebug() << "PMR: Initializing proxy model" << this; setSourceModel( mModel ); mModelType = mModel->modelType(); - setSortRole( ProjectsModel_future::Roles::ProjectFullName ); - setSortCaseSensitivity( Qt::CaseSensitivity::CaseInsensitive ); + setFilterRole( ProjectsModel_future::ProjectFullName ); + setFilterCaseSensitivity( Qt::CaseInsensitive ); + + sort( 0, Qt::AscendingOrder ); } QString ProjectsProxyModel_future::searchExpression() const @@ -25,68 +31,69 @@ QString ProjectsProxyModel_future::searchExpression() const return mSearchExpression; } +ProjectsModel_future *ProjectsProxyModel_future::projectSourceModel() const +{ + return mModel; +} + void ProjectsProxyModel_future::setSearchExpression( QString searchExpression ) { if ( mSearchExpression == searchExpression ) return; mSearchExpression = searchExpression; - - if ( mSearchExpression.isEmpty() ) - invalidate(); - else - setFilterRegularExpression( QRegularExpression( mSearchExpression ) ); - + setFilterFixedString( mSearchExpression ); emit searchExpressionChanged( mSearchExpression ); } -bool ProjectsProxyModel_future::filterAcceptsRow( int, const QModelIndex &sourceParent ) const +void ProjectsProxyModel_future::setProjectSourceModel( ProjectsModel_future *sourceModel ) { - // return true if it passes search filter - QString projectName = sourceModel()->data( sourceParent, ProjectsModel_future::Roles::ProjectName ).toString(); - QString projectNamespace = sourceModel()->data( sourceParent, ProjectsModel_future::Roles::ProjectNamespace ).toString(); - - QRegExp filter = filterRegExp(); - if ( filter.isEmpty() ) - return true; + if ( mModel == sourceModel ) + return; - return ( projectName.contains( filter ) || projectNamespace.contains( filter ) ); + mModel = sourceModel; + QObject::connect( mModel, &ProjectsModel_future::modelInitialized, this, &ProjectsProxyModel_future::initialize ); } -//bool ProjectsProxyModel_future::lessThan( const QModelIndex &left, const QModelIndex &right ) const -//{ -// TODO: Maybe simply setting sort role as projectFullName would work the same -// if ( mModelType == LocalProjectsModel ) -// { +bool ProjectsProxyModel_future::lessThan( const QModelIndex &left, const QModelIndex &right ) const +{ + if ( mModelType == ProjectsModel_future::LocalProjectsModel ) + { + bool lProjectIsMergin = mModel->data( left, ProjectsModel_future::ProjectIsMergin ).toBool(); + bool rProjectIsMergin = mModel->data( right, ProjectsModel_future::ProjectIsMergin ).toBool(); + /** * Ordering of local projects: first non-mergin projects (using folder name), * then mergin projects (sorted first by namespace, then project name) */ -// if ( projectNamespace.isEmpty() && other.projectNamespace.isEmpty() ) -// { -// return folderName.compare( other.folderName, Qt::CaseInsensitive ) < 0; -// } -// if ( !projectNamespace.isEmpty() && other.projectNamespace.isEmpty() ) -// { -// return false; -// } -// if ( projectNamespace.isEmpty() && !other.projectNamespace.isEmpty() ) -// { -// return true; -// } - -// if ( projectNamespace.compare( other.projectNamespace, Qt::CaseInsensitive ) == 0 ) -// { -// return projectName.compare( other.projectName, Qt::CaseInsensitive ) < 0; -// } -// if ( projectNamespace.compare( other.projectNamespace, Qt::CaseInsensitive ) < 0 ) -// { -// return true; -// } -// else -// return false; -// return true; -// } -//} + if ( !lProjectIsMergin && !rProjectIsMergin ) + { + QString lProjectFullName = mModel->data( left, ProjectsModel_future::ProjectFullName ).toString(); + QString rProjectFullName = mModel->data( right, ProjectsModel_future::ProjectFullName ).toString(); + + return lProjectFullName.compare( rProjectFullName, Qt::CaseInsensitive ) < 0; + } + if ( !lProjectIsMergin && rProjectIsMergin ) + { + return false; + } + if ( lProjectIsMergin && !rProjectIsMergin ) + { + return true; + } + + QString lNamespace = mModel->data( left, ProjectsModel_future::ProjectNamespace ).toString(); + QString lProjectName = mModel->data( left, ProjectsModel_future::ProjectName ).toString(); + QString rNamespace = mModel->data( right, ProjectsModel_future::ProjectNamespace ).toString(); + QString rProjectName = mModel->data( right, ProjectsModel_future::ProjectName ).toString(); + + if ( lNamespace == rNamespace ) + { + return lProjectName.compare( rProjectName, Qt::CaseInsensitive ) < 0; + } + return lNamespace.compare( rNamespace, Qt::CaseInsensitive ) < 0; + } + return false; +} diff --git a/app/projectsproxymodel_future.h b/app/projectsproxymodel_future.h index 66b4539fb..bf35046ea 100644 --- a/app/projectsproxymodel_future.h +++ b/app/projectsproxymodel_future.h @@ -20,28 +20,33 @@ */ class ProjectsProxyModel_future : public QSortFilterProxyModel { - Q_OBJECT - public: - explicit ProjectsProxyModel_future( ProjectsModel_future *projectSourceModel, QObject *parent = nullptr ); - ~ProjectsProxyModel_future() override {}; + Q_OBJECT Q_PROPERTY( QString searchExpression READ searchExpression WRITE setSearchExpression NOTIFY searchExpressionChanged ) + Q_PROPERTY( ProjectsModel_future *projectSourceModel READ projectSourceModel WRITE setProjectSourceModel ) + +public: + explicit ProjectsProxyModel_future( QObject *parent = nullptr ); + ~ProjectsProxyModel_future() override {}; QString searchExpression() const; + ProjectsModel_future *projectSourceModel() const; public slots: - void setSearchExpression(QString SearchExpression); + void setSearchExpression( QString searchExpression ); + void setProjectSourceModel( ProjectsModel_future *sourceModel ); signals: - void searchExpressionChanged(QString SearchExpression); + void searchExpressionChanged( QString SearchExpression ); protected: - bool filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent ) const override; -// bool lessThan( const QModelIndex &left, const QModelIndex &right ) const override; + bool lessThan( const QModelIndex &left, const QModelIndex &right ) const override; private: + void initialize(); + ProjectsModel_future *mModel; - ProjectModelTypes mModelType; + ProjectsModel_future::ProjectModelTypes mModelType = ProjectsModel_future::EmptyProjectsModel; QString mSearchExpression; }; From ca479c568a3868adf9d3e11c9d7c985c2fbb40eb Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Wed, 31 Mar 2021 12:19:12 +0200 Subject: [PATCH 20/53] refactor qml code --- app/qml/ExtendedMenuItem.qml | 4 +- app/qml/InputStyle.qml | 5 + app/qml/MainPanelButton.qml | 3 +- app/qml/MerginProjectPanel.qml | 914 ---------------- app/qml/ProjectDelegateItem.qml | 179 ---- app/qml/ProjectListPage.qml | 62 ++ app/qml/ProjectPanel.qml | 1095 ++++++++++++++++++++ app/qml/components/ProjectDelegateItem.qml | 367 +++++++ app/qml/components/ProjectList.qml | 179 ++++ app/qml/main.qml | 63 +- app/qml/qml.qrc | 14 +- 11 files changed, 1753 insertions(+), 1132 deletions(-) delete mode 100644 app/qml/MerginProjectPanel.qml delete mode 100644 app/qml/ProjectDelegateItem.qml create mode 100644 app/qml/ProjectListPage.qml create mode 100644 app/qml/ProjectPanel.qml create mode 100644 app/qml/components/ProjectDelegateItem.qml create mode 100644 app/qml/components/ProjectList.qml diff --git a/app/qml/ExtendedMenuItem.qml b/app/qml/ExtendedMenuItem.qml index 68d2bcd93..6e528ba9a 100644 --- a/app/qml/ExtendedMenuItem.qml +++ b/app/qml/ExtendedMenuItem.qml @@ -39,6 +39,7 @@ Item { id: iconContainer height: rowHeight width: rowHeight + anchors.verticalCenter: parent ? parent.verticalCenter : undefined Image { id: icon @@ -64,6 +65,7 @@ Item { x: iconContainer.width + panelMargin width: parent.width - rowHeight height: rowHeight + anchors.verticalCenter: parent ? parent.verticalCenter : undefined Text { id: mainText @@ -87,6 +89,6 @@ Item { width: row.width height: 1 visible: root.showBorder - anchors.bottom: parent.bottom + anchors.bottom: parent ? parent.bottom : undefined } } diff --git a/app/qml/InputStyle.qml b/app/qml/InputStyle.qml index d75ea674b..e1d03d0d5 100644 --- a/app/qml/InputStyle.qml +++ b/app/qml/InputStyle.qml @@ -19,6 +19,7 @@ QtObject { property color fontColorBright: "#679D70" property color panelBackground2: "#C6CCC7" property color activeButtonColor: "#006146" + property color activeButtonColorOrange: "#FD9626" // Secondary colors property color clrPanelMain: "white" @@ -85,6 +86,10 @@ QtObject { property var comboboxIcon: "qrc:/combobox.svg" property var qrCodeIcon: "qrc:/qrcode.svg" property var exclamationIcon: "qrc:/exclamation-circle.svg" + property var syncIcon: "qrc:sync.svg" + property var downloadIcon: "qrc:download.svg" + property var stopIcon: "qrc:stop.svg" + property var moreMenuIcon: "qrc:/more_menu.svg" property var vectorPointIcon: "qrc:/mIconPointLayer.svg" property var vectorLineIcon: "qrc:/mIconLineLayer.svg" diff --git a/app/qml/MainPanelButton.qml b/app/qml/MainPanelButton.qml index 363e1c5ee..f164048af 100644 --- a/app/qml/MainPanelButton.qml +++ b/app/qml/MainPanelButton.qml @@ -25,6 +25,7 @@ Rectangle { property color backgroundColor: InputStyle.fontColor property bool isHighlighted: false property bool enabled: true + property bool handleClicks: true // enable property is used also for color property bool faded: false signal activated() @@ -35,7 +36,7 @@ Rectangle { MouseArea { anchors.fill: parent - enabled: root.enabled + enabled: root.enabled && handleClicks onClicked: { root.activated() } diff --git a/app/qml/MerginProjectPanel.qml b/app/qml/MerginProjectPanel.qml deleted file mode 100644 index ca74bfa78..000000000 --- a/app/qml/MerginProjectPanel.qml +++ /dev/null @@ -1,914 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick 2.7 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.0 -import QtQuick.Dialogs 1.2 -import QgsQuick 0.1 as QgsQuick -import lc 1.0 -import "." // import InputStyle singleton -import "./components/" - -Item { - - property int activeProjectIndex: -1 - property string activeProjectPath: __projectsModel.data(__projectsModel.index(activeProjectIndex), ProjectModel.Path) - property var busyIndicator - - property real rowHeight: InputStyle.rowHeightHeader * 1.2 - property real iconSize: rowHeight/3 - property bool showMergin: false - property real panelMargin: InputStyle.panelMargin - - - function openPanel() { - projectsPanel.visible = true - stackView.visible = true - } - - id: projectsPanel - visible: false - focus: true - - onFocusChanged: { // pass focus to stackview - stackView.focus = true - } - - StackView { - id: stackView - initialItem: projectsPanelComp - anchors.fill: parent - focus: true - visible: false - z: projectsPanel.z + 1 - property bool pending: false - - function clearStackAndClose() { - if ( stackView.depth > 1 ) - stackView.pop( null ) // pops everything besides an initialItem - - stackView.visible = false - } - - function popOnePageOrClose() { - if ( stackView.depth > 1 ) - { - stackView.pop() - } - } - - Keys.onReleased: { - if (event.key === Qt.Key_Back || event.key === Qt.Key_Escape) { - event.accepted = true; - - if (stackView.depth > 1) { - stackView.currentItem.back() - } - else if (projectsPanel.activeProjectPath) { - stackView.clearStackAndClose() - projectsPanel.visible = false - } - } - } - - onVisibleChanged: { - if ( stackView.visible ) - stackView.forceActiveFocus() - } - } - - - BusyIndicator { - id: busyIndicator - width: parent.width/8 - height: width - running: stackView.pending - visible: running - anchors.centerIn: parent - z: parent.z + 1 - } - - Component { - id: projectsPanelComp - Item { - - objectName: "projectsPanel" - - function getStatusIcon(status, pending) { - if (pending) return "stop.svg" - - if (status === "noVersion") return "download.svg" - else if (status === "outOfDate") return "sync.svg" - else if (status === "upToDate") return "check.svg" - else if (status === "modified") return "sync.svg" - - return "more_menu.svg" - } - - function refreshProjectList() { - if (toolbar.highlighted === exploreBtn.text) { - exploreBtn.activated() - } else if (toolbar.highlighted === sharedProjectsBtn.text) { - sharedProjectsBtn.activated() - } else if (toolbar.highlighted === myProjectsBtn.text) { - myProjectsBtn.activated() - } else homeBtn.activated() - } - - Component.onCompleted: { - // load model just after all components are prepared - // otherwise GridView's delegate item is initialized invalidately - grid.model = __projectsModel - merginProjectsList.model = __merginProjectsModel - } - - Connections { - target: __projectsModel - onModelReset: { - var index = __projectsModel.rowAccordingPath(activeProjectPath) - if (index !== activeProjectIndex) { - activeProjectIndex = index - } - } - } - - Connections { - target: __projectWizard - onProjectCreated: { - if (stackView.currentItem.objectName === "projectWizard") { - stackView.popOnePageOrClose() - } - } - } - - Connections { - target: __merginApi - onListProjectsFinished: { - stackView.pending = false - } - onListProjectsFailed: { - reloadList.visible = true - } - onApiVersionStatusChanged: { - stackView.pending = false - if (__merginApi.apiVersionStatus === MerginApiStatus.OK && stackView.currentItem.objectName === "authPanel") { - if (__merginApi.userAuth.hasAuthData()) { - refreshProjectList() - } else if (toolbar.highlighted !== homeBtn.text) { - if (stackView.currentItem.objectName !== "authPanel") { - stackView.push(authPanelComp, {state: "login"}) - } - } - } - } - onAuthRequested: { - stackView.pending = false - stackView.push(authPanelComp, {state: "login"}) - } - onAuthChanged: { - stackView.pending = false - if (__merginApi.userAuth.hasAuthData()) { - stackView.popOnePageOrClose() - refreshProjectList() - projectsPanel.forceActiveFocus() - } else { - homeBtn.activated() - } - - } - onAuthFailed: { - homeBtn.activated() - stackView.pending = false - projectsPanel.forceActiveFocus() - } - onRegistrationFailed: stackView.pending = false - onRegistrationSucceeded: stackView.pending = false - } - - - // background - Rectangle { - width: parent.width - height: parent.height - color: InputStyle.clrPanelMain - } - - - PanelHeader { - id: header - height: InputStyle.rowHeightHeader - width: parent.width - color: InputStyle.clrPanelMain - rowHeight: InputStyle.rowHeightHeader - titleText: qsTr("Projects") - - onBack: { - if (projectsPanel.activeProjectPath) { - projectsPanel.visible = false - stackView.clearStackAndClose() - } - } - withBackButton: projectsPanel.activeProjectPath - - Item { - id: avatar - width: InputStyle.rowHeightHeader * 0.8 - height: InputStyle.rowHeightHeader - anchors.right: parent.right - anchors.rightMargin: projectsPanel.panelMargin - - Rectangle { - id: avatarImage - anchors.centerIn: parent - width: avatar.width - height: avatar.width - color: InputStyle.fontColor - radius: width*0.5 - antialiasing: true - - MouseArea { - anchors.fill: parent - onClicked: { - if (__merginApi.userAuth.hasAuthData() && __merginApi.apiVersionStatus === MerginApiStatus.OK) { - __merginApi.getUserInfo() - stackView.push( accountPanelComp) - reloadList.visible = false - } - else - myProjectsBtn.activated() // open auth form - } - } - - Image { - id: userIcon - anchors.centerIn: avatarImage - source: 'account.svg' - height: avatarImage.height * 0.8 - width: height - sourceSize.width: width - sourceSize.height: height - fillMode: Image.PreserveAspectFit - } - - ColorOverlay { - anchors.fill: userIcon - source: userIcon - color: "#FFFFFF" - } - } - } - } - - SearchBar { - id: searchBar - y: header.height - allowTimer: true - - onSearchTextChanged: { - if (toolbar.highlighted === homeBtn.text) { - __projectsModel.searchExpression = text - } else if (toolbar.highlighted === exploreBtn.text) { - // Filtered by request - exploreBtn.activated() - } else if (toolbar.highlighted === sharedProjectsBtn.text) { - __merginProjectsModel.searchExpression = text - } else if (toolbar.highlighted === myProjectsBtn.text) { - __merginProjectsModel.searchExpression = text - } - } - } - - // Content - ColumnLayout { - id: contentLayout - height: projectsPanel.height-header.height-searchBar.height-toolbar.height - width: parent.width - y: header.height + searchBar.height - spacing: 0 - - // Info label - Item { - id: infoLabel - width: parent.width - height: toolbar.highlighted === exploreBtn.text ? projectsPanel.rowHeight * 2 : 0 - visible: height - - Text { - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - wrapMode: Text.WordWrap - color: InputStyle.panelBackgroundDarker - font.pixelSize: InputStyle.fontPixelSizeNormal - text: qsTr("Explore public projects.") - visible: parent.height - } - - // To not propagate click on canvas on background - MouseArea { - anchors.fill: parent - } - - Item { - id: infoLabelHideBtn - height: projectsPanel.iconSize - width: height - anchors.right: parent.right - anchors.top: parent.top - anchors.rightMargin: projectsPanel.panelMargin - anchors.topMargin: projectsPanel.panelMargin - - MouseArea { - anchors.fill: parent - onClicked: infoLabel.visible = false - } - - Image { - id: infoLabelHide - anchors.centerIn: infoLabelHideBtn - source: 'no.svg' - height: infoLabelHideBtn.height - width: height - sourceSize.width: width - sourceSize.height: height - fillMode: Image.PreserveAspectFit - } - - ColorOverlay { - anchors.fill: infoLabelHide - source: infoLabelHide - color: InputStyle.panelBackgroundDark - } - } - - Rectangle { - id: borderLine - color: InputStyle.panelBackground2 - width: parent.width - height: 1 * QgsQuick.Utils.dp - anchors.bottom: parent.bottom - } - } - - ListView { - id: grid - Layout.fillWidth: true - Layout.fillHeight: true - contentWidth: grid.width - clip: true - visible: !showMergin - maximumFlickVelocity: __androidUtils.isAndroid ? InputStyle.scrollVelocityAndroid : maximumFlickVelocity - - property int cellWidth: width - property int cellHeight: projectsPanel.rowHeight - property int borderWidth: 1 - - delegate: delegateItem - - footer: DelegateButton { - height: projectsPanel.rowHeight - width: parent.width - text: qsTr("Create project") - onClicked: { - if (__inputUtils.hasStoragePermission()) { - stackView.push(projectWizardComp) - } else if (__inputUtils.acquireStoragePermission()) { - restartAppDialog.open() - } - } - } - - Text { - id: noProjectsText - anchors.fill: parent - textFormat: Text.RichText - text: "" + - qsTr("No downloaded projects found.%1Learn %2how to create projects%3 and %4download them%3 onto your device.") - .arg("
") - .arg("") - .arg("") - .arg("") - - onLinkActivated: Qt.openUrlExternally(link) - visible: grid.count === 0 && !storagePermissionText.visible - color: InputStyle.fontColor - font.pixelSize: InputStyle.fontPixelSizeNormal - font.bold: true - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - padding: InputStyle.panelMargin/2 - } - - Text { - id: storagePermissionText - anchors.fill: parent - textFormat: Text.RichText - text: "" + - qsTr("Input needs a storage permission, %1click to grant it%2 and then restart application.") - .arg("") - .arg("") - - onLinkActivated: { - if ( __inputUtils.acquireStoragePermission() ) { - restartAppDialog.open() - } - } - visible: !__inputUtils.hasStoragePermission() - color: InputStyle.fontColor - font.pixelSize: InputStyle.fontPixelSizeNormal - font.bold: true - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - padding: InputStyle.panelMargin/2 - } - } - - ListView { - id: merginProjectsList - visible: showMergin && !busyIndicator.running - Layout.fillWidth: true - Layout.fillHeight: true - contentWidth: grid.width - clip: true - maximumFlickVelocity: __androidUtils.isAndroid ? InputStyle.scrollVelocityAndroid : maximumFlickVelocity - - onCountChanged: { - if (merginProjectsList.visible || __merginProjectsModel.lastPage > 1) { - merginProjectsList.positionViewAtIndex(merginProjectsList.currentIndex, ListView.End) - } - } - - property int cellWidth: width - property int cellHeight: projectsPanel.rowHeight - property int borderWidth: 1 - - Label { - anchors.fill: parent - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - visible: !merginProjectsList.contentHeight - text: reloadList.visible ? qsTr("Unable to get the list of projects.") : qsTr("No projects found!") - color: InputStyle.fontColor - font.pixelSize: InputStyle.fontPixelSizeNormal - font.bold: true - } - - delegate: delegateItemMergin - } - } - - Component { - id: delegateItem - - ProjectDelegateItem { - id: delegateItemContent - cellWidth: projectsPanel.width - cellHeight: projectsPanel.rowHeight - iconSize: projectsPanel.iconSize - width: cellWidth - height: passesFilter ? cellHeight : 0 - visible: height ? true : false - statusIconSource:"more_menu.svg" - itemMargin: projectsPanel.panelMargin - projectFullName: (projectNamespace && projectName) ? (projectNamespace + "/" + projectName) : folderName - disabled: !isValid // invalid project - highlight: { - if (disabled) return true - return path === projectsPanel.activeProjectPath ? true : false - } - - Menu { - property real menuItemHeight: projectsPanel.rowHeight * 0.8 - property bool isMerginProject: projectNamespace !== "" - id: contextMenu - height: menuItemHeight * 2 - width:Math.min( parent.width, 300 * QgsQuick.Utils.dp ) - leftMargin: Math.max(parent.width - width, 0) - - //! sets y-offset either above or below related item according relative position to end of the list - onAboutToShow: { - var itemRelativeY = parent.y - grid.contentY - if (itemRelativeY + contextMenu.height >= grid.height) - contextMenu.y = -contextMenu.height - else - contextMenu.y = parent.height - } - - MenuItem { - height: contextMenu.isMerginProject ? contextMenu.menuItemHeight : 0 - ExtendedMenuItem { - height: parent.height - rowHeight: parent.height - width: parent.width - contentText: qsTr("Status") - imageSource: InputStyle.infoIcon - overlayImage: true - } - onClicked: { - if (__merginProjectStatusModel.loadProjectInfo(delegateItemContent.projectFullName)) { - stackView.push(statusPanelComp) - } else __inputUtils.showNotification(qsTr("No Changes")) - } - } - - MenuItem { - height: contextMenu.isMerginProject ? 0: contextMenu.menuItemHeight - ExtendedMenuItem { - height: parent.height - rowHeight: parent.height - width: parent.width - contentText: contextMenu.isMerginProject ? qsTr("Detach from Mergin") : qsTr("Upload to Mergin") - imageSource: contextMenu.isMerginProject ? InputStyle.detachIcon : InputStyle.uploadIcon - overlayImage: true - } - onClicked: { - if (!contextMenu.isMerginProject) { - __merginApi.migrateProjectToMergin(projectName) - } - } - } - - MenuItem { - height: contextMenu.menuItemHeight - ExtendedMenuItem { - height: parent.height - rowHeight: parent.height - width: parent.width - contentText: qsTr("Remove from device") - imageSource: InputStyle.removeIcon - overlayImage: true - } - onClicked: { - deleteDialog.relatedProjectIndex = index - deleteDialog.open() - } - } - } - - onItemClicked: { - if (showMergin) return - - projectsPanel.activeProjectIndex = index - projectsPanel.visible = false - } - - onMenuClicked:contextMenu.open() - } - } - - Component { - id: delegateItemMergin - - ProjectDelegateItem { - cellWidth: projectsPanel.width - cellHeight: projectsPanel.rowHeight - width: cellWidth - height: passesFilter ? cellHeight : 0 - visible: height ? true : false - pending: pendingProject - statusIconSource: getStatusIcon(status, pendingProject) - iconSize: projectsPanel.iconSize - projectFullName: __merginApi.getFullProjectName(projectNamespace, projectName) - progressValue: syncProgress - isAdditional: status === "nonProjectItem" - - onMenuClicked: { - if (status === "upToDate") return - - if (pendingProject) { - if (status === "modified") { - __merginApi.uploadCancel(projectFullName) - } - if (status === "noVersion" || status === "outOfDate") { - __merginApi.updateCancel(projectFullName) - } - return - } - - if ( !__inputUtils.hasStoragePermission() ) { - if ( __inputUtils.acquireStoragePermission() ) - restartAppDialog.open() - return - } - - if (status === "noVersion" || status === "outOfDate") { - var withoutAuth = !__merginApi.userAuth.hasAuthData() && toolbar.highlighted === exploreBtn.text - __merginApi.updateProject(projectNamespace, projectName, withoutAuth) - } else if (status === "modified") { - __merginApi.uploadProject(projectNamespace, projectName) - } - } - - onDelegateButtonClicked: { - var flag = "" - var searchText = "" - if (toolbar.highlighted == myProjectsBtn.text) { - flag = "created" - } else if (toolbar.highlighted == sharedProjectsBtn.text) { - flag = "shared" - } else if (toolbar.highlighted == exploreBtn.text) { - searchText = searchBar.text - } - - // Note that current index used to save last item position - merginProjectsList.currentIndex = merginProjectsList.count - 1 - __merginApi.listProjects(searchText, flag, "", __merginProjectsModel.lastPage + 1) - } - - } - } - - // Toolbar - Rectangle { - property int itemSize: toolbar.height * 0.8 - property string highlighted: homeBtn.text - - id: toolbar - height: InputStyle.rowHeightHeader - width: parent.width - anchors.bottom: parent.bottom - color: InputStyle.clrPanelBackground - - MouseArea { - anchors.fill: parent - onClicked: {} // dont do anything, just do not let click event propagate - } - - onHighlightedChanged: { - searchBar.deactivate() - if (toolbar.highlighted === homeBtn.text) { - __projectsModel.searchExpression = "" - } else { - __merginApi.pingMergin() - } - } - - Row { - height: toolbar.height - width: parent.width - anchors.bottom: parent.bottom - - Item { - width: parent.width/parent.children.length - height: parent.height - - MainPanelButton { - - id: homeBtn - width: toolbar.itemSize - text: qsTr("Home") - imageSource: "home.svg" - faded: toolbar.highlighted !== homeBtn.text - - onActivated: { - toolbar.highlighted = homeBtn.text; - showMergin = false - } - } - } - - Item { - width: parent.width/parent.children.length - height: parent.height - MainPanelButton { - id: myProjectsBtn - width: toolbar.itemSize - text: qsTr("My projects") - imageSource: "account.svg" - faded: toolbar.highlighted !== myProjectsBtn.text - - onActivated: { - toolbar.highlighted = myProjectsBtn.text - stackView.pending = true - showMergin = true - __merginApi.listProjects("", "created") - } - } - } - - Item { - width: parent.width/parent.children.length - height: parent.height - MainPanelButton { - id: sharedProjectsBtn - width: toolbar.itemSize - text: parent.width > sharedProjectsBtn.width * 2 ? qsTr("Shared with me") : qsTr("Shared") - imageSource: "account-multi.svg" - faded: toolbar.highlighted !== sharedProjectsBtn.text - - onActivated: { - toolbar.highlighted = sharedProjectsBtn.text - stackView.pending = true - showMergin = true - __merginApi.listProjects("", "shared") - } - } - } - - Item { - width: parent.width/parent.children.length - height: parent.height - MainPanelButton { - id: exploreBtn - width: toolbar.itemSize - text: qsTr("Explore") - imageSource: "explore.svg" - faded: toolbar.highlighted !== exploreBtn.text - - onActivated: { - toolbar.highlighted = exploreBtn.text - stackView.pending = true - showMergin = true - __merginApi.listProjects( searchBar.text ) - } - } - } - } - } - - // Other components - MessageDialog { - id: deleteDialog - visible: false - property int relatedProjectIndex - - title: qsTr( "Remove project" ) - text: qsTr( "Any unsynchronized changes will be lost." ) - icon: StandardIcon.Warning - standardButtons: StandardButton.Ok | StandardButton.Cancel - - //! Using onButtonClicked instead of onAccepted,onRejected which have been called twice - onButtonClicked: { - if (clickedButton === StandardButton.Ok) { - if (relatedProjectIndex < 0) { - return; - } - __projectsModel.deleteProject(relatedProjectIndex) - if (projectsPanel.activeProjectIndex === relatedProjectIndex) { - __loader.load("") - projectsPanel.activeProjectIndex = -1 - } - deleteDialog.relatedProjectIndex = -1 - visible = false - } - else if (clickedButton === StandardButton.Cancel) { - deleteDialog.relatedProjectIndex = -1 - visible = false - } - } - } - - MessageDialog { - id: restartAppDialog - title: qsTr( "Input needs to be restarted" ) - text: qsTr( "To apply changes after granting storage permission, Input needs to be restarted. Click close and open Input again." ) - icon: StandardIcon.Warning - visible: false - standardButtons: StandardButton.Close - onRejected: __inputUtils.quitApp() - } - - Item { - id: reloadList - width: parent.width - height: grid.cellHeight - visible: false - Layout.alignment: Qt.AlignVCenter - y: projectsPanel.height/3 * 2 - - Button { - id: reloadBtn - width: reloadList.width - 2* InputStyle.panelMargin - height: reloadList.height - text: qsTr("Retry") - font.pixelSize: reloadBtn.height/2 - anchors.horizontalCenter: parent.horizontalCenter - onClicked: { - stackView.pending = true - // filters suppose to not change - __merginApi.listProjects( searchBar.text ) - reloadList.visible = false - } - background: Rectangle { - color: InputStyle.highlightColor - } - - contentItem: Text { - text: reloadBtn.text - font: reloadBtn.font - color: InputStyle.clrPanelMain - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - } - } - } - - } - - Component { - id: authPanelComp - AuthPanel { - id: authPanel - objectName: "authPanel" - visible: false - pending: stackView.pending - height: projectsPanel.height - width: projectsPanel.width - toolbarHeight: InputStyle.rowHeightHeader - onBack: { - stackView.popOnePageOrClose() - if (stackView.currentItem.objectName === "projectsPanel") { - __merginApi.authFailed() // activate homeBtn - } - } - } - } - - - Component { - id: statusPanelComp - ProjectStatusPanel { - id: statusPanel - height: projectsPanel.height - width: projectsPanel.width - visible: false - onBack: stackView.popOnePageOrClose() - } - } - - - Component { - id: accountPanelComp - - AccountPage { - id: accountPanel - height: projectsPanel.height - width: projectsPanel.width - visible: true - onBack: { - stackView.popOnePageOrClose() - } - onManagePlansClicked: { - if (__purchasing.hasInAppPurchases && (__purchasing.hasManageSubscriptionCapability || !__merginApi.userInfo.ownsActiveSubscription )) { - stackView.push( subscribePanelComp) - } else { - Qt.openUrlExternally(__purchasing.subscriptionManageUrl); - } - } - onSignOutClicked: { - if (__merginApi.userAuth.hasAuthData()) { - __merginApi.clearAuth() - stackView.popOnePageOrClose() - } - } - onRestorePurchasesClicked: { - __purchasing.restore() - } - } - } - - - Component { - id: subscribePanelComp - - SubscribePage { - id: subscribePanel - height: projectsPanel.height - width: projectsPanel.width - onBackClicked: { - stackView.popOnePageOrClose() - } - onSubscribeClicked: { - stackView.popOnePageOrClose() - } - } - } - - Component { - id: projectWizardComp - - ProjectWizardPage { - id: projectWizardPanel - objectName: "projectWizard" - height: projectsPanel.height - width: projectsPanel.width - onBack: { - stackView.popOnePageOrClose() - } - } - } - -} diff --git a/app/qml/ProjectDelegateItem.qml b/app/qml/ProjectDelegateItem.qml deleted file mode 100644 index dcfef51ef..000000000 --- a/app/qml/ProjectDelegateItem.qml +++ /dev/null @@ -1,179 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick 2.7 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.0 -import lc 1.0 -import QgsQuick 0.1 as QgsQuick -import "." // import InputStyle singleton -import "./components" - -Rectangle { - id: itemContainer - color: itemContainer.highlight ? InputStyle.panelItemHighlight : itemContainer.primaryColor - - property color primaryColor: InputStyle.clrPanelMain - property color secondaryColor: InputStyle.fontColor - property int cellWidth: width - property int cellHeight: height - property real iconSize: height/2 - property real borderWidth: 1 * QgsQuick.Utils.dp - property bool highlight: false - property bool pending: false - property string statusIconSource: "more_menu.svg" - property string projectFullName // / - property bool disabled: false - property real itemMargin: InputStyle.panelMargin - property real progressValue: 0 - property bool isAdditional: false - - - signal itemClicked(); - signal menuClicked() - signal delegateButtonClicked() - - MouseArea { - anchors.fill: parent - onClicked: if (!disabled) itemClicked() - } - - Rectangle { - id: backgroundRect - visible: disabled - width: parent.width - height: parent.height - color: InputStyle.panelBackgroundDark - } - - Item { - width: parent.width - height: parent.height - visible: !itemContainer.isAdditional - - RowLayout { - id: row - anchors.fill: parent - anchors.leftMargin: itemContainer.itemMargin - spacing: InputStyle.panelMargin - - Item { - id: textContainer - height: itemContainer.cellHeight - Layout.fillWidth: true - - Text { - id: mainText - text: __inputUtils.formatProjectName(itemContainer.projectFullName) - height: textContainer.height/2 - width: textContainer.width - font.pixelSize: InputStyle.fontPixelSizeNormal - color: itemContainer.highlight? itemContainer.primaryColor : itemContainer.secondaryColor - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignBottom - elide: Text.ElideRight - } - - Text { - id: secondaryText - visible: !pending - height: textContainer.height/2 - text: projectInfo ? projectInfo : "" - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.top: mainText.bottom - font.pixelSize: InputStyle.fontPixelSizeSmall - color: itemContainer.highlight ? itemContainer.primaryColor : InputStyle.panelBackgroundDark - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignTop - elide: Text.ElideRight - } - - ProgressBar { - property real itemHeight: InputStyle.fontPixelSizeSmall - - id: progressBar - anchors.top: mainText.bottom - height: InputStyle.fontPixelSizeSmall - width: secondaryText.width - value: progressValue - visible: pending - - background: Rectangle { - implicitWidth: parent.width - implicitHeight: progressBar.itemHeight - color: InputStyle.panelBackgroundLight - } - - contentItem: Item { - implicitWidth: parent.width - implicitHeight: progressBar.itemHeight - - Rectangle { - width: progressBar.visualPosition * parent.width - height: parent.height - color: InputStyle.fontColor - } - } - } - - } - - Item { - id: statusContainer - height: itemContainer.cellHeight - width: height - y: 0 - - MouseArea { - anchors.fill: parent - onClicked:menuClicked() - } - - Image { - id: statusIcon - anchors.centerIn: parent - source: statusIconSource - height: itemContainer.iconSize - width: height - sourceSize.width: width - sourceSize.height: height - fillMode: Image.PreserveAspectFit - } - - ColorOverlay { - anchors.fill: statusIcon - source: statusIcon - color: itemContainer.highlight ? itemContainer.primaryColor : itemContainer.secondaryColor - } - } - } - - Rectangle { - id: borderLine - color: InputStyle.panelBackground2 - width: itemContainer.width - height: itemContainer.borderWidth - anchors.bottom: parent.bottom - } - } - - // Additional item - DelegateButton { // TODO: replace with footer property on projects listview - visible: itemContainer.isAdditional - width: itemContainer.width - height: itemContainer.height - text: qsTr("Fetch more") - - onClicked: itemContainer.delegateButtonClicked() - } - -} diff --git a/app/qml/ProjectListPage.qml b/app/qml/ProjectListPage.qml new file mode 100644 index 000000000..947e235c5 --- /dev/null +++ b/app/qml/ProjectListPage.qml @@ -0,0 +1,62 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick 2.0 +import lc 1.0 + +import "./components" + +Item { + id: root + + property int projectModelType: ProjectsModel.EmptyProjectsModel + property string activeProjectId: "" + + property alias list: projectlist + + signal openProjectRequested( string projectId, string projectFilePath ) + signal showLocalChangesRequested( string projectId ) + + function refreshProjectsList() { + searchBar.deactivate() + projectlist.refreshProjectList() + } + + SearchBar { + id: searchBar + + anchors { + top: parent.top + left: parent.left + right: parent.right + bottom: projectlist.top + } + + allowTimer: true + + onSearchTextChanged: projectlist.searchTextChanged( text ) + } + + ProjectList { + id: projectlist + + projectModelType: root.projectModelType + activeProjectId: root.activeProjectId + + anchors { + left: parent.left + right: parent.right + top: searchBar.bottom + bottom: parent.bottom + } + + onOpenProjectRequested: root.openProjectRequested( projectId, projectFilePath ) + onShowLocalChangesRequested: root.showLocalChangesRequested( projectId ) + } +} diff --git a/app/qml/ProjectPanel.qml b/app/qml/ProjectPanel.qml new file mode 100644 index 000000000..477ad49fd --- /dev/null +++ b/app/qml/ProjectPanel.qml @@ -0,0 +1,1095 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtGraphicalEffects 1.0 +import QtQuick.Dialogs 1.2 +import QgsQuick 0.1 as QgsQuick +import lc 1.0 +import "." // import InputStyle singleton +import "./components/" + +Item { + id: root + + property string activeProjectId: "" + property string activeProjectPath: "" + + property real rowHeight: InputStyle.rowHeightHeader * 1.2 + property real panelMargin: InputStyle.panelMargin + + signal openProjectRequested( string projectId, string projectPath ) + + function openPanel() { + root.visible = true + stackView.visible = true + } + + function hidePanel() { + root.visible = false + stackView.clearStackAndClose() + } + + visible: false + focus: true + + onFocusChanged: { // pass focus to stackview + stackView.focus = true + } + + StackView { + id: stackView + + initialItem: projectsPanelComp + anchors.fill: parent + focus: true + visible: false + z: root.z + 1 + property bool pending: false + + function clearStackAndClose() { + if ( stackView.depth > 1 ) + stackView.pop( null ) // pops everything besides an initialItem + + stackView.visible = false + } + + function popOnePageOrClose() { + if ( stackView.depth > 1 ) + { + stackView.pop() + } + } + + Keys.onReleased: { + if (event.key === Qt.Key_Back || event.key === Qt.Key_Escape) { + event.accepted = true; + + if (stackView.depth > 1) { + stackView.currentItem.back() + } + else if (root.activeProjectPath) { + stackView.clearStackAndClose() + root.visible = false + } + } + } + + onVisibleChanged: { + if ( stackView.visible ) + stackView.forceActiveFocus() + } + } + + BusyIndicator { + id: busyIndicator + width: parent.width/8 + height: width + running: stackView.pending + visible: running + anchors.centerIn: parent + z: parent.z + 1 + } + + Component { + id: projectsPanelComp + + Page { + id: projectsPage + + function setupProjectOpen( projectId, projectPath ) { + activeProjectId = projectId + activeProjectPath = projectPath + openProjectRequested( projectId, projectPath ) + + if ( projectId && projectPath ) // this is not project reset + hidePanel() + } + + function showChanges( projectId ) { + if ( __merginProjectStatusModel.loadProjectInfo( projectId ) ) { + stackView.push( statusPanelComp ) + } + else __inputUtils.showNotification( qsTr( "No Changes" ) ) + } + + function refreshProjectList() { + + stackView.pending = true + switch( pageContent.state ) { + case "local": + localProjectsPage.refreshProjectsList() + break + case "created": + createdProjectsPage.refreshProjectsList() + break + case "shared": + sharedProjectsPage.refreshProjectsList() + break + case "public": + publicProjectsPage.refreshProjectsList() + break + } + } + + header: PanelHeader { + id: pageHeader + + titleText: qsTr("Projects") + color: InputStyle.clrPanelMain + height: InputStyle.rowHeightHeader + rowHeight: InputStyle.rowHeightHeader + + onBack: { + if ( root.activeProjectId ) { + root.hidePanel() + } + } + withBackButton: root.activeProjectPath + + Item { + id: avatar + + width: InputStyle.rowHeightHeader * 0.8 + height: InputStyle.rowHeightHeader + anchors.right: parent.right + anchors.rightMargin: root.panelMargin + + Rectangle { + id: avatarImage + + anchors.centerIn: parent + width: avatar.width + height: avatar.width + color: InputStyle.fontColor + radius: width*0.5 + antialiasing: true + + MouseArea { + anchors.fill: parent + onClicked: { + if (__merginApi.userAuth.hasAuthData() && __merginApi.apiVersionStatus === MerginApiStatus.OK) { + __merginApi.getUserInfo() + stackView.push( accountPanelComp ) + reloadList.visible = false + } + else + stackView.push( authPanelComp, { state: "login" }) + } + } + + Image { + id: userIcon + + anchors.centerIn: avatarImage + source: 'account.svg' + height: avatarImage.height * 0.8 + width: height + sourceSize.width: width + sourceSize.height: height + fillMode: Image.PreserveAspectFit + } + + ColorOverlay { + anchors.fill: userIcon + source: userIcon + color: "#FFFFFF" + } + } + } + } + + background: Rectangle { + anchors.fill: parent + color: InputStyle.clrPanelMain + } + + Item { + id: pageContent + + anchors.fill: parent + + states: [ + State { + name: "local" + }, + State { + name: "created" + }, + State { + name: "shared" + }, + State { + name: "public" + } + ] + + onStateChanged: { + refreshProjectList() + console.log("New state: ", pageContent.state) + } + + StackLayout { + id: projectListLayout + + anchors.fill: parent + currentIndex: pageFooter.currentIndex + + ProjectListPage { + id: localProjectsPage + + projectModelType: ProjectsModel.LocalProjectsModel + activeProjectId: root.activeProjectId + list.visible: !stackView.pending + + onOpenProjectRequested: setupProjectOpen( projectId, projectFilePath ) + onShowLocalChangesRequested: showChanges( projectId ) + list.onActiveProjectDeleted: setupProjectOpen( "", "" ) + } + + ProjectListPage { + id: createdProjectsPage + + projectModelType: ProjectsModel.CreatedProjectsModel + activeProjectId: root.activeProjectId + list.visible: !stackView.pending + + onOpenProjectRequested: setupProjectOpen( projectId, projectFilePath ) + onShowLocalChangesRequested: showChanges( projectId ) + list.onActiveProjectDeleted: setupProjectOpen( "", "" ) + } + + ProjectListPage { + id: sharedProjectsPage + + projectModelType: ProjectsModel.SharedProjectsModel + activeProjectId: root.activeProjectId + list.visible: !stackView.pending + + onOpenProjectRequested: setupProjectOpen( projectId, projectFilePath ) + onShowLocalChangesRequested: showChanges( projectId ) + list.onActiveProjectDeleted: setupProjectOpen( "", "" ) + } + + ProjectListPage { + id: publicProjectsPage + + projectModelType: ProjectsModel.PublicProjectsModel + activeProjectId: root.activeProjectId + list.visible: !stackView.pending + + onOpenProjectRequested: setupProjectOpen( projectId, projectFilePath ) + onShowLocalChangesRequested: showChanges( projectId ) + list.onActiveProjectDeleted: setupProjectOpen( "", "" ) + } + } + } + + footer: TabBar { + id: pageFooter + + property int itemSize: pageFooter.height * 0.8 + + spacing: 0 + contentHeight: InputStyle.rowHeightHeader + + TabButton { + id: localProjectsBtn + + background: Rectangle { + anchors.fill: parent + color: InputStyle.fontColor + } + + MainPanelButton { + id: localProjectsInnerBtn + + text: qsTr("Home") + imageSource: "home.svg" + width: pageFooter.itemSize + + handleClicks: false + faded: pageFooter.currentIndex !== localProjectsBtn.TabBar.index + } + + onClicked: pageContent.state = "local" + } + + TabButton { + id: createdProjectsBtn + + background: Rectangle { + anchors.fill: parent + color: InputStyle.fontColor + } + + MainPanelButton { + id: createdProjectsInnerBtn + + text: qsTr("My projects") + imageSource: "account.svg" + width: pageFooter.itemSize + + handleClicks: false + faded: pageFooter.currentIndex !== createdProjectsBtn.TabBar.index + } + + onClicked: pageContent.state = "created" + } + + TabButton { + id: sharedProjectsBtn + + background: Rectangle { + anchors.fill: parent + color: InputStyle.fontColor + } + + MainPanelButton { + id: sharedProjectsInnerBtn + + imageSource: "account-multi.svg" + width: pageFooter.itemSize + text: parent.width > sharedProjectsInnerBtn.width * 2 ? qsTr("Shared with me") : qsTr("Shared") + + handleClicks: false + faded: pageFooter.currentIndex !== sharedProjectsBtn.TabBar.index + } + + onClicked: pageContent.state = "shared" + } + + TabButton { + id: publicProjectsBtn + + background: Rectangle { + anchors.fill: parent + color: InputStyle.fontColor + } + + MainPanelButton { + id: publicProjectsInnerBtn + + text: qsTr("Explore") + imageSource: "explore.svg" + width: pageFooter.itemSize + + handleClicks: false + faded: pageFooter.currentIndex !== publicProjectsBtn.TabBar.index + } + + onClicked: pageContent.state = "public" + } + } + +// SearchBar { +// id: searchBar +// y: pageHeader.height +// allowTimer: true + +// onSearchTextChanged: { +// if (toolbar.highlighted === homeBtn.text) { +// __localProjectsProxyModel.searchExpression = text +// } else if (toolbar.highlighted === exploreBtn.text) { +// // Filtered by request +// exploreBtn.activated() +// } else if (toolbar.highlighted === sharedProjectsBtn.text) { +// __merginProjectsModel.searchExpression = text +// } else if (toolbar.highlighted === myProjectsBtn.text) { +// __merginProjectsModel.searchExpression = text +// } +// } +// } + +// // Content +// ColumnLayout { +// id: contentLayout +// height: projectsPanel.height-pageHeader.height-searchBar.height-toolbar.height +// width: parent.width +// y: pageHeader.height + searchBar.height +// spacing: 0 + + // Info label +// Item { +// id: infoLabel +// width: parent.width +// height: toolbar.highlighted === exploreBtn.text ? projectsPanel.rowHeight * 2 : 0 +// visible: height + +// Text { +// anchors.horizontalCenter: parent.horizontalCenter +// anchors.verticalCenter: parent.verticalCenter +// horizontalAlignment: Text.AlignHCenter +// verticalAlignment: Text.AlignVCenter +// wrapMode: Text.WordWrap +// color: InputStyle.panelBackgroundDarker +// font.pixelSize: InputStyle.fontPixelSizeNormal +// text: qsTr("Explore public projects.") +// visible: parent.height +// } + +// // To not propagate click on canvas on background +// MouseArea { +// anchors.fill: parent +// } + +// Item { +// id: infoLabelHideBtn +// height: projectsPanel.iconSize +// width: height +// anchors.right: parent.right +// anchors.top: parent.top +// anchors.rightMargin: projectsPanel.panelMargin +// anchors.topMargin: projectsPanel.panelMargin + +// MouseArea { +// anchors.fill: parent +// onClicked: infoLabel.visible = false +// } + +// Image { +// id: infoLabelHide +// anchors.centerIn: infoLabelHideBtn +// source: 'no.svg' +// height: infoLabelHideBtn.height +// width: height +// sourceSize.width: width +// sourceSize.height: height +// fillMode: Image.PreserveAspectFit +// } + +// ColorOverlay { +// anchors.fill: infoLabelHide +// source: infoLabelHide +// color: InputStyle.panelBackgroundDark +// } +// } + +// Rectangle { +// id: borderLine +// color: InputStyle.panelBackground2 +// width: parent.width +// height: 1 * QgsQuick.Utils.dp +// anchors.bottom: parent.bottom +// } +// } + +// ProjectListPage { +// id: localProjectsList + + +// } + +// ListView { +// id: grid +// Layout.fillWidth: true +// Layout.fillHeight: true +// contentWidth: grid.width +// clip: true +// visible: !showMergin +// maximumFlickVelocity: __androidUtils.isAndroid ? InputStyle.scrollVelocityAndroid : maximumFlickVelocity + +// model: ProjectsProxyModel { +// projectSourceModel: ProjectsModel { +// id: myModel +// localProjectsManager: __localProjectsManager +// modelType: ProjectsModel.LocalProjectsModel +// merginApi: __merginApi +// } +// } + +// property int cellWidth: width +// property int cellHeight: projectsPanel.rowHeight +// property int borderWidth: 1 + +// delegate: delegateItem + +// footer: DelegateButton { +// height: projectsPanel.rowHeight +// width: parent.width +// text: qsTr("Create project") +// onClicked: { +// if (__inputUtils.hasStoragePermission()) { +// stackView.push(projectWizardComp) +// } else if (__inputUtils.acquireStoragePermission()) { +// restartAppDialog.open() +// } +// } +// } + +// Text { +// id: noProjectsText +// anchors.fill: parent +// textFormat: Text.RichText +// text: "" + +// qsTr("No downloaded projects found.%1Learn %2how to create projects%3 and %4download them%3 onto your device.") +// .arg("
") +// .arg("") +// .arg("") +// .arg("") + +// onLinkActivated: Qt.openUrlExternally(link) +// visible: grid.count === 0 && !storagePermissionText.visible +// color: InputStyle.fontColor +// font.pixelSize: InputStyle.fontPixelSizeNormal +// font.bold: true +// verticalAlignment: Text.AlignVCenter +// horizontalAlignment: Text.AlignHCenter +// wrapMode: Text.WordWrap +// padding: InputStyle.panelMargin/2 +// } + +// Text { +// id: storagePermissionText +// anchors.fill: parent +// textFormat: Text.RichText +// text: "" + +// qsTr("Input needs a storage permission, %1click to grant it%2 and then restart application.") +// .arg("") +// .arg("") + +// onLinkActivated: { +// if ( __inputUtils.acquireStoragePermission() ) { +// restartAppDialog.open() +// } +// } +// visible: !__inputUtils.hasStoragePermission() +// color: InputStyle.fontColor +// font.pixelSize: InputStyle.fontPixelSizeNormal +// font.bold: true +// verticalAlignment: Text.AlignVCenter +// horizontalAlignment: Text.AlignHCenter +// wrapMode: Text.WordWrap +// padding: InputStyle.panelMargin/2 +// } +// } + +// ListView { +// id: merginProjectsList + +// property int paginatedPage: 0 + +// visible: showMergin && !busyIndicator.running +// Layout.fillWidth: true +// Layout.fillHeight: true +// contentWidth: grid.width +// clip: true +// maximumFlickVelocity: __androidUtils.isAndroid ? InputStyle.scrollVelocityAndroid : maximumFlickVelocity + +// onCountChanged: { +// if (merginProjectsList.visible || paginatedPage > 1) { +// merginProjectsList.positionViewAtIndex(merginProjectsList.currentIndex, ListView.End) +// } +// } + +// property int cellWidth: width +// property int cellHeight: projectsPanel.rowHeight +// property int borderWidth: 1 + +// TODO: unable to get list of the projects +// Label { +// anchors.fill: parent +// horizontalAlignment: Qt.AlignHCenter +// verticalAlignment: Qt.AlignVCenter +// visible: !merginProjectsList.contentHeight +// text: reloadList.visible ? qsTr("Unable to get the list of projects.") : qsTr("No projects found!") +// color: InputStyle.fontColor +// font.pixelSize: InputStyle.fontPixelSizeNormal +// font.bold: true +// } + +// delegate: delegateItemMergin + +// footer: Button { +// text: "ListProjects API" +// onClicked: { +// __myProjectsModel.listProjects(merginProjectsList.paginatedPage + 1) +// merginProjectsList.paginatedPage++ +// } +// } +// } + +// } + +// Component { +// id: delegateItem + +// ProjectDelegateItem { +// id: delegateItemContent + +// projectFullName: model.ProjectFullName +// projectId: model.ProjectId +// projectDescription: model.ProjectDescription +// projectStatus: model.ProjectSyncStatus +// projectIsValid: model.ProjectIsValid +// projectIsPending: model.ProjectPending ? true : false +// projectSyncProgress: model.ProjectSyncProgress ? ProjectSyncProgress : 0 +// projectIsLocal: model.ProjectIsLocal +// projectIsMergin: model.ProjectIsMergin + +// width: parent.width +// height: projectsPanel.rowHeight +// viewContentY: ListView.view.contentY +// viewHeight: ListView.view.height + +// onOpenRequested: console.log( "PMR: Open", projectId ) +// onSyncRequested: console.log( "PMR: Sync", projectId ) +// onMigrateRequested: console.log( "PMR: Upload", projectId ) +// onRemoveRequested: console.log( "PMR: Remove", projectId ) +// onStopSyncRequested: console.log( "PMR: Stop", projectId ) +// onShowChangesRequested: console.log( "PMR: Show changes", projectId ) +// } +// } + +// ProjectDelegateItem { +// id: delegateItemContent +// cellWidth: projectsPanel.width +// cellHeight: projectsPanel.rowHeight +// iconSize: projectsPanel.iconSize +// width: cellWidth +// height: cellHeight +// visible: height ? true : false +// statusIconSource: "more_menu.svg" +// itemMargin: projectsPanel.panelMargin +// projectFullName: ProjectFullName +// projectDescription: getStatusIcon( ProjectSyncStatus, ProjectPending ) +// disabled: !ProjectIsValid // invalid project +// highlight: { +// if (disabled) return true +// return ProjectFilePath === projectsPanel.activeProjectPath ? true : false +// } + +// Menu { +// property real menuItemHeight: projectsPanel.rowHeight * 0.8 +// id: contextMenu +// height: menuItemHeight * 2 +// width:Math.min( parent.width, 300 * QgsQuick.Utils.dp ) +// leftMargin: Math.max(parent.width - width, 0) + +// //! sets y-offset either above or below related item according relative position to end of the list +// onAboutToShow: { +// var itemRelativeY = parent.y - grid.contentY +// if (itemRelativeY + contextMenu.height >= grid.height) +// contextMenu.y = -contextMenu.height +// else +// contextMenu.y = parent.height +// } + +// MenuItem { +// height: ProjectIsMergin ? contextMenu.menuItemHeight : 0 +// ExtendedMenuItem { +// height: parent.height +// rowHeight: parent.height +// width: parent.width +// contentText: qsTr("Status") +// imageSource: InputStyle.infoIcon +// overlayImage: true +// } +// onClicked: { +// if (__merginProjectStatusModel.loadProjectInfo(delegateItemContent.projectFullName)) { +// stackView.push(statusPanelComp) +// } else __inputUtils.showNotification(qsTr("No Changes")) +// } +// } + +// MenuItem { +// height: ProjectIsMergin ? 0: contextMenu.menuItemHeight +// ExtendedMenuItem { +// height: parent.height +// rowHeight: parent.height +// width: parent.width +// contentText: qsTr("Upload to Mergin") +// imageSource: InputStyle.uploadIcon +// overlayImage: true +// } +// onClicked: { +// if (!ProjectIsMergin) { +// __merginApi.migrateProjectToMergin(projectName) +// } +// } +// } + +// MenuItem { +// height: contextMenu.menuItemHeight +// ExtendedMenuItem { +// height: parent.height +// rowHeight: parent.height +// width: parent.width +// contentText: qsTr("Remove from device") +// imageSource: InputStyle.removeIcon +// overlayImage: true +// } +// onClicked: { +// deleteDialog.relatedProjectDirectory = ProjectDirectory +// deleteDialog.open() +// } +// } +// } + +// onItemClicked: { +// if (showMergin) return + +// projectsPanel.activeProjectIndex = index +// projectsPanel.visible = false +// } + +// onMenuClicked:contextMenu.open() +// } + +// Component { +// id: delegateItemMergin + +// Rectangle { +// width: 20 +// height: 20 +// color: "green" +// } + +// ProjectDelegateItem { +// cellWidth: projectsPanel.width +// cellHeight: projectsPanel.rowHeight +// width: cellWidth +// height: cellHeight +// visible: height ? true : false +// pending: ProjectPending +// statusIconSource: getStatusIcon(ProjectSyncStatus, ProjectPending) +// iconSize: projectsPanel.iconSize +// projectFullName: ProjectFullName +// progressValue: ProjectSyncProgress +// projectDescription: ProjectDescription +// isAdditional: __myProjectsModel.canFetchMore() // TODO: replace with delegate button on listview footer + +// onMenuClicked: { +// if ( !__inputUtils.hasStoragePermission() ) { +// if ( __inputUtils.acquireStoragePermission() ) +// restartAppDialog.open() // TODO: replace with reload data! +// return +// } + +// if (ProjectSyncStatus === ProjectStatus.UpToDate) return + +// if ( ProjectPending ) { +// __myProjectsModel.stopProjectSync(ProjectNamespace, ProjectName) +// return +// } + +// __myProjectsModel.syncProject(ProjectNamespace, ProjectName) +// } + +// onDelegateButtonClicked: { // TODO: replace with footer property +// var searchText = searchBar.text + +// // Note that current index used to save last item position +// merginProjectsList.currentIndex = merginProjectsList.count - 1 // TODO: huh? + +// __myProjectsModel.listProjects(merginProjectsList.paginatedPage + 1, searchText) +// } + +// } +// } + + + // Toolbar +// Rectangle { +// property int itemSize: toolbar.height * 0.8 +// property string highlighted: homeBtn.text + +// id: toolbar +// height: InputStyle.rowHeightHeader +// width: parent.width +// anchors.bottom: parent.bottom +// color: InputStyle.clrPanelBackground +// visible: false + +// MouseArea { +// anchors.fill: parent +// onClicked: {} // dont do anything, just do not let click event propagate +// } + +// onHighlightedChanged: { +//// searchBar.deactivate() +// if (toolbar.highlighted === homeBtn.text) { +//// __projectsModel.searchExpression = "" +// } else { +// __merginApi.pingMergin() +// } +// } + +// Row { +// height: toolbar.height +// width: parent.width +// anchors.bottom: parent.bottom + +// Item { +// width: parent.width/parent.children.length +// height: parent.height + +// MainPanelButton { + +// id: homeBtn +// width: toolbar.itemSize +// text: qsTr("Home") +// imageSource: "home.svg" +// faded: toolbar.highlighted !== homeBtn.text + +// onActivated: { +// toolbar.highlighted = homeBtn.text; +// showMergin = false +//// stackView.pending = true +// myModel.listProjectsByName() +// } +// } +// } + +// Item { +// width: parent.width/parent.children.length +// height: parent.height +// MainPanelButton { +// id: myProjectsBtn +// width: toolbar.itemSize +// text: qsTr("My projects") +// imageSource: "account.svg" +// faded: toolbar.highlighted !== myProjectsBtn.text + +// onActivated: { +// toolbar.highlighted = myProjectsBtn.text +// stackView.pending = true +// showMergin = true +// __myProjectsModel.listProjects() +// } +// } +// } + +// Item { +// width: parent.width/parent.children.length +// height: parent.height +// MainPanelButton { +// id: sharedProjectsBtn +// width: toolbar.itemSize +// text: parent.width > sharedProjectsBtn.width * 2 ? qsTr("Shared with me") : qsTr("Shared") +// imageSource: "account-multi.svg" +// faded: toolbar.highlighted !== sharedProjectsBtn.text + +// onActivated: { +// toolbar.highlighted = sharedProjectsBtn.text +// stackView.pending = true +// showMergin = true +// __merginApi.listProjects("", "shared") +// } +// } +// } + +// Item { +// width: parent.width/parent.children.length +// height: parent.height +// MainPanelButton { +// id: exploreBtn +// width: toolbar.itemSize +// text: qsTr("Explore") +// imageSource: "explore.svg" +// faded: toolbar.highlighted !== exploreBtn.text + +// onActivated: { +// toolbar.highlighted = exploreBtn.text +// stackView.pending = true +// showMergin = true +// __merginApi.listProjects( searchBar.text ) +// } +// } +// } +// } +// } + + // Other components + + Item { + id: reloadList + width: parent.width + height: root.rowHeight + visible: false + Layout.alignment: Qt.AlignVCenter + y: root.height/3 * 2 + + Button { + id: reloadBtn + width: reloadList.width - 2* InputStyle.panelMargin + height: reloadList.height + text: qsTr("Retry") + font.pixelSize: reloadBtn.height/2 + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + stackView.pending = true + // filters suppose to not change + __merginApi.listProjects( searchBar.text ) + reloadList.visible = false + } + background: Rectangle { + color: InputStyle.highlightColor + } + + contentItem: Text { + text: reloadBtn.text + font: reloadBtn.font + color: InputStyle.clrPanelMain + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + } + } + + Connections { + target: __projectWizard + onProjectCreated: { + if (stackView.currentItem.objectName === "projectWizard") { + stackView.popOnePageOrClose() + } + } + } + + Connections { + target: __merginApi + onListProjectsFinished: { + stackView.pending = false + } + onListProjectsFailed: { + reloadList.visible = true + } + onListProjectsByNameFinished: stackView.pending = false + onApiVersionStatusChanged: { + stackView.pending = false + if (__merginApi.apiVersionStatus === MerginApiStatus.OK && stackView.currentItem.objectName === "authPanel") { + if (__merginApi.userAuth.hasAuthData()) { + refreshProjectList() + } else if (toolbar.highlighted !== homeBtn.text) { + if (stackView.currentItem.objectName !== "authPanel") { + stackView.push(authPanelComp, {state: "login"}) + } + } + } + } + onAuthRequested: { + stackView.pending = false + stackView.push(authPanelComp, {state: "login"}) + } + onAuthChanged: { + stackView.pending = false + if (__merginApi.userAuth.hasAuthData()) { + refreshProjectList() + root.forceActiveFocus() + } + stackView.popOnePageOrClose() + + } + onAuthFailed: { + homeBtn.activated() + stackView.pending = false + root.forceActiveFocus() + } + onRegistrationFailed: stackView.pending = false + onRegistrationSucceeded: stackView.pending = false + + //TODO: on sync project failed: push auth panel too + } + } + } + + Component { + id: authPanelComp + AuthPanel { + id: authPanel + objectName: "authPanel" + visible: false + pending: stackView.pending + height: root.height + width: root.width + toolbarHeight: InputStyle.rowHeightHeader + onBack: { + stackView.popOnePageOrClose() + if (stackView.currentItem.objectName === "projectsPanel") { + __merginApi.authFailed() // activate homeBtn + } + } + } + } + + Component { + id: statusPanelComp + ProjectStatusPanel { + id: statusPanel + height: root.height + width: root.width + visible: false + onBack: stackView.popOnePageOrClose() + } + } + + Component { + id: accountPanelComp + + AccountPage { + id: accountPanel + height: root.height + width: root.width + visible: true + onBack: { + stackView.popOnePageOrClose() + } + onManagePlansClicked: { + if (__purchasing.hasInAppPurchases && (__purchasing.hasManageSubscriptionCapability || !__merginApi.userInfo.ownsActiveSubscription )) { + stackView.push( subscribePanelComp) + } else { + Qt.openUrlExternally(__purchasing.subscriptionManageUrl); + } + } + onSignOutClicked: { + if (__merginApi.userAuth.hasAuthData()) { + __merginApi.clearAuth() + stackView.popOnePageOrClose() + } + } + onRestorePurchasesClicked: { + __purchasing.restore() + } + } + } + + Component { + id: subscribePanelComp + + SubscribePage { + id: subscribePanel + height: root.height + width: root.width + onBackClicked: { + stackView.popOnePageOrClose() + } + onSubscribeClicked: { + stackView.popOnePageOrClose() + } + } + } + + Component { + id: projectWizardComp + + ProjectWizardPage { + id: projectWizardPanel + objectName: "projectWizard" + height: root.height + width: root.width + onBack: { + stackView.popOnePageOrClose() + } + } + } + +} diff --git a/app/qml/components/ProjectDelegateItem.qml b/app/qml/components/ProjectDelegateItem.qml new file mode 100644 index 000000000..f659bbc0c --- /dev/null +++ b/app/qml/components/ProjectDelegateItem.qml @@ -0,0 +1,367 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 +import QtGraphicalEffects 1.0 +import QtQml.Models 2.14 + +import QgsQuick 0.1 as QgsQuick +import lc 1.0 +import "../" + +Rectangle { + id: root + + // Required properties + property string projectFullName + property string projectId + property string projectDescription + property int projectStatus + property bool projectIsValid + property bool projectIsPending + property real projectSyncProgress + property bool projectIsLocal + property bool projectIsMergin + property string projectRemoteError + + property color primaryColor: InputStyle.clrPanelMain + property color secondaryColor: InputStyle.fontColor + property real itemMargin: InputStyle.panelMargin + + property real iconSize: height * 0.3 + property real borderWidth: 1 * QgsQuick.Utils.dp + property real menuItemHeight: height * 0.8 + + property real viewContentY: 0 + property real viewHeight: 0 + property bool highlight: false + + signal openRequested() + signal syncRequested() + signal migrateRequested() + signal removeRequested() + signal stopSyncRequested() + signal showChangesRequested() + + function getStatusIcon() { + if ( projectIsPending ) + return InputStyle.stopIcon + + if ( projectIsLocal && projectIsMergin ) { + // downloaded mergin projects + if ( projectStatus === ProjectStatus.OutOfDate || + projectStatus === ProjectStatus.Modified ) { + return InputStyle.syncIcon + } + return "" // no icon if this project does not have changes + } + + if ( projectIsMergin && projectStatus === ProjectStatus.NoVersion ) + return InputStyle.downloadIcon + + if ( projectIsLocal && projectStatus === ProjectStatus.NoVersion ) + return InputStyle.uploadIcon + + return "" + } + + function getMoreMenuItems() { + if ( projectIsMergin && projectIsLocal ) + { + if ( ( projectStatus === ProjectStatus.OutOfDate || + projectStatus === ProjectStatus.Modified ) && + projectIsValid ) + return "sync,changes,remove" + + return "changes,remove" + } + else if ( !projectIsMergin && projectIsLocal ) + return "upload,remove" + else + return "download" + } + + function fillMoreMenu() { + // fill more menu with corresponding items + let itemsMap = { + "sync": { + "name": qsTr("Synchronize project"), + "iconSource": InputStyle.syncIcon, + "callback": () => { root.syncRequested() } + }, + "changes": { + "name": qsTr("Local changes"), + "iconSource": InputStyle.infoIcon, + "callback": () => { root.showChangesRequested() } + }, + "remove": { + "name": qsTr("Remove from device"), + "iconSource": InputStyle.removeIcon, + "callback": () => { root.removeRequested() } + }, + "upload": { + "name": qsTr("Upload to Mergin"), + "iconSource": InputStyle.uploadIcon, + "callback": () => { root.migrateRequested() } + }, + "download": { + "name": qsTr("Download from Mergin"), + "iconSource": InputStyle.downloadIcon, + "callback": () => { root.syncRequested() } + } + } + + let items = getMoreMenuItems() + items = items.split(',') + + if ( contextMenu.contentData ) + while( contextMenu.contentData.length > 0 ) + contextMenu.takeItem( 0 ); + + items.forEach( item => { contextMenu.addItem( + menuItemComponent.createObject( null, itemsMap[item] ) ) + }) + contextMenu.height = items.length * root.menuItemHeight + } + + onProjectStatusChanged: { + console.log("Project " + projectId + " status changed to: " + projectStatus) + fillMoreMenu() + } + + color: root.highlight || !projectIsValid ? InputStyle.panelItemHighlight : root.primaryColor + + MouseArea { + anchors.fill: parent + enabled: projectIsValid + onClicked: openRequested() + } + + Rectangle { + visible: !projectIsValid + width: parent.width + height: parent.height + color: InputStyle.panelBackgroundDark + } + + RowLayout { + id: row + + anchors.fill: parent + anchors.leftMargin: root.itemMargin + spacing: 0 + + Item { + id: textContainer + + height: root.height + Layout.fillWidth: true + + Text { + id: mainText + + text: __inputUtils.formatProjectName( projectFullName ) + height: textContainer.height/2 + width: textContainer.width + font.pixelSize: InputStyle.fontPixelSizeNormal + color: root.highlight || !projectIsValid ? root.primaryColor : root.secondaryColor + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + elide: Text.ElideRight + } + + Text { + id: secondaryText + + visible: !projectIsPending + height: textContainer.height/2 +// text: projectDescription + text: projectStatus + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.top: mainText.bottom + font.pixelSize: InputStyle.fontPixelSizeSmall + color: root.highlight || !projectIsValid ? root.primaryColor : InputStyle.panelBackgroundDark + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignTop + elide: Text.ElideRight + } + + ProgressBar { + id: progressBar + + property real itemHeight: InputStyle.fontPixelSizeSmall + + anchors.top: mainText.bottom + height: InputStyle.fontPixelSizeSmall + width: secondaryText.width + value: projectSyncProgress + visible: projectIsPending + + background: Rectangle { + implicitWidth: parent.width + implicitHeight: progressBar.itemHeight + color: InputStyle.panelBackgroundLight + } + + contentItem: Item { + implicitWidth: parent.width + implicitHeight: progressBar.itemHeight + + Rectangle { + width: progressBar.visualPosition * parent.width + height: parent.height + color: InputStyle.fontColor + } + } + } + } + + Item { + id: statusIconContainer + + property string iconSource: getStatusIcon() + + visible: projectIsValid && iconSource !== "" + Layout.preferredWidth: root.height + height: root.height + + Image { + id: statusIcon + + anchors.centerIn: parent + source: statusIconContainer.iconSource + height: root.iconSize + width: height + sourceSize.width: width + sourceSize.height: height + fillMode: Image.PreserveAspectFit + } + + ColorOverlay { + anchors.fill: statusIcon + source: statusIcon + color: root.highlight || !projectIsValid ? root.primaryColor : InputStyle.activeButtonColorOrange + } + + MouseArea { + anchors.fill: parent + enabled: projectIsValid + onClicked: { + if ( projectRemoteError ) { + __inputUtils.showNotification( qsTr( "Could not synchronize project, please make sure you are logged in and have sufficient rights." ) ) + return + } + if ( projectIsPending ) + stopSyncRequested() + else if ( !projectIsMergin ) + migrateRequested() + else + syncRequested() + } + } + } + + Item { + id: moreMenuContainer + + Layout.preferredWidth: root.height + height: root.height + + Image { + id: moreMenuIcon + + anchors.centerIn: parent + source: InputStyle.moreMenuIcon + height: root.iconSize + width: height + sourceSize.width: width + sourceSize.height: height + fillMode: Image.PreserveAspectFit + } + + ColorOverlay { + anchors.fill: moreMenuIcon + source: moreMenuIcon + color: root.highlight || !projectIsValid ? root.primaryColor : InputStyle.activeButtonColorOrange + } + + MouseArea { + anchors.fill: parent + onClicked: contextMenu.open() + } + } + } + + Rectangle { // border line + color: InputStyle.panelBackground2 + width: root.width + height: root.borderWidth + anchors.bottom: parent.bottom + } + + // More Menu + Menu { + id: contextMenu + + width: Math.min( root.width, 300 * QgsQuick.Utils.dp ) + leftMargin: Math.max( root.width - width, 0 ) + z: 100 + + enter: Transition { + ParallelAnimation { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 100 } + } + } + exit: Transition { + ParallelAnimation { + NumberAnimation { property: "opacity"; from: 1.0; to: 0; duration: 100 } + } + } + + Component.onCompleted: fillMoreMenu() + + //! sets y-offset either above or below related item according relative position to end of the list + onAboutToShow: { + let itemRelativeY = parent.y - root.viewContentY + if ( itemRelativeY + contextMenu.height >= root.viewHeight ) + contextMenu.y = -contextMenu.height + parent.height / 3 + else + contextMenu.y = ( parent.height * 2 ) / 3 + } + } + + Component { + id: menuItemComponent + + MenuItem { + id: menuItem + + property string name: "" + property var callback: function cb() {} // default callback + property string iconSource: "" + + height: root.menuItemHeight + + ExtendedMenuItem { + height: parent.height + rowHeight: parent.height * 0.8 + width: parent.width + contentText: menuItem.name + imageSource: menuItem.iconSource + overlayImage: true + } + + onClicked: callback() + } + } +} diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml new file mode 100644 index 000000000..5ed62bb6c --- /dev/null +++ b/app/qml/components/ProjectList.qml @@ -0,0 +1,179 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Dialogs 1.2 +import lc 1.0 +import "../" +import "." + +Item { + id: root + + property int projectModelType: ProjectsModel.EmptyProjectsModel + property string activeProjectId: "" + + signal openProjectRequested( string projectId, string projectFilePath ) + signal showLocalChangesRequested( string projectId ) + signal activeProjectDeleted() + + function searchTextChanged( searchText ) { + if ( projectModelType === ProjectsModel.PublicProjectsModel ) + { + controllerModel.listProjects( searchText ) + } + else viewModel.searchExpression = searchText + } + + function refreshProjectList() { + controllerModel.listProjects() + } + + function modelData( fromRole, fromValue, desiredRole ) { + controllerModel.dataFrom( fromRole, fromValue, desiredRole ) + } + + ListView { + id: listview + + Component.onCompleted: { + // set proper footer (add project / fetch more) + if ( root.projectModelType === ProjectsModel.LocalProjectsModel ) + listview.footer = addProjectButtonComponent + else + listview.footer = fetchMoreButtonComponent + } + + anchors.fill: parent + clip: true + maximumFlickVelocity: __androidUtils.isAndroid ? InputStyle.scrollVelocityAndroid : maximumFlickVelocity + + // Proxy model with source projects model + model: ProjectsProxyModel { + id: viewModel + + projectSourceModel: ProjectsModel { + id: controllerModel + + merginApi: __merginApi + localProjectsManager: __localProjectsManager + modelType: root.projectModelType + } + } + + // Project delegate + delegate: ProjectDelegateItem { + id: projectDelegate + + width: parent.width + height: InputStyle.rowHeightHeader * 1.2 + + projectFullName: model.ProjectFullName + projectId: model.ProjectId + projectDescription: model.ProjectDescription + projectStatus: model.ProjectSyncStatus ? model.ProjectSyncStatus : ProjectStatus.NoVersion + projectIsValid: model.ProjectIsValid + projectIsPending: model.ProjectPending ? model.ProjectPending : false + projectSyncProgress: model.ProjectSyncProgress ? model.ProjectSyncProgress : -1 + projectIsLocal: model.ProjectIsLocal + projectIsMergin: model.ProjectIsMergin + projectRemoteError: model.ProjectRemoteError ? model.ProjectRemoteError : "" + + highlight: model.ProjectId === root.activeProjectId + + viewContentY: ListView.view.contentY + viewHeight: ListView.view.height + + onOpenRequested: root.openProjectRequested( projectId, model.ProjectFilePath ) + onSyncRequested: controllerModel.syncProject( projectId ) + onMigrateRequested: controllerModel.migrateProject( projectId ) + onRemoveRequested: { + removeDialog.relatedProjectId = projectId + removeDialog.open() + } + onStopSyncRequested: controllerModel.stopProjectSync( projectId ) + onShowChangesRequested: root.showLocalChangesRequested( projectId ) + } + } + + Component { + id: fetchMoreButtonComponent + + DelegateButton { + width: parent.width + height: visible ? InputStyle.rowHeight : 0 + text: qsTr( "Fetch more" ) + + visible: controllerModel.hasMoreProjects + onClicked: controllerModel.fetchAnotherPage( viewModel.searchExpression ) + } + } + + Component { + id: addProjectButtonComponent + + DelegateButton { + width: parent.width + height: InputStyle.rowHeight + text: qsTr("Create project") + + onClicked: { + if ( __inputUtils.hasStoragePermission() ) { + stackView.push(projectWizardComp) + } + else if ( __inputUtils.acquireStoragePermission() ) { +// TODO: reload project dir ~> rather connect to permission granted signal and reload project dir + restartAppDialog.open() + } + } + } + } + + MessageDialog { + id: removeDialog + property string relatedProjectId + + title: qsTr( "Remove project" ) + text: qsTr( "Any unsynchronized changes will be lost." ) + icon: StandardIcon.Warning + standardButtons: StandardButton.Ok | StandardButton.Cancel + + //! Using onButtonClicked instead of onAccepted,onRejected which have been called twice + onButtonClicked: { + if (clickedButton === StandardButton.Ok) { + if (relatedProjectId === "") + return + + if ( root.activeProjectId === relatedProjectId ) + root.activeProjectDeleted() + + controllerModel.removeLocalProject( relatedProjectId ) + + removeDialog.relatedProjectId = "" + visible = false + } + else if (clickedButton === StandardButton.Cancel) { + removeDialog.relatedProjectId = "" + visible = false + } + } + } + + MessageDialog { + id: restartAppDialog + + title: qsTr( "Input needs to be restarted" ) + text: qsTr( "To apply changes after granting storage permission, Input needs to be restarted. Click close and open Input again." ) + icon: StandardIcon.Warning + visible: false + standardButtons: StandardButton.Close + onRejected: __inputUtils.quitApp() + } +} diff --git a/app/qml/main.qml b/app/qml/main.qml index 888250e98..ab52c57a4 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -245,21 +245,24 @@ ApplicationWindow { } Component.onCompleted: { - if (__appSettings.defaultProject) { - var path = __appSettings.defaultProject ? __appSettings.defaultProject : openProjectPanel.activeProjectPath - var defaultIndex = __projectsModel.rowAccordingPath(path) - var isValid = __projectsModel.data(__projectsModel.index(defaultIndex), ProjectModel.IsValid) - if (isValid && __loader.load(path)) { - openProjectPanel.activeProjectIndex = defaultIndex !== -1 ? defaultIndex : 0 - __appSettings.activeProject = path - } else { - // if default project load failed, delete default setting - __appSettings.defaultProject = "" - openProjectPanel.openPanel() - } - } else { - openProjectPanel.openPanel() + // load default project + if ( __appSettings.defaultProject ) { + let path = __appSettings.defaultProject + let isValid = __localProjectsManager.projectIsValid( path ) + let id = __localProjectsManager.projectId( path ) + + if ( isValid && __loader.load( path ) ) { + projectPanel.activeProjectPath = path + projectPanel.activeProjectId = id + __appSettings.activeProject = path } + else { + // if default project load failed, delete default setting + __appSettings.defaultProject = "" + projectPanel.openPanel() + } + } + else projectPanel.openPanel() InputStyle.deviceRatio = window.screen.devicePixelRatio InputStyle.realWidth = window.width @@ -441,7 +444,7 @@ ApplicationWindow { gpsIndicatorColor: getGpsIndicatorColor() - onOpenProjectClicked: openProjectPanel.openPanel() + onOpenProjectClicked: projectPanel.openPanel() onOpenMapThemesClicked: mapThemesPanel.visible = true onMyLocationClicked: { mapCanvas.mapSettings.setCenter(positionKit.projectedPosition) @@ -582,26 +585,26 @@ ApplicationWindow { } } - MerginProjectPanel { - id: openProjectPanel + ProjectPanel { + id: projectPanel height: window.height width: window.width z: zPanel onVisibleChanged: { - if (openProjectPanel.visible) - openProjectPanel.forceActiveFocus() + if (projectPanel.visible) + projectPanel.forceActiveFocus() else { mainPanel.forceActiveFocus() } } - onActiveProjectIndexChanged: { - openProjectPanel.activeProjectPath = __projectsModel.data(__projectsModel.index(openProjectPanel.activeProjectIndex), ProjectModel.Path) - __appSettings.defaultProject = openProjectPanel.activeProjectPath - __appSettings.activeProject = openProjectPanel.activeProjectPath - __loader.load(openProjectPanel.activeProjectPath) + onOpenProjectRequested: { + console.log("loading ", projectPath) + __appSettings.defaultProject = projectPath + __appSettings.activeProject = projectPath + __loader.load( projectPath ) } } @@ -680,17 +683,15 @@ ApplicationWindow { var msg = message ? message : qsTr("Failed to communicate with Mergin.%1Try improving your network connection.".arg("
")) showAsDialog ? showDialog(msg) : showMessage(msg) } - onNotify: { - showMessage(message) - } + onNotify: showMessage(message) onProjectDataChanged: { - var projectName = __projectsModel.data(__projectsModel.index(openProjectPanel.activeProjectIndex), ProjectModel.ProjectName) - var projectNamespace = __projectsModel.data(__projectsModel.index(openProjectPanel.activeProjectIndex), ProjectModel.ProjectNamespace) - var currentProjectFullName = __merginApi.getFullProjectName(projectNamespace, projectName) +// var projectName = __projectsModel.data(__projectsModel.index(openProjectPanel.activeProjectIndex), ProjectModel.ProjectName) +// var projectNamespace = __projectsModel.data(__projectsModel.index(openProjectPanel.activeProjectIndex), ProjectModel.ProjectNamespace) +// var currentProjectFullName = __merginApi.getFullProjectName(projectNamespace, projectName) //! if current project has been updated, refresh canvas - if (projectFullName === currentProjectFullName) { + if (projectFullName === projectPanel.activeProjectId) { mapCanvas.mapSettings.extentChanged() } } diff --git a/app/qml/qml.qrc b/app/qml/qml.qrc index ba452f38f..ddada93c7 100644 --- a/app/qml/qml.qrc +++ b/app/qml/qml.qrc @@ -22,13 +22,12 @@ PositionMarker.qml Shadow.qml NumberSpin.qml - ProjectDelegateItem.qml AuthPanel.qml ExternalResourceBundle.qml RecordToolbar.qml RecordCrosshair.qml AboutPanel.qml - MerginProjectPanel.qml + ProjectPanel.qml AccountPage.qml Highlight.qml LoadingIndicator.qml @@ -51,6 +50,11 @@ ValueRelationWidget.qml TextHyperlink.qml LogPanel.qml + CodeReader.qml + CodeReaderHandler.qml + CodeReaderOverlay.qml + Banner.qml + ProjectListPage.qml ProjectWizardPage.qml components/DelegateButton.qml components/FieldRow.qml @@ -59,9 +63,7 @@ components/SettingsSwitch.qml components/SimpleTextWithIcon.qml components/Symbol.qml - CodeReader.qml - CodeReaderHandler.qml - CodeReaderOverlay.qml - Banner.qml + components/ProjectDelegateItem.qml + components/ProjectList.qml From eb51a93403ed386cb705991571da5036bcaca19f Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Wed, 31 Mar 2021 12:25:21 +0200 Subject: [PATCH 21/53] update c++ code according to qml refactor --- app/inpututils.cpp | 33 ++++ app/inpututils.h | 4 + app/localprojectsmanager.cpp | 165 +++++++++++--------- app/localprojectsmanager.h | 82 +--------- app/merginapi.cpp | 17 +-- app/project_future.cpp | 36 +++++ app/project_future.h | 9 +- app/projectsmodel_future.cpp | 241 +++++++++++++++++++----------- app/projectsmodel_future.h | 34 +++-- app/projectsproxymodel_future.cpp | 7 +- 10 files changed, 362 insertions(+), 266 deletions(-) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index 2903add45..f4b75a75a 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -581,6 +581,39 @@ QString InputUtils::renameWithDateTime( const QString &srcPath, const QDateTime return QString(); } +QDateTime InputUtils::getLastModifiedFileDateTime( const QString &path ) +{ + QDateTime lastModified; + QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); + while ( it.hasNext() ) + { + it.next(); + if ( !MerginApi::isInIgnore( it.fileInfo() ) ) + { + if ( it.fileInfo().lastModified() > lastModified ) + { + lastModified = it.fileInfo().lastModified(); + } + } + } + return lastModified.toUTC(); +} + +int InputUtils::getProjectFilesCount( const QString &path ) +{ + int count = 0; + QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); + while ( it.hasNext() ) + { + it.next(); + if ( !MerginApi::isInIgnore( it.fileInfo() ) ) + { + count++; + } + } + return count; +} + QString InputUtils::downloadInProgressFilePath( const QString &projectDir ) { return projectDir + "/.mergin/.project.downloading"; diff --git a/app/inpututils.h b/app/inpututils.h index 6a0e489b7..42895011d 100644 --- a/app/inpututils.h +++ b/app/inpututils.h @@ -171,6 +171,10 @@ class InputUtils: public QObject static void registerInputExpressionFunctions(); static bool removeDir( const QString &projectDir ); + static QDateTime getLastModifiedFileDateTime( const QString &path ); + + static int getProjectFilesCount( const QString &path ); + signals: Q_INVOKABLE void showNotificationRequested( const QString &message ); diff --git a/app/localprojectsmanager.cpp b/app/localprojectsmanager.cpp index b06b6950f..96ebb2105 100644 --- a/app/localprojectsmanager.cpp +++ b/app/localprojectsmanager.cpp @@ -22,7 +22,7 @@ LocalProjectsManager::LocalProjectsManager( const QString &dataDir ) reloadDataDir(); } -void LocalProjectsManager::reloadDataDir() // TODO: maybe add function to reload one specific project +void LocalProjectsManager::reloadDataDir() { mProjects.clear(); QStringList entryList = QDir( mDataDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); @@ -39,16 +39,15 @@ void LocalProjectsManager::reloadDataDir() // TODO: maybe add function to reload info.projectNamespace = metadata.projectNamespace; info.localVersion = metadata.version; } -// else -// { -// info.projectName = folderName; -// } + else + { + info.projectName = folderName; + } mProjects << info; } + qDebug() << "LPM found " << mProjects.size() << " in " << mDataDir; emit dataDirReloaded(); - - qDebug() << "LocalProjectsManager: found" << mProjects.size() << "projects"; } LocalProject_future LocalProjectsManager::projectFromDirectory( const QString &projectDir ) const @@ -119,11 +118,11 @@ void LocalProjectsManager::addMerginProject( const QString &projectDir, const QS addProject( projectDir, projectNamespace, projectName ); } -void LocalProjectsManager::removeLocalProject( const QString &projectDir ) +void LocalProjectsManager::removeLocalProject( const QString &projectId ) { for ( int i = 0; i < mProjects.count(); ++i ) { - if ( mProjects[i].projectDir == projectDir ) + if ( mProjects[i].id() == projectId ) { emit aboutToRemoveLocalProject( mProjects[i] ); @@ -135,6 +134,30 @@ void LocalProjectsManager::removeLocalProject( const QString &projectDir ) } } +bool LocalProjectsManager::projectIsValid( const QString &path ) const +{ + for ( int i = 0; i < mProjects.count(); ++i ) + { + if ( mProjects[i].qgisProjectFilePath == path ) + { + return mProjects[i].projectError.isEmpty(); + } + } + return false; +} + +QString LocalProjectsManager::projectId( const QString &path ) const +{ + for ( int i = 0; i < mProjects.count(); ++i ) + { + if ( mProjects[i].qgisProjectFilePath == path ) + { + return mProjects[i].id(); + } + } + return QString(); +} + //void LocalProjectsManager::removeMerginInfo( const QString &projectFullName ) //{ // for ( int i = 0; i < mProjects.count(); ++i ) @@ -245,7 +268,7 @@ QString LocalProjectsManager::findQgisProjectFile( const QString &projectDir, QS return QString(); } -void LocalProjectsManager::addProject(const QString &projectDir, const QString &projectNamespace, const QString &projectName) +void LocalProjectsManager::addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ) { LocalProject_future project; project.projectDir = projectDir; @@ -257,73 +280,73 @@ void LocalProjectsManager::addProject(const QString &projectDir, const QString & emit localProjectAdded( project ); } -static QDateTime _getLastModifiedFileDateTime( const QString &path ) -{ - QDateTime lastModified; - QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); - while ( it.hasNext() ) - { - it.next(); - if ( !MerginApi::isInIgnore( it.fileInfo() ) ) - { - if ( it.fileInfo().lastModified() > lastModified ) - { - lastModified = it.fileInfo().lastModified(); - } - } - } - return lastModified.toUTC(); -} +//static QDateTime _getLastModifiedFileDateTime( const QString &path ) +//{ +// QDateTime lastModified; +// QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); +// while ( it.hasNext() ) +// { +// it.next(); +// if ( !MerginApi::isInIgnore( it.fileInfo() ) ) +// { +// if ( it.fileInfo().lastModified() > lastModified ) +// { +// lastModified = it.fileInfo().lastModified(); +// } +// } +// } +// return lastModified.toUTC(); +//} -static int _getProjectFilesCount( const QString &path ) -{ - int count = 0; - QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); - while ( it.hasNext() ) - { - it.next(); - if ( !MerginApi::isInIgnore( it.fileInfo() ) ) - { - count++; - } - } - return count; -} +//static int _getProjectFilesCount( const QString &path ) +//{ +// int count = 0; +// QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); +// while ( it.hasNext() ) +// { +// it.next(); +// if ( !MerginApi::isInIgnore( it.fileInfo() ) ) +// { +// count++; +// } +// } +// return count; +//} -ProjectStatus::Status LocalProjectsManager::currentProjectStatus( const std::shared_ptr project ) -{ - if ( !project || !project->isMergin() || !project->isLocal() ) // This is not a Mergin project or not downloaded project - return ProjectStatus::NoVersion; +//ProjectStatus::Status LocalProjectsManager::currentProjectStatus( const std::shared_ptr project ) +//{ +// if ( !project || !project->isMergin() || !project->isLocal() ) // This is not a Mergin project or not downloaded project +// return ProjectStatus::NoVersion; - // There was no sync yet - if ( project->local->localVersion < 0 ) - { - return ProjectStatus::NoVersion; - } +// // There was no sync yet +// if ( project->local->localVersion < 0 ) +// { +// return ProjectStatus::NoVersion; +// } - // - // TODO: this check for local modifications should be revisited - // - - // Something has locally changed after last sync with server - QString metadataFilePath = project->local->projectDir + "/" + MerginApi::sMetadataFile; - QDateTime lastModified = _getLastModifiedFileDateTime( project->local->projectDir ); - QDateTime lastSync = QFileInfo( metadataFilePath ).lastModified(); - MerginProjectMetadata meta = MerginProjectMetadata::fromCachedJson( metadataFilePath ); - int filesCount = _getProjectFilesCount( project->local->projectDir ); - if ( lastSync < lastModified || meta.files.count() != filesCount ) - { - return ProjectStatus::Modified; - } +// // +// // TODO: this check for local modifications should be revisited +// // + +// // Something has locally changed after last sync with server +// QString metadataFilePath = project->local->projectDir + "/" + MerginApi::sMetadataFile; +// QDateTime lastModified = _getLastModifiedFileDateTime( project->local->projectDir ); +// QDateTime lastSync = QFileInfo( metadataFilePath ).lastModified(); +// MerginProjectMetadata meta = MerginProjectMetadata::fromCachedJson( metadataFilePath ); +// int filesCount = _getProjectFilesCount( project->local->projectDir ); +// if ( lastSync < lastModified || meta.files.count() != filesCount ) +// { +// return ProjectStatus::Modified; +// } - // Version is lower than latest one, last sync also before updated - if ( project->local->localVersion < project->mergin->serverVersion ) - { - return ProjectStatus::OutOfDate; - } +// // Version is lower than latest one, last sync also before updated +// if ( project->local->localVersion < project->mergin->serverVersion ) +// { +// return ProjectStatus::OutOfDate; +// } - return ProjectStatus::UpToDate; -} +// return ProjectStatus::UpToDate; +//} //void LocalProjectsManager::updateProjectStatus( LocalProject_future &project ) //{ diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index 0b5a0652f..0aab423a6 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -13,65 +13,19 @@ #include #include -//enum ProjectStatus -//{ -// NoVersion, //!< the project is not available locally -// UpToDate, //!< both server and local copy are in sync with no extra modifications -// OutOfDate, //!< server has newer version than what is available locally (but the project is not modified locally) -// Modified, //!< there are some local modifications in the project that need to be pushed (note: also server may have newer version) -// NonProjectItem //!< only for mock projects, acts like a hook to enable extra functionality for models working with projects . -//}; -//Q_ENUMS( ProjectStatus ) - - -//! Summary information about a local project -//struct LocalProjectInfo -//{ -// bool isValid() const { return !projectDir.isEmpty(); } - -// bool isShowable() const { return qgisProjectError.isEmpty(); } - -// QString projectDir; //!< full path to the project directory - -// QString qgisProjectFilePath; //!< path to the .qgs/.qgz file (or empty if not have exactly one such file) - -// QString qgisProjectError; //!< If project is invalid, projectError carry more information why -// // TODO: reset when project is synchronized - -// // -// // mergin-specific project info (may be empty) -// // - -// QString projectName; -// QString projectNamespace; - -// int localVersion = -1; //!< the project version that is currently available locally -// int serverVersion = -1; //!< the project version most recently seen on server (may be -1 if no info from server is available) - -// ProjectStatus status = NoVersion; - -// bool operator ==( const LocalProjectInfo &other ) { return ( projectName == other.projectName && projectNamespace == other.projectNamespace );} -// bool operator !=( const LocalProjectInfo &other ) { return !( *this == other ); } - -// // Sync status (e.g. progress) is not kept here because if a project does not exist locally yet -// // and it is only being downloaded for the first time, it's not in the list of local projects either -// // and we would need to do some workarounds for that. -//}; - - class LocalProjectsManager : public QObject { Q_OBJECT public: explicit LocalProjectsManager( const QString &dataDir ); + //! Loads all projects from mDataDir, removes all old projects + void reloadDataDir(); + QString dataDir() const { return mDataDir; } LocalProjectsList projects() const { return mProjects; } - //! Loads all projects from mDataDir, removes all old projects - void reloadDataDir(); - LocalProject_future projectFromDirectory( const QString &projectDir ) const; LocalProject_future projectFromProjectFilePath( const QString &projectFilePath ) const; @@ -81,46 +35,25 @@ class LocalProjectsManager : public QObject //! Adds entry about newly created project void addLocalProject( const QString &projectDir, const QString &projectName ); -// bool hasMerginProject( const QString &projectFullName ) const; -// bool hasMerginProject( const QString &projectNamespace, const QString &projectName ) const; - -// void updateProjectStatus( const QString &projectDir ); - //! Adds entry for downloaded project void addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); - //! Should add an entry about newly created local project -// void addLocalProject( const QString &projectDir, const QString &projectName ); - //! Should forget about that project (it has been removed already) - Q_INVOKABLE void removeLocalProject( const QString &projectDir ); + Q_INVOKABLE void removeLocalProject( const QString &projectId ); - //! Resets mergin related info for given project. -// void removeMerginInfo( const QString &projectFullName ); + Q_INVOKABLE bool projectIsValid( const QString &path ) const; - //! Recursively removes project's directory (only when it exists in the list) -// void deleteProjectDirectory( const QString &projectDir ); - - // - // updates of mergin info - // + Q_INVOKABLE QString projectId( const QString &path ) const; //! after successful update/upload - both server and local version are the same void updateLocalVersion( const QString &projectDir, int version ); - //! after receiving project info with server version (local version stays the same -// void updateMerginServerVersion( const QString &projectDir, int version ); - - //! Updates qgisProjectError (after successful project synced) -// void updateProjectErrors( const QString &projectDir, const QString &errMsg ); - //! Updates proejct's namespace void updateNamespace( const QString &projectDir, const QString &projectNamespace ); //! Finds all QGIS project files and set the err variable if any occured. QString findQgisProjectFile( const QString &projectDir, QString &err ); - static ProjectStatus::Status currentProjectStatus( const std::shared_ptr project ); // TODO: maybe move somewhere else? signals: @@ -131,9 +64,6 @@ class LocalProjectsManager : public QObject void localProjectDataChanged( const LocalProject_future &project ); void dataDirReloaded(); -// private: -// void updateProjectStatus( LocalProject_future &project ); // TODO: local project manager should not have status - private: void addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); diff --git a/app/merginapi.cpp b/app/merginapi.cpp index fb9092121..7fe8a593e 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -1875,9 +1875,8 @@ void MerginApi::uploadInfoReplyFinished() transaction.projectDir = projectInfo.projectDir; Q_ASSERT( !transaction.projectDir.isEmpty() ); - MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data ); // get the latest server version from our reply (we do not update it in LocalProjectsManager though... I guess we don't need to) -// projectInfo.serverVersion = serverProject.version; + MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data ); // now let's figure a key question: are we on the most recent version of the project // if we're about to do upload? because if not, we need to do local update first @@ -1893,8 +1892,6 @@ void MerginApi::uploadInfoReplyFinished() QList localFiles = getLocalProjectFiles( transaction.projectDir + "/" ); MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile ); -// mLocalProjects.updateMerginServerVersion( transaction.projectDir, serverProject.version ); - transaction.diff = compareProjectFiles( oldServerProject.files, serverProject.files, localFiles, transaction.projectDir ); InputUtils::log( "push " + projectFullName, transaction.diff.dump() ); @@ -2356,7 +2353,13 @@ MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc ) { for ( auto it = object.begin(); it != object.end(); ++it ) { - result << parseProjectMetadata( it->toObject() ); + MerginProject_future project = parseProjectMetadata( it->toObject() ); + if ( !project.remoteError.isEmpty() ) + { + // add project namespace/name from object name in case of error + MerginApi::extractProjectName( it.key(), project.projectNamespace, project.projectName ); + } + result << project; } } return result; @@ -2436,10 +2439,6 @@ void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSucc // update info of local projects mLocalProjects.updateLocalVersion( transaction.projectDir, transaction.version ); -// mLocalProjects.updateMerginServerVersion( transaction.projectDir, transaction.version ); -// TODO: Is it neccessary to update server version at all? -// emit updateServerVersion( transaction.projectDir, transaction.version ); - InputUtils::log( "sync " + projectFullName, QStringLiteral( "### Finished ### New project version: %1\n" ).arg( transaction.version ) ); } else diff --git a/app/project_future.cpp b/app/project_future.cpp index c278f0272..d7406850c 100644 --- a/app/project_future.cpp +++ b/app/project_future.cpp @@ -9,6 +9,7 @@ #include "project_future.h" #include "merginapi.h" +#include "inpututils.h" QString LocalProject_future::id() const { @@ -23,3 +24,38 @@ QString MerginProject_future::id() const { return MerginApi::getFullProjectName( projectNamespace, projectName ); } + +ProjectStatus::Status ProjectStatus::projectStatus( const std::shared_ptr project ) +{ + if ( !project || !project->isMergin() || !project->isLocal() ) // This is not a Mergin project or not downloaded project + return ProjectStatus::NoVersion; + + // There was no sync yet + if ( project->local->localVersion < 0 ) + { + return ProjectStatus::NoVersion; + } + + // + // TODO: this check for local modifications should be revisited + // + + // Something has locally changed after last sync with server + QString metadataFilePath = project->local->projectDir + "/" + MerginApi::sMetadataFile; + QDateTime lastModified = InputUtils::getLastModifiedFileDateTime( project->local->projectDir ); + QDateTime lastSync = QFileInfo( metadataFilePath ).lastModified(); + MerginProjectMetadata meta = MerginProjectMetadata::fromCachedJson( metadataFilePath ); + int filesCount = InputUtils::getProjectFilesCount( project->local->projectDir ); + if ( lastSync < lastModified || meta.files.count() != filesCount ) + { + return ProjectStatus::Modified; + } + + // Version is lower than latest one, last sync also before updated + if ( project->local->localVersion < project->mergin->serverVersion ) + { + return ProjectStatus::OutOfDate; + } + + return ProjectStatus::UpToDate; +} diff --git a/app/project_future.h b/app/project_future.h index b6ffc43b5..87c433158 100644 --- a/app/project_future.h +++ b/app/project_future.h @@ -16,18 +16,21 @@ #include #include +struct Project_future; + namespace ProjectStatus { Q_NAMESPACE enum Status { - NoVersion, //!< the project is not available locally + NoVersion, //!< the project is not downloaded UpToDate, //!< both server and local copy are in sync with no extra modifications OutOfDate, //!< server has newer version than what is available locally (but the project is not modified locally) - Modified, //!< there are some local modifications in the project that need to be pushed (note: also server may have newer version) - + Modified //!< there are some local modifications in the project that need to be pushed (note: also server may have newer version) // Maybe orphaned state in future }; Q_ENUM_NS( Status ) + + Status projectStatus( const std::shared_ptr project ); } struct LocalProject_future diff --git a/app/projectsmodel_future.cpp b/app/projectsmodel_future.cpp index 8e99f552c..042a1a8b2 100644 --- a/app/projectsmodel_future.cpp +++ b/app/projectsmodel_future.cpp @@ -64,11 +64,11 @@ QVariant ProjectsModel_future::data( const QModelIndex &index, int role ) const switch ( role ) { case ProjectName: return QVariant( project->projectName() ); case ProjectNamespace: return QVariant( project->projectNamespace() ); - case ProjectFullName: return MerginApi::getFullProjectName( project->projectNamespace(), project->projectName() ); + case ProjectFullName: return QVariant( project->projectId() ); case ProjectId: return QVariant( project->projectId() ); case ProjectIsLocal: return QVariant( project->isLocal() ); case ProjectIsMergin: return QVariant( project->isMergin() ); - case ProjectSyncStatus: return QVariant( LocalProjectsManager::currentProjectStatus( project ) ); + case ProjectSyncStatus: return QVariant( project->isMergin() ? project->mergin->status : ProjectStatus::NoVersion ); case ProjectFilePath: return QVariant( project->isLocal() ? project->local->qgisProjectFilePath : QString() ); case ProjectDirectory: return QVariant( project->isLocal() ? project->local->projectDir : QString() ); case ProjectIsValid: { @@ -111,23 +111,13 @@ QModelIndex ProjectsModel_future::index( int row, int col, const QModelIndex &pa return createIndex( row, 0, nullptr ); } -bool ProjectsModel_future::canFetchMore( const QModelIndex & ) const -{ -// return mServerProjectsCount > mProjects.size(); - return true; -} - -void ProjectsModel_future::fetchMore( const QModelIndex & ) -{ - -} - QHash ProjectsModel_future::roleNames() const { QHash roles; roles[Roles::ProjectName] = QStringLiteral( "ProjectName" ).toLatin1(); roles[Roles::ProjectNamespace] = QStringLiteral( "ProjectNamespace" ).toLatin1(); roles[Roles::ProjectFullName] = QStringLiteral( "ProjectFullName" ).toLatin1(); + roles[Roles::ProjectId] = QStringLiteral( "ProjectId" ).toLatin1(); roles[Roles::ProjectDirectory] = QStringLiteral( "ProjectDirectory" ).toLatin1(); roles[Roles::ProjectIsLocal] = QStringLiteral( "ProjectIsLocal" ).toLatin1(); roles[Roles::ProjectIsMergin] = QStringLiteral( "ProjectIsMergin" ).toLatin1(); @@ -146,7 +136,7 @@ int ProjectsModel_future::rowCount( const QModelIndex & ) const return mProjects.count(); } -void ProjectsModel_future::listProjects( int page, QString searchExpression ) +void ProjectsModel_future::listProjects( const QString &searchExpression, int page ) { if ( mModelType == LocalProjectsModel ) { @@ -154,7 +144,7 @@ void ProjectsModel_future::listProjects( int page, QString searchExpression ) return; } - mLastRequestId = mBackend->listProjects( "", modelTypeToFlag(), searchExpression, page ); + mLastRequestId = mBackend->listProjects( searchExpression, modelTypeToFlag(), "", page ); } void ProjectsModel_future::listProjectsByName() @@ -167,6 +157,99 @@ void ProjectsModel_future::listProjectsByName() mLastRequestId = mBackend->listProjectsByName( projectNames() ); } +bool ProjectsModel_future::hasMoreProjects() const +{ + if ( mProjects.size() < mServerProjectsCount ) + return true; + + return false; +} + +void ProjectsModel_future::fetchAnotherPage( const QString &searchExpression ) +{ + listProjects( searchExpression, mPaginatedPage + 1 ); +} + +QVariant ProjectsModel_future::dataFrom( int fromRole, QVariant fromValue, int desiredRole ) const +{ + switch ( fromRole ) + { + case ProjectId: + { + std::shared_ptr project = projectFromId( fromValue.toString() ); + if ( project ) + { + QModelIndex ix = index( mProjects.indexOf( project ) ); + return data( ix, desiredRole ); + } + return QVariant(); + } + + case ProjectFilePath: + { + for ( int i = 0; i < mProjects.size(); i++ ) + { + if ( mProjects[i]->isLocal() && mProjects[i]->local->qgisProjectFilePath == fromValue.toString() ) + { + QModelIndex ix = index( i ); + return data( ix, desiredRole ); + } + } + } + default: return QVariant(); + } + + return QVariant(); +} + +void ProjectsModel_future::onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ) +{ + qDebug() << "PMR: onListProjectsFinished(): received response with requestId = " << requestId; + if ( mLastRequestId != requestId ) + { + qDebug() << "PMR: onListProjectsFinished(): ignoring request with id " << requestId; + return; + } + + qDebug() << "PMR: onListProjectsFinished(): project count = " << projectsCount << " but mergin projects emited: " << merginProjects.size(); + + if ( page == 1 ) + { + // if we are populating first page, reset model and throw away previous projects + beginResetModel(); + mergeProjects( merginProjects, pendingProjects, false ); + printProjects(); + endResetModel(); + } + else + { + // paginating next page, keep previous projects and emit model add items + beginInsertRows( QModelIndex(), mProjects.size(), mProjects.size() + merginProjects.size() - 1 ); + mergeProjects( merginProjects, pendingProjects, true ); + printProjects(); + endInsertRows(); + } + + mServerProjectsCount = projectsCount; + mPaginatedPage = page; + emit hasMoreProjectsChanged(); +} + +void ProjectsModel_future::onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ) +{ + qDebug() << "PMR: onListProjectsByNameFinished(): received response with requestId = " << requestId; + if ( mLastRequestId != requestId ) + { + qDebug() << "PMR: onListProjectsByNameFinished(): ignoring request with id " << requestId; + return; + } + + beginResetModel(); + mergeProjects( merginProjects, pendingProjects ); + printProjects(); + endResetModel(); +} + void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious ) { LocalProjectsList localProjects = mLocalProjectsManager->projects(); @@ -182,9 +265,7 @@ void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjec for ( const auto &localProject : localProjects ) { std::shared_ptr project = std::shared_ptr( new Project_future() ); - std::unique_ptr local = std::unique_ptr( new LocalProject_future( localProject ) ); - - project->local = std::move( local ); + project->local = std::unique_ptr( new LocalProject_future( localProject ) ); MerginProject_future remoteEntry; remoteEntry.projectName = project->local->projectName; @@ -201,6 +282,7 @@ void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjec project->mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; project->mergin->pending = true; } + project->mergin->status = ProjectStatus::projectStatus( project ); } else if ( project->local->localVersion > -1 ) { @@ -208,6 +290,7 @@ void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjec project->mergin = std::unique_ptr( new MerginProject_future() ); project->mergin->projectName = project->local->projectName; project->mergin->projectNamespace = project->local->projectNamespace; + project->mergin->status = ProjectStatus::projectStatus( project ); } mProjects << project; @@ -238,70 +321,32 @@ void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjec int ix = localProjects.indexOf( localProject ); project->local = std::unique_ptr( new LocalProject_future( localProjects[ix] ) ); } + project->mergin->status = ProjectStatus::projectStatus( project ); mProjects << project; } } } -int ProjectsModel_future::serverProjectsCount() const -{ - return mServerProjectsCount; -} - -void ProjectsModel_future::onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ) -{ - qDebug() << "PMR: onListProjectsFinished(): received response with requestId = " << requestId; - if ( mLastRequestId != requestId ) - { - qDebug() << "PMR: onListProjectsFinished(): ignoring request with id " << requestId; - return; - } - - setServerProjectsCount( projectsCount ); - - qDebug() << "PMR: onListProjectsFinished(): project count = " << projectsCount << " but mergin projects emited: " << merginProjects.size(); - - beginResetModel(); - mergeProjects( merginProjects, pendingProjects, page != 1 ); // throw projects only if paginating first page - printProjects(); - endResetModel(); -} - -void ProjectsModel_future::onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ) -{ - qDebug() << "PMR: onListProjectsByNameFinished(): received response with requestId = " << requestId; - if ( mLastRequestId != requestId ) - { - qDebug() << "PMR: onListProjectsByNameFinished(): ignoring request with id " << requestId; - return; - } - - beginResetModel(); - mergeProjects( merginProjects, pendingProjects ); - printProjects(); - endResetModel(); -} - -void ProjectsModel_future::syncProject( const QString &projectNamespace, const QString &projectName ) +void ProjectsModel_future::syncProject( const QString &projectId ) { - std::shared_ptr project = projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + std::shared_ptr project = projectFromId( projectId ); if ( project == nullptr ) { - qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "not in projects list"; + qDebug() << "PMR: project" << projectId << "not in projects list"; return; } if ( !project->isMergin() ) { - qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "is not a mergin project"; + qDebug() << "PMR: project" << projectId << "is not a mergin project"; return; } if ( project->mergin->pending ) { - qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "is already syncing"; + qDebug() << "PMR: project" << projectId << "is already syncing"; return; } @@ -309,35 +354,35 @@ void ProjectsModel_future::syncProject( const QString &projectNamespace, const Q { qDebug() << "PMR: updating project:" << project->mergin->id(); - bool useAuth = !mBackend->userAuth()->hasAuthData() && mModelType == ProjectModelTypes::ExploreProjectsModel; - mBackend->updateProject( projectNamespace, projectName, useAuth ); + bool useAuth = !mBackend->userAuth()->hasAuthData() && mModelType == ProjectModelTypes::PublicProjectsModel; + mBackend->updateProject( project->mergin->projectNamespace, project->mergin->projectName, useAuth ); } else if ( project->mergin->status == ProjectStatus::Modified ) { qDebug() << "PMR: uploading project:" << project->mergin->id(); - mBackend->uploadProject( projectNamespace, projectName ); + mBackend->uploadProject( project->mergin->projectNamespace, project->mergin->projectName ); } } -void ProjectsModel_future::stopProjectSync( const QString &projectNamespace, const QString &projectName ) +void ProjectsModel_future::stopProjectSync( const QString &projectId ) { - std::shared_ptr project = projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + std::shared_ptr project = projectFromId( projectId ); if ( project == nullptr ) { - qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "not in projects list"; + qDebug() << "PMR: project" << projectId << "not in projects list"; return; } if ( !project->isMergin() ) { - qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "is not a mergin project"; + qDebug() << "PMR: project" << projectId << "is not a mergin project"; return; } if ( !project->mergin->pending ) { - qDebug() << "PMR: project" << MerginApi::getFullProjectName(projectNamespace, projectName) << "is not pending"; + qDebug() << "PMR: project" << projectId << "is not pending"; return; } @@ -353,9 +398,18 @@ void ProjectsModel_future::stopProjectSync( const QString &projectNamespace, con } } -void ProjectsModel_future::removeLocalProject( const QString &projectDir ) +void ProjectsModel_future::removeLocalProject( const QString &projectId ) +{ + mLocalProjectsManager->removeLocalProject( projectId ); +} + +void ProjectsModel_future::migrateProject( const QString &projectId ) { - mLocalProjectsManager->removeLocalProject( projectDir ); + // if it is indeed a local project + std::shared_ptr project = projectFromId( projectId ); + + if ( project->isLocal() ) + mBackend->migrateProjectToMergin( project->local->projectName ); } void ProjectsModel_future::onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully ) @@ -398,6 +452,8 @@ void ProjectsModel_future::onProjectAdded( const LocalProject_future &project ) { // add local information ~ project downloaded proj->local = std::unique_ptr( new LocalProject_future( project ) ); + if ( proj->isMergin() ) + proj->mergin->status = ProjectStatus::projectStatus( proj ); QModelIndex ix = index( mProjects.indexOf( proj ) ); emit dataChanged( ix, ix ); @@ -424,13 +480,27 @@ void ProjectsModel_future::onAboutToRemoveProject( const LocalProject_future pro if ( proj ) { - int removeIndex = mProjects.indexOf( proj ); + if ( mModelType == LocalProjectsModel ) + { + int removeIndex = mProjects.indexOf( proj ); + + beginRemoveRows( QModelIndex(), removeIndex, removeIndex ); + mProjects.removeOne( proj ); + endRemoveRows(); + + qDebug() << "PMR: Deleted project" << project.id(); + } + else + { + // just remove local part + proj->local.reset(); - beginRemoveRows( QModelIndex(), removeIndex, removeIndex ); - mProjects.removeOne( proj ); - endRemoveRows(); + if ( proj->isMergin() ) + proj->mergin->status = ProjectStatus::projectStatus( proj ); - qDebug() << "PMR: Deleted project" << project.id(); + QModelIndex ix = index( mProjects.indexOf( proj ) ); + emit dataChanged( ix, ix ); + } } } @@ -441,6 +511,9 @@ void ProjectsModel_future::onProjectDataChanged( const LocalProject_future &proj if ( proj ) { proj->local = std::unique_ptr( new LocalProject_future( project ) ); + if ( proj->isMergin() ) + proj->mergin->status = ProjectStatus::projectStatus( proj ); + QModelIndex editIndex = index( mProjects.indexOf( proj ) ); emit dataChanged( editIndex, editIndex ); @@ -454,7 +527,7 @@ void ProjectsModel_future::onProjectDetachedFromMergin( const QString &projectFu if ( proj ) { - proj->mergin = nullptr; + proj->mergin.reset(); QModelIndex editIndex = index( mProjects.indexOf( proj ) ); emit dataChanged( editIndex, editIndex ); @@ -474,15 +547,6 @@ void ProjectsModel_future::onProjectAttachedToMergin( const QString &projectFull qDebug() << "PMR: Project attached to mergin " << projectFullName; } -void ProjectsModel_future::setServerProjectsCount( int serverProjectsCount ) -{ - if ( mServerProjectsCount == serverProjectsCount ) - return; - - mServerProjectsCount = serverProjectsCount; - emit serverProjectsCountChanged( mServerProjectsCount ); -} - void ProjectsModel_future::setMerginApi( MerginApi *merginApi ) { if ( !merginApi || mBackend == merginApi ) @@ -517,7 +581,7 @@ QString ProjectsModel_future::modelTypeToFlag() const { switch ( mModelType ) { - case MyProjectsModel: + case CreatedProjectsModel: return QStringLiteral( "created" ); case SharedProjectsModel: return QStringLiteral( "shared" ); @@ -565,6 +629,7 @@ void ProjectsModel_future::loadLocalProjects() { beginResetModel(); mergeProjects( MerginProjectsList(), Transactions() ); // Fills model with local projects + printProjects(); endResetModel(); } } diff --git a/app/projectsmodel_future.h b/app/projectsmodel_future.h index e03ba39c0..2dbabfb32 100644 --- a/app/projectsmodel_future.h +++ b/app/projectsmodel_future.h @@ -53,9 +53,9 @@ class ProjectsModel_future : public QAbstractListModel { EmptyProjectsModel = 0, // default, holding no projects ~ invalid model LocalProjectsModel, - MyProjectsModel, + CreatedProjectsModel, SharedProjectsModel, - ExploreProjectsModel, + PublicProjectsModel, RecentProjectsModel }; Q_ENUM( ProjectModelTypes ) @@ -63,47 +63,52 @@ class ProjectsModel_future : public QAbstractListModel ProjectsModel_future( QObject *parent = nullptr ); ~ProjectsModel_future() override {}; - Q_PROPERTY( int serverProjectsCount READ serverProjectsCount WRITE setServerProjectsCount NOTIFY serverProjectsCountChanged ) // TODO: replace with builtin canFetchMore - // From Qt 5.15 we can use REQUIRED keyword here that will ensure object will be always instantiated from QML with these mandatory properties Q_PROPERTY( MerginApi *merginApi READ merginApi WRITE setMerginApi ) Q_PROPERTY( LocalProjectsManager *localProjectsManager READ localProjectsManager WRITE setLocalProjectsManager ) Q_PROPERTY( ProjectModelTypes modelType READ modelType WRITE setModelType ) + Q_PROPERTY( bool hasMoreProjects READ hasMoreProjects NOTIFY hasMoreProjectsChanged ) + // Needed methods from QAbstractListModel Q_INVOKABLE QVariant data( const QModelIndex &index, int role ) const override; Q_INVOKABLE QModelIndex index( int row, int column = 0, const QModelIndex &parent = QModelIndex() ) const override; - Q_INVOKABLE bool canFetchMore( const QModelIndex &parent ) const override; - Q_INVOKABLE void fetchMore( const QModelIndex &parent ) override; QHash roleNames() const override; int rowCount( const QModelIndex &parent = QModelIndex() ) const override; //! Called to list projects, either fetch more or get first - Q_INVOKABLE void listProjects( int page = 1, const QString searchExpression = QString() ); + Q_INVOKABLE void listProjects( const QString &searchExpression = QString(), int page = 1 ); //! Called to list projects, either fetch more or get first Q_INVOKABLE void listProjectsByName(); //! Syncs specified project - upload or update - Q_INVOKABLE void syncProject( const QString &projectNamespace, const QString &projectName ); + Q_INVOKABLE void syncProject( const QString &projectId ); //! Stops running project upload or update - Q_INVOKABLE void stopProjectSync( const QString &projectNamespace, const QString &projectName ); + Q_INVOKABLE void stopProjectSync( const QString &projectId ); //! Forwards call to LocalProjectsManager to remove local project - Q_INVOKABLE void removeLocalProject( const QString &projectDir ); + Q_INVOKABLE void removeLocalProject( const QString &projectId ); + + //! Migrates local project to mergin + Q_INVOKABLE void migrateProject( const QString &projectId ); + + Q_INVOKABLE void fetchAnotherPage( const QString &searchExpression ); + + Q_INVOKABLE QVariant dataFrom( int fromRole, QVariant fromValue, int desiredRole ) const; //! Method merging local and remote projects based on the model type void mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious = false ); - int serverProjectsCount() const; - ProjectsModel_future::ProjectModelTypes modelType() const; MerginApi *merginApi() const { return mBackend; } LocalProjectsManager *localProjectsManager() const { return mLocalProjectsManager; } + bool hasMoreProjects() const; + public slots: // MerginAPI - backend signals void onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ); @@ -118,17 +123,15 @@ public slots: void onAboutToRemoveProject( const LocalProject_future project ); void onProjectDataChanged( const LocalProject_future &project ); - void setServerProjectsCount( int serverProjectsCount ); void setMerginApi( MerginApi *merginApi ); void setLocalProjectsManager( LocalProjectsManager *localProjectsManager ); void setModelType( ProjectModelTypes modelType ); signals: - void serverProjectsCountChanged( int serverProjectsCount ); void modelInitialized(); + void hasMoreProjectsChanged(); private: - QString modelTypeToFlag() const; void printProjects() const; QStringList projectNames() const; @@ -146,6 +149,7 @@ public slots: //! For pagination int mServerProjectsCount = -1; + int mPaginatedPage = 1; //! For processing only my requests QString mLastRequestId; diff --git a/app/projectsproxymodel_future.cpp b/app/projectsproxymodel_future.cpp index 3afb20571..8c3332b9d 100644 --- a/app/projectsproxymodel_future.cpp +++ b/app/projectsproxymodel_future.cpp @@ -11,12 +11,10 @@ ProjectsProxyModel_future::ProjectsProxyModel_future( QObject *parent ) : QSortFilterProxyModel( parent ) { - qDebug() << "PMR: Building proxy model " << this; } void ProjectsProxyModel_future::initialize() { - qDebug() << "PMR: Initializing proxy model" << this; setSourceModel( mModel ); mModelType = mModel->modelType(); @@ -72,16 +70,17 @@ bool ProjectsProxyModel_future::lessThan( const QModelIndex &left, const QModelI { QString lProjectFullName = mModel->data( left, ProjectsModel_future::ProjectFullName ).toString(); QString rProjectFullName = mModel->data( right, ProjectsModel_future::ProjectFullName ).toString(); + qDebug() << "Comparing " << lProjectFullName << rProjectFullName; return lProjectFullName.compare( rProjectFullName, Qt::CaseInsensitive ) < 0; } if ( !lProjectIsMergin && rProjectIsMergin ) { - return false; + return true; } if ( lProjectIsMergin && !rProjectIsMergin ) { - return true; + return false; } QString lNamespace = mModel->data( left, ProjectsModel_future::ProjectNamespace ).toString(); From 7d91f1d25d19c78e30c3070d9c385b44409596fc Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Wed, 31 Mar 2021 12:35:18 +0200 Subject: [PATCH 22/53] removed unused code --- app/localprojectsmanager.cpp | 147 +------------------- app/localprojectsmanager.h | 2 - app/main.cpp | 36 +---- app/merginapi.cpp | 29 +--- app/merginapi.h | 37 +---- app/merginprojectmodel.cpp | 254 ----------------------------------- app/merginprojectmodel.h | 117 ---------------- app/projectsmodel.cpp | 205 ---------------------------- app/projectsmodel.h | 125 ----------------- app/sources.pri | 4 - 10 files changed, 10 insertions(+), 946 deletions(-) delete mode 100644 app/merginprojectmodel.cpp delete mode 100644 app/merginprojectmodel.h delete mode 100644 app/projectsmodel.cpp delete mode 100644 app/projectsmodel.h diff --git a/app/localprojectsmanager.cpp b/app/localprojectsmanager.cpp index 96ebb2105..277e20829 100644 --- a/app/localprojectsmanager.cpp +++ b/app/localprojectsmanager.cpp @@ -46,7 +46,8 @@ void LocalProjectsManager::reloadDataDir() mProjects << info; } - qDebug() << "LPM found " << mProjects.size() << " in " << mDataDir; + + qDebug() << "Found " << mProjects.size() << " local projects in " << mDataDir; emit dataDirReloaded(); } @@ -85,29 +86,6 @@ LocalProject_future LocalProjectsManager::projectFromMerginName( const QString & return projectFromMerginName( MerginApi::getFullProjectName( projectNamespace, projectName ) ); } -//bool LocalProjectsManager::hasMerginProject( const QString &projectFullName ) const -//{ -// return projectFromMerginName( projectFullName ).isValid(); -//} - -//bool LocalProjectsManager::hasMerginProject( const QString &projectNamespace, const QString &projectName ) const -//{ -// return hasMerginProject( MerginApi::getFullProjectName( projectNamespace, projectName ) ); -//} - -//void LocalProjectsManager::updateProjectStatus( const QString &projectDir ) -//{ -// for ( LocalProject_future &info : mProjects ) -// { -// if ( info.projectDir == projectDir ) -// { -// updateProjectStatus( info ); -// return; -// } -// } -// Q_ASSERT( false ); // should not happen -//} - void LocalProjectsManager::addLocalProject( const QString &projectDir, const QString &projectName ) { addProject( projectDir, QString(), projectName ); @@ -158,22 +136,6 @@ QString LocalProjectsManager::projectId( const QString &path ) const return QString(); } -//void LocalProjectsManager::removeMerginInfo( const QString &projectFullName ) -//{ -// for ( int i = 0; i < mProjects.count(); ++i ) -// { -// if ( mProjects[i].id() == projectFullName ) -// { -// mProjects[i].localVersion = -1; -// mProjects[i].projectNamespace.clear(); -// InputUtils::removeDir( mProjects[i].projectDir + "/.mergin" ); - -// emit localProjectDataChanged( mProjects[i].projectDir ); -// return; -// } -// } -//} - void LocalProjectsManager::updateLocalVersion( const QString &projectDir, int version ) { for ( int i = 0; i < mProjects.count(); ++i ) @@ -189,33 +151,6 @@ void LocalProjectsManager::updateLocalVersion( const QString &projectDir, int ve Q_ASSERT( false ); // should not happen } -//void LocalProjectsManager::updateMerginServerVersion( const QString &projectDir, int version ) -//{ -// for ( int i = 0; i < mProjects.count(); ++i ) -// { -// if ( mProjects[i].projectDir == projectDir ) -// { -// mProjects[i].serverVersion = version; -// updateProjectStatus( mProjects[i] ); -// return; -// } -// } -// Q_ASSERT( false ); // should not happen -//} - -//void LocalProjectsManager::updateProjectErrors( const QString &projectDir, const QString &errMsg ) -//{ -// for ( int i = 0; i < mProjects.count(); ++i ) -// { -// if ( mProjects[i].projectDir == projectDir ) -// { -// // Effects only local project list, no need to send projectMetadataChanged -// mProjects[i].qgisProjectError = errMsg; -// return; -// } -// } -//} - void LocalProjectsManager::updateNamespace( const QString &projectDir, const QString &projectNamespace ) { for ( int i = 0; i < mProjects.count(); ++i ) @@ -279,81 +214,3 @@ void LocalProjectsManager::addProject( const QString &projectDir, const QString mProjects << project; emit localProjectAdded( project ); } - -//static QDateTime _getLastModifiedFileDateTime( const QString &path ) -//{ -// QDateTime lastModified; -// QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); -// while ( it.hasNext() ) -// { -// it.next(); -// if ( !MerginApi::isInIgnore( it.fileInfo() ) ) -// { -// if ( it.fileInfo().lastModified() > lastModified ) -// { -// lastModified = it.fileInfo().lastModified(); -// } -// } -// } -// return lastModified.toUTC(); -//} - -//static int _getProjectFilesCount( const QString &path ) -//{ -// int count = 0; -// QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); -// while ( it.hasNext() ) -// { -// it.next(); -// if ( !MerginApi::isInIgnore( it.fileInfo() ) ) -// { -// count++; -// } -// } -// return count; -//} - -//ProjectStatus::Status LocalProjectsManager::currentProjectStatus( const std::shared_ptr project ) -//{ -// if ( !project || !project->isMergin() || !project->isLocal() ) // This is not a Mergin project or not downloaded project -// return ProjectStatus::NoVersion; - -// // There was no sync yet -// if ( project->local->localVersion < 0 ) -// { -// return ProjectStatus::NoVersion; -// } - -// // -// // TODO: this check for local modifications should be revisited -// // - -// // Something has locally changed after last sync with server -// QString metadataFilePath = project->local->projectDir + "/" + MerginApi::sMetadataFile; -// QDateTime lastModified = _getLastModifiedFileDateTime( project->local->projectDir ); -// QDateTime lastSync = QFileInfo( metadataFilePath ).lastModified(); -// MerginProjectMetadata meta = MerginProjectMetadata::fromCachedJson( metadataFilePath ); -// int filesCount = _getProjectFilesCount( project->local->projectDir ); -// if ( lastSync < lastModified || meta.files.count() != filesCount ) -// { -// return ProjectStatus::Modified; -// } - -// // Version is lower than latest one, last sync also before updated -// if ( project->local->localVersion < project->mergin->serverVersion ) -// { -// return ProjectStatus::OutOfDate; -// } - -// return ProjectStatus::UpToDate; -//} - -//void LocalProjectsManager::updateProjectStatus( LocalProject_future &project ) -//{ -// ProjectStatus newStatus = currentProjectStatus( project ); -// if ( newStatus != project.status ) -// { -// project.status = newStatus; -// emit projectMetadataChanged( project.projectDir ); -// } -//} diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index 0aab423a6..f9af46f73 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -54,8 +54,6 @@ class LocalProjectsManager : public QObject //! Finds all QGIS project files and set the err variable if any occured. QString findQgisProjectFile( const QString &projectDir, QString &err ); - static ProjectStatus::Status currentProjectStatus( const std::shared_ptr project ); // TODO: maybe move somewhere else? - signals: void projectMetadataChanged( const QString &projectDir ); void localMerginProjectAdded( const QString &projectDir ); diff --git a/app/main.cpp b/app/main.cpp index 8c41aab12..c912a609f 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -32,14 +32,12 @@ #include "ios/iosutils.h" #include "inpututils.h" #include "positiondirection.h" -#include "projectsmodel.h" #include "mapthemesmodel.h" #include "digitizingcontroller.h" #include "merginapi.h" #include "merginapistatus.h" #include "merginsubscriptionstatus.h" #include "merginsubscriptiontype.h" -#include "merginprojectmodel.h" #include "merginprojectstatusmodel.h" #include "layersproxymodel.h" #include "layersmodel.h" @@ -220,7 +218,6 @@ void initDeclarative() qmlRegisterUncreatableType( "lc", 1, 0, "MerginUserAuth", "" ); qmlRegisterUncreatableType( "lc", 1, 0, "MerginUserInfo", "" ); qmlRegisterUncreatableType( "lc", 1, 0, "MerginPlan", "" ); - qmlRegisterUncreatableType( "lc", 1, 0, "ProjectModel", "" ); qmlRegisterUncreatableType( "lc", 1, 0, "MapThemesModel", "" ); qmlRegisterUncreatableType( "lc", 1, 0, "Loader", "" ); qmlRegisterUncreatableType( "lc", 1, 0, "AppSettings", "" ); @@ -366,27 +363,14 @@ int main( int argc, char *argv[] ) // Create Input classes AndroidUtils au; IosUtils iosUtils; - LocalProjectsManager localProjects( projectDir ); - ProjectModel pm( localProjects ); + LocalProjectsManager localProjectsManager( projectDir ); MapThemesModel mtm; - std::unique_ptr ma = std::unique_ptr( new MerginApi( localProjects ) ); + std::unique_ptr ma = std::unique_ptr( new MerginApi( localProjectsManager ) ); InputUtils iu; - MerginProjectModel mpm( localProjects ); - MerginProjectStatusModel mpsm( localProjects ); + MerginProjectStatusModel mpsm( localProjectsManager ); InputHelp help( ma.get(), &iu ); ProjectWizard pw( projectDir ); - // project models - instance for each category - ProjectsModel_future myProjectsModel( ma.get(), ProjectModelTypes::MyProjectsModel, localProjects ); - ProjectsModel_future localProjectsModel( ma.get(), ProjectModelTypes::LocalProjectsModel, localProjects ); - ProjectsModel_future sharedProjectsModel( ma.get(), ProjectModelTypes::SharedProjectsModel, localProjects ); - ProjectsModel_future exploreProjectsModel( ma.get(), ProjectModelTypes::ExploreProjectsModel, localProjects ); - - ProjectsProxyModel_future myProjectsProxyModel( &myProjectsModel ); - ProjectsProxyModel_future localProjectsProxyModel( &localProjectsModel ); - ProjectsProxyModel_future sharedProjectsProxyModel( &sharedProjectsModel ); - ProjectsProxyModel_future exploreProjectsProxyModel( &exploreProjectsModel ); - // layer models LayersModel lm; LayersProxyModel browseLpm( &lm, LayerModelTypes::BrowseDataLayerSelection ); @@ -402,11 +386,7 @@ int main( int argc, char *argv[] ) // Connections QObject::connect( &app, &QGuiApplication::applicationStateChanged, &loader, &Loader::appStateChanged ); QObject::connect( &app, &QCoreApplication::aboutToQuit, &loader, &Loader::appAboutToQuit ); -// QObject::connect( ma.get(), &MerginApi::syncProjectFinished, &pm, &ProjectModel::syncedProjectFinished ); -// QObject::connect( ma.get(), &MerginApi::projectDetached, &pm, &ProjectModel::findProjectFiles ); - QObject::connect( &pw, &ProjectWizard::projectCreated, &localProjects, &LocalProjectsManager::addLocalProject ); -// QObject::connect( ma.get(), &MerginApi::listProjectsFinished, &mpm, &MerginProjectModel::updateModel ); -// QObject::connect( ma.get(), &MerginApi::syncProjectStatusChanged, &mpm, &MerginProjectModel::syncProjectStatusChanged ); + QObject::connect( &pw, &ProjectWizard::projectCreated, &localProjectsManager, &LocalProjectsManager::addLocalProject ); QObject::connect( ma.get(), &MerginApi::reloadProject, &loader, &Loader::reloadProject ); QObject::connect( &mtm, &MapThemesModel::mapThemeChanged, &recordingLpm, &LayersProxyModel::onMapThemeChanged ); QObject::connect( &loader, &Loader::projectReloaded, vm.get(), &VariablesManager::merginProjectChanged ); @@ -494,23 +474,17 @@ int main( int argc, char *argv[] ) engine.rootContext()->setContextProperty( "__inputUtils", &iu ); engine.rootContext()->setContextProperty( "__inputProjUtils", &inputProjUtils ); engine.rootContext()->setContextProperty( "__inputHelp", &help ); - engine.rootContext()->setContextProperty( "__projectsModel", &pm ); engine.rootContext()->setContextProperty( "__loader", &loader ); engine.rootContext()->setContextProperty( "__mapThemesModel", &mtm ); engine.rootContext()->setContextProperty( "__appSettings", &as ); engine.rootContext()->setContextProperty( "__merginApi", ma.get() ); - engine.rootContext()->setContextProperty( "__merginProjectsModel", &mpm ); engine.rootContext()->setContextProperty( "__merginProjectStatusModel", &mpsm ); engine.rootContext()->setContextProperty( "__recordingLayersModel", &recordingLpm ); engine.rootContext()->setContextProperty( "__browseDataLayersModel", &browseLpm ); engine.rootContext()->setContextProperty( "__activeLayer", &al ); engine.rootContext()->setContextProperty( "__purchasing", purchasing.get() ); engine.rootContext()->setContextProperty( "__projectWizard", &pw ); - - engine.rootContext()->setContextProperty( "__myProjectsModel", &myProjectsModel ); // TODO: maybe project models do not need to be exposed? - engine.rootContext()->setContextProperty( "__localProjectsModel", &localProjectsModel ); - engine.rootContext()->setContextProperty( "__myProjectsProxyModel", &myProjectsProxyModel ); - engine.rootContext()->setContextProperty( "__localProjectsProxyModel", &localProjectsProxyModel ); + engine.rootContext()->setContextProperty( "__localProjectsManager", &localProjectsManager ); #ifdef MOBILE_OS engine.rootContext()->setContextProperty( "__appwindowvisibility", QWindow::Maximized ); diff --git a/app/merginapi.cpp b/app/merginapi.cpp index 7fe8a593e..37bed160b 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -30,8 +30,7 @@ #include const QString MerginApi::sMetadataFile = QStringLiteral( "/.mergin/mergin.json" ); -//const QString MerginApi::sDefaultApiRoot = QStringLiteral( "https://public.cloudmergin.com/" ); -const QString MerginApi::sDefaultApiRoot = QStringLiteral( "https://dev.dev.cloudmergin.com/" ); +const QString MerginApi::sDefaultApiRoot = QStringLiteral( "https://public.cloudmergin.com/" ); const QSet MerginApi::sIgnoreExtensions = QSet() << "gpkg-shm" << "gpkg-wal" << "qgs~" << "qgz~" << "pyc" << "swap"; const QSet MerginApi::sIgnoreFiles = QSet() << "mergin.json" << ".DS_Store"; const int MerginApi::UPLOAD_CHUNK_SIZE = 10 * 1024 * 1024; // Should be the same as on Mergin server @@ -107,13 +106,11 @@ QString MerginApi::listProjects( const QString &searchExpression, const QString InputUtils::log( "list projects", QStringLiteral( "Requesting: " ) + url.toString() ); connect( reply, &QNetworkReply::finished, this, [this, requestId]() {this->listProjectsReplyFinished( requestId );} ); - qDebug() << "MerginAPI: ListProjects, returning requestId: " << requestId; return requestId; } QString MerginApi::listProjectsByName( const QStringList &projectNames ) { - setApiRoot( defaultApiRoot() ); // construct JSON body QJsonDocument body; QJsonObject projects; @@ -121,7 +118,6 @@ QString MerginApi::listProjectsByName( const QStringList &projectNames ) projects.insert( "projects", projectsArr ); body.setObject( projects ); - qDebug() << "PMR: listProjectsByName(): requesting projects " << projectNames; QUrl url( mApiRoot + QStringLiteral( "/v1/project/by_names" ) ); @@ -1189,11 +1185,6 @@ QString MerginApi::merginUserName() const return userAuth()->username(); } -//MerginProjectList MerginApi::projects() -//{ -// return mRemoteProjects; -//} - QList MerginApi::getLocalProjectFiles( const QString &projectPath ) { QList merginFiles; @@ -1236,22 +1227,6 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) projectCount = doc.object().value( "count" ).toInt(); projectList = parseProjectsFromJson( doc ); } -// else -// { -// mRemoteProjects.clear(); -// } - - // for any local projects we can update the latest server version - // TODO: this should now be done inside model so no need to do it here (LocalProjects do not have server version anymore) -// for ( MerginProjectListEntry project : mRemoteProjects ) -// { -// QString fullProjectName = getFullProjectName( project.projectNamespace, project.projectName ); -// LocalProjectInfo localProject = mLocalProjects.projectFromMerginName( fullProjectName ); -// if ( localProject.isValid() ) -// { -// mLocalProjects.updateMerginServerVersion( localProject.projectDir, project.version ); -// } -// } InputUtils::log( "list projects", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) ); } @@ -1261,7 +1236,6 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjects" ), r->errorString(), serverMsg ); emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjects" ) ); InputUtils::log( "list projects", QStringLiteral( "FAILED - %1" ).arg( message ) ); -// mRemoteProjects.clear(); emit listProjectsFailed(); } @@ -1514,7 +1488,6 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) // add the local project if not there yet if ( !mLocalProjects.projectFromMerginName( projectFullName ).isValid() ) { - qDebug() << "PMR: Downloaded project" << projectFullName; QString projectNamespace, projectName; extractProjectName( projectFullName, projectNamespace, projectName ); diff --git a/app/merginapi.h b/app/merginapi.h index 620e3cfeb..3cb52cfa7 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -166,29 +166,6 @@ struct TransactionStatus ProjectDiff diff; }; - -//struct MerginProjectListEntry // TODO: replace with RemoteProject from Project_future.h -//{ -// bool isValid() const { return !projectName.isEmpty() && !projectNamespace.isEmpty(); } - -// QString projectName; -// QString projectNamespace; -// int version = -1; -// QDateTime serverUpdated; // available latest version of project files on server - -// bool operator ==( const MerginProjectListEntry &other ) -// { -// return ( this->projectName == other.projectName ) && ( this->projectNamespace == other.projectNamespace ); -// } - -// bool operator !=( const MerginProjectListEntry &other ) -// { -// return !( *this == other ); -// } -//}; - -//typedef QList MerginProjectList; // TODO: replace with RemoteProject from Project_future.h - typedef QHash Transactions; Q_DECLARE_METATYPE( Transactions ); @@ -247,7 +224,7 @@ class MerginApi: public QObject /** * Sends non-blocking POST request to the server to download/update a project with a given name. On downloadProjectReplyFinished, * when a response is received, parses data-stream to files and rewrites local files with them. Extra files which don't match server - * files are removed. Eventually emits syncProjectFinished on which MerginProjectModel updates status of the project item. + * files are removed. Eventually emits syncProjectFinished on which ProjectModel updates status of the project item. * If update has been successful, updates metadata file of the project. * Emits also notify signal with a message for the GUI. * \param projectNamespace Project's namespace used in request. @@ -259,7 +236,7 @@ class MerginApi: public QObject /** * Sends non-blocking POST request to the server to upload changes in a project with a given name. * Firstly updateProject is triggered to fetch new changes. If it was successful, sends update post request with list of local changes - * and modified/newly added files in JSON. Eventually emits syncProjectFinished on which MerginProjectModel updates status of the project item. + * and modified/newly added files in JSON. Eventually emits syncProjectFinished on which ProjectModel updates status of the project item. * Emits also notify signal with a message for the GUI. * \param projectNamespace Project's namespace used in request. * \param projectName Project's name used in request. @@ -385,9 +362,6 @@ class MerginApi: public QObject */ static ProjectDiff compareProjectFiles( const QList &oldServerFiles, const QList &newServerFiles, const QList &localFiles, const QString &projectDir ); - //! Returns the most recent list of projects fetched from the server -// MerginProjectList projects(); - static QList getLocalProjectFiles( const QString &projectPath ); QString apiRoot() const; @@ -395,12 +369,6 @@ class MerginApi: public QObject QString merginUserName() const; // TODO: remove (use can be replaced with userInfo->username) - //! Disk usage of current logged in user in Mergin instance in Bytes - int diskUsage() const; // TODO: remove (no use) - - //! Total storage limit of current logged in user in Mergin instance in Bytes - int storageLimit() const; // TODO: remove (no use) - MerginApiStatus::VersionStatus apiVersionStatus() const; void setApiVersionStatus( const MerginApiStatus::VersionStatus &apiVersionStatus ); @@ -569,7 +537,6 @@ class MerginApi: public QObject QNetworkAccessManager mManager; QString mApiRoot; LocalProjectsManager &mLocalProjects; -// MerginProjectList mRemoteProjects; // TODO: remove (no use, only in tests - TBD) QString mDataDir; // dir with all projects MerginUserInfo *mUserInfo; //owned by this (qml grouped-properties) diff --git a/app/merginprojectmodel.cpp b/app/merginprojectmodel.cpp deleted file mode 100644 index d7109cb52..000000000 --- a/app/merginprojectmodel.cpp +++ /dev/null @@ -1,254 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -#include "merginprojectmodel.h" - -#include - -MerginProjectModel::MerginProjectModel( LocalProjectsManager &localProjects, QObject *parent ) - : QAbstractListModel( parent ) - , mLocalProjects( localProjects ) -{ - QObject::connect( &mLocalProjects, &LocalProjectsManager::projectMetadataChanged, this, &MerginProjectModel::projectMetadataChanged ); - QObject::connect( &mLocalProjects, &LocalProjectsManager::localMerginProjectAdded, this, &MerginProjectModel::onLocalProjectAdded ); - QObject::connect( &mLocalProjects, &LocalProjectsManager::localProjectRemoved, this, &MerginProjectModel::onLocalProjectRemoved ); - - mAdditionalItem->status = NonProjectItem; -} - -QVariant MerginProjectModel::data( const QModelIndex &index, int role ) const -{ - int row = index.row(); - if ( row < 0 || row >= mMerginProjects.count() ) - return QVariant(); - - const MerginProject *project = mMerginProjects.at( row ).get(); - - switch ( role ) - { - case ProjectName: - return QVariant( project->projectName ); - case ProjectNamespace: return QVariant( project->projectNamespace ); - case ProjectInfo: - { - // TODO: better project info - // TODO: clientUpdated currently not being set - if ( !project->clientUpdated.isValid() ) - { - return project->serverUpdated.toLocalTime().toString(); - } - else - { - return project->clientUpdated.toLocalTime().toString(); - } - } - - case Status: - { - switch ( project->status ) - { - case ProjectStatus::OutOfDate: - return QVariant( QStringLiteral( "outOfDate" ) ); - case ProjectStatus::UpToDate: - return QVariant( QStringLiteral( "upToDate" ) ); - case ProjectStatus::NoVersion: - return QVariant( QStringLiteral( "noVersion" ) ); - case ProjectStatus::Modified: - return QVariant( QStringLiteral( "modified" ) ); - case ProjectStatus::NonProjectItem: - return QVariant( QStringLiteral( "nonProjectItem" ) ); - } - break; - } - case Pending: return QVariant( project->pending ); - case PassesFilter: return mSearchExpression.isEmpty() || project->projectName.contains( mSearchExpression, Qt::CaseInsensitive ) - || project->projectNamespace.contains( mSearchExpression, Qt::CaseInsensitive ); - case SyncProgress: return QVariant( project->progress ); - } - - return QVariant(); -} - - -QHash MerginProjectModel::roleNames() const -{ - QHash roleNames = QAbstractListModel::roleNames(); - roleNames[ProjectName] = "projectName"; - roleNames[ProjectNamespace] = "projectNamespace"; - roleNames[ProjectInfo] = "projectInfo"; - roleNames[Status] = "status"; - roleNames[Pending] = "pendingProject"; - roleNames[PassesFilter] = "passesFilter"; - roleNames[SyncProgress] = "syncProgress"; - return roleNames; -} - -ProjectList MerginProjectModel::projects() -{ - return mMerginProjects; -} - -int MerginProjectModel::rowCount( const QModelIndex &parent ) const -{ - Q_UNUSED( parent ) - return mMerginProjects.count(); -} - -void MerginProjectModel::updateModel( const MerginProjectList &merginProjects, QHash pendingProjects, int expectedProjectCount, int page ) -{ - beginResetModel(); - mMerginProjects.removeOne( mAdditionalItem ); // TODO: remove - - if ( page == 1 ) - { - mMerginProjects.clear(); - } - setLastPage( page ); - - - for ( MerginProjectListEntry entry : merginProjects ) - { - QString fullProjectName = MerginApi::getFullProjectName( entry.projectNamespace, entry.projectName ); - std::shared_ptr project = std::make_shared(); - project->projectNamespace = entry.projectNamespace; - project->projectName = entry.projectName; - project->serverUpdated = entry.serverUpdated; - - // figure out info from local projects (projectDir etc) - LocalProjectInfo localProject = mLocalProjects.projectFromMerginName( entry.projectNamespace, entry.projectName ); - if ( localProject.isValid() ) - { - project->projectDir = localProject.projectDir; - project->status = localProject.status; - // TODO: what else to copy? - } - - if ( pendingProjects.contains( fullProjectName ) ) - { - - TransactionStatus projectTransaction = pendingProjects.value( fullProjectName ); - project->progress = projectTransaction.transferedSize / projectTransaction.totalSize; - project->pending = true; - } - - mMerginProjects << project; - } - - if ( mMerginProjects.count() < expectedProjectCount ) // TODO: remove - { - mMerginProjects << mAdditionalItem; - } - - endResetModel(); -} - -int MerginProjectModel::findProjectIndex( const QString &projectFullName ) -{ - int row = 0; - for ( std::shared_ptr project : mMerginProjects ) - { - if ( MerginApi::getFullProjectName( project->projectNamespace, project->projectName ) == projectFullName ) - return row; - row++; - } - return -1; -} - -void MerginProjectModel::setLastPage( int lastPage ) -{ - mLastPage = lastPage; - emit lastPageChanged(); -} - -int MerginProjectModel::lastPage() const -{ - return mLastPage; -} - -QString MerginProjectModel::searchExpression() const -{ - return mSearchExpression; -} - -void MerginProjectModel::setSearchExpression( const QString &searchExpression ) -{ - if ( searchExpression != mSearchExpression ) - { - mSearchExpression = searchExpression; - // Hack to model changed signal - beginResetModel(); - endResetModel(); - } -} - -void MerginProjectModel::syncProjectStatusChanged( const QString &projectFullName, qreal progress ) -{ - int row = findProjectIndex( projectFullName ); - if ( row == -1 ) - return; - - std::shared_ptr project = mMerginProjects[row]; - project->pending = progress >= 0; - project->progress = progress >= 0 ? progress : 0; - - QModelIndex ix = index( row ); - emit dataChanged( ix, ix ); -} - - -void MerginProjectModel::projectMetadataChanged( const QString &projectDir ) -{ - int row = 0; - for ( std::shared_ptr project : mMerginProjects ) - { - if ( project->projectDir == projectDir ) - { - LocalProjectInfo localProject = mLocalProjects.projectFromDirectory( projectDir ); - if ( !localProject.isValid() ) - return; - - // update cached information - project->status = localProject.status; - - QModelIndex ix = index( row ); - emit dataChanged( ix, ix ); - return; - } - row++; - } -} - -void MerginProjectModel::onLocalProjectAdded( const QString &projectDir ) -{ - LocalProjectInfo localProject = mLocalProjects.projectFromDirectory( projectDir ); - if ( !localProject.isValid() ) - return; - - QString projectFullName = MerginApi::getFullProjectName( localProject.projectNamespace, localProject.projectName ); - int i = findProjectIndex( projectFullName ); - if ( i == -1 ) - return; - - std::shared_ptr project = mMerginProjects[i]; - - // store project dir - project->projectDir = localProject.projectDir; - - // update metadata and emit dataChanged() signal - projectMetadataChanged( projectDir ); -} - -void MerginProjectModel::onLocalProjectRemoved( const QString &projectDir ) -{ - // TODO: implement - // (at this point this is not needed because after removal we need to switch tab - // and that will re-fetch the list of projects) - Q_UNUSED( projectDir ); -} - diff --git a/app/merginprojectmodel.h b/app/merginprojectmodel.h deleted file mode 100644 index e45eee127..000000000 --- a/app/merginprojectmodel.h +++ /dev/null @@ -1,117 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -#ifndef MERGINPROJECTMODEL_H -#define MERGINPROJECTMODEL_H - -#include -#include -#include -#include -#include "merginapi.h" - - -/** - * Basic information about a remote mergin project from the received list of projects. - * - * We add some local information for project is also available locally: - * - general local info: project dir, downloaded version number, project status - * - sync status: whether sync is active, progress indicator - */ -struct MerginProject -{ - QString projectName; - QString projectNamespace; - QString projectDir; // full path to the project directory - QDateTime clientUpdated; // client's version of project files - QDateTime serverUpdated; // available latest version of project files on server - bool pending = false; // if there is a pending request for downlaod/update a project - ProjectStatus status = NoVersion; - qreal progress = 0; // progress in case of pending download/upload (values [0..1]) -}; - -typedef QList> ProjectList; - - -class MerginProjectModel: public QAbstractListModel -{ - Q_OBJECT - Q_PROPERTY( QString searchExpression READ searchExpression WRITE setSearchExpression ) - Q_PROPERTY( int lastPage READ lastPage NOTIFY lastPageChanged ) - - public: - enum Roles - { - ProjectName = Qt::UserRole + 1, - ProjectNamespace, - Size, - ProjectInfo, - Status, - Pending, - PassesFilter, - SyncProgress - }; - Q_ENUMS( Roles ) - - explicit MerginProjectModel( LocalProjectsManager &localProjects, QObject *parent = nullptr ); - - Q_INVOKABLE QVariant data( const QModelIndex &index, int role ) const override; - - ProjectList projects(); - - QHash roleNames() const override; - - int rowCount( const QModelIndex &parent = QModelIndex() ) const override; - - /** - * Updates list of projects with synchronization progress if a project is pending. - * \param merginProjects List of mergin projects - * \param pendingProjects Projects in pending state - * \param expectedProjectCount Total number of projects - * \param page Int representing page. - */ - void updateModel( const MerginProjectList &merginProjects, QHash pendingProjects, int expectedProjectCount, int page ); - - int filterCreator() const; // TODO: remove, no use - void setFilterCreator( int filterCreator ); - - int filterWriter() const; // TODO: remove, no use - void setFilterWriter( int filterWriter ); - - QString searchExpression() const; // TODO: remove, search will be in proxy model - void setSearchExpression( const QString &searchExpression ); - - int lastPage() const; - void setLastPage( int lastPage ); - - signals: - void lastPageChanged(); - - public slots: - void syncProjectStatusChanged( const QString &projectFullName, qreal progress ); - - private slots: - - void projectMetadataChanged( const QString &projectDir ); - void onLocalProjectAdded( const QString &projectDir ); - void onLocalProjectRemoved( const QString &projectDir ); - - private: - - int findProjectIndex( const QString &projectFullName ); - - ProjectList mMerginProjects; - LocalProjectsManager &mLocalProjects; - QString mSearchExpression; - int mLastPage; - //! Special item as a placeholder for custom component with extended funtionality - std::shared_ptr mAdditionalItem = std::make_shared(); - -}; -#endif // MERGINPROJECTMODEL_H diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp deleted file mode 100644 index bc2a2d17c..000000000 --- a/app/projectsmodel.cpp +++ /dev/null @@ -1,205 +0,0 @@ -/*************************************************************************** - qgsquicklayertreemodel.cpp - -------------------------------------- - Date : Nov 2017 - Copyright : (C) 2017 by Peter Petrik - Email : zilolv at gmail dot com - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -#include "projectsmodel.h" - -#include -#include -#include -#include - -#include "inpututils.h" -#include "merginapi.h" - -ProjectModel::ProjectModel( LocalProjectsManager &localProjects, QObject *parent ) - : QAbstractListModel( parent ) - , mLocalProjects( localProjects ) -{ - findProjectFiles(); - - QObject::connect( &mLocalProjects, &LocalProjectsManager::localProjectAdded, this, &ProjectModel::addLocalProject ); -} - -ProjectModel::~ProjectModel() {} - -void ProjectModel::findProjectFiles() -{ - beginResetModel(); - // populate from mLocalProjects - mProjectFiles.clear(); - const QList projects = mLocalProjects.projects(); - for ( const LocalProjectInfo &project : projects ) - { - QDir dir( project.projectDir ); - QFileInfo fi( project.qgisProjectFilePath ); - - ProjectFile projectFile; - projectFile.path = project.qgisProjectFilePath; - projectFile.folderName = dir.dirName(); - projectFile.projectName = project.projectName; - projectFile.projectNamespace = project.projectNamespace; - projectFile.isValid = project.isShowable(); - QDateTime created = fi.created().toLocalTime(); - if ( projectFile.isValid ) - { - projectFile.info = QString( created.toString() ); - } - else - { - projectFile.info = project.qgisProjectError; - } - mProjectFiles << projectFile; - } - - std::sort( mProjectFiles.begin(), mProjectFiles.end() ); - endResetModel(); -} - - -QVariant ProjectModel::data( const QModelIndex &index, int role ) const -{ - int row = index.row(); - if ( row < 0 || row >= mProjectFiles.count() ) - return QVariant( "" ); - - const ProjectFile &projectFile = mProjectFiles.at( row ); - - switch ( role ) - { - case ProjectName: return QVariant( projectFile.projectName ); - case ProjectNamespace: return QVariant( projectFile.projectNamespace ); - case FolderName: return QVariant( projectFile.folderName ); - case Path: return QVariant( projectFile.path ); - case ProjectInfo: return QVariant( projectFile.info ); - case IsValid: return QVariant( projectFile.isValid ); - case PassesFilter: return mSearchExpression.isEmpty() || projectFile.folderName.contains( mSearchExpression, Qt::CaseInsensitive ); - } - - return QVariant(); -} - -QHash ProjectModel::roleNames() const -{ - QHash roleNames = QAbstractListModel::roleNames(); - roleNames[ProjectName] = "projectName"; - roleNames[ProjectNamespace] = "projectNamespace"; - roleNames[FolderName] = "folderName"; - roleNames[Path] = "path"; - roleNames[ProjectInfo] = "projectInfo"; - roleNames[IsValid] = "isValid"; - roleNames[PassesFilter] = "passesFilter"; - return roleNames; -} - -QModelIndex ProjectModel::index( int row, int column, const QModelIndex &parent ) const -{ - Q_UNUSED( column ); - Q_UNUSED( parent ); - return createIndex( row, 0, nullptr ); -} - -int ProjectModel::rowAccordingPath( QString path ) const -{ - int i = 0; - for ( ProjectFile prj : mProjectFiles ) - { - if ( prj.path == path ) - { - return i; - } - i++; - } - return -1; -} - -void ProjectModel::deleteProject( int row ) -{ - if ( row < 0 || row >= mProjectFiles.length() ) - { - InputUtils::log( "Deleting local project error", QStringLiteral( "Unable to delete local project, index out of bounds" ) ); - return; - } - - ProjectFile project = mProjectFiles.at( row ); - - mLocalProjects.deleteProjectDirectory( mLocalProjects.dataDir() + "/" + project.folderName ); - - beginResetModel(); - mProjectFiles.removeAt( row ); - endResetModel(); -} - -int ProjectModel::rowCount( const QModelIndex &parent ) const -{ - Q_UNUSED( parent ); - return mProjectFiles.count(); -} - -QString ProjectModel::dataDir() const -{ - return mLocalProjects.dataDir(); -} - -QString ProjectModel::searchExpression() const -{ - return mSearchExpression; -} - -void ProjectModel::setSearchExpression( const QString &searchExpression ) -{ - if ( searchExpression != mSearchExpression ) - { - mSearchExpression = searchExpression; - // Hack to model changed signal - beginResetModel(); - endResetModel(); - } -} - -bool ProjectModel::containsProject( const QString &projectNamespace, const QString &projectName ) -{ - return mLocalProjects.hasMerginProject( projectNamespace, projectName ); -} - -void ProjectModel::syncedProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully ) -{ - - // Do basic validity check - if ( successfully ) - { - QString errMsg; - mLocalProjects.findQgisProjectFile( projectDir, errMsg ); - mLocalProjects.updateProjectErrors( projectDir, errMsg ); - } - - reloadProjectFiles( projectDir, projectFullName, successfully ); -} - -void ProjectModel::addLocalProject( const QString &projectDir ) // TODO: not needed if localProjectsManager emits added signal -{ - Q_UNUSED( projectDir ); - mLocalProjects.reloadProjectDir(); - findProjectFiles(); -} - -void ProjectModel::reloadProjectFiles( QString projectFolder, QString projectName, bool successful ) -{ - if ( !successful ) return; - - if ( projectFolder.isEmpty() ) return; - - Q_UNUSED( projectName ); - findProjectFiles(); -} diff --git a/app/projectsmodel.h b/app/projectsmodel.h deleted file mode 100644 index 6a2905c7b..000000000 --- a/app/projectsmodel.h +++ /dev/null @@ -1,125 +0,0 @@ -/*************************************************************************** - qgsquicklayertreemodel.h - -------------------------------------- - Date : Nov 2017 - Copyright : (C) 2017 by Peter Petrik - Email : zilolv at gmail dot com - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - - -#ifndef PROJECTSMODEL_H -#define PROJECTSMODEL_H - -#include -#include -#include - -class LocalProjectsManager; - -/* - * Given data directory, find all QGIS projects (*.qgs or *.qgz) in the directory and subdirectories - * and create list model from them. Available are full path to the file, name of the project - * and short name of the project (clipped to N chars) - */ -class ProjectModel : public QAbstractListModel -{ - Q_OBJECT - Q_PROPERTY( QString dataDir READ dataDir ) // never changes - Q_PROPERTY( QString searchExpression READ searchExpression WRITE setSearchExpression ) - - public: - enum Roles - { - ProjectName = Qt::UserRole + 1, // name of a project file - ProjectNamespace, - FolderName, - Path, - ProjectInfo, - Size, - IsValid, - PassesFilter - }; - Q_ENUMS( Roles ) - - explicit ProjectModel( LocalProjectsManager &localProjects, QObject *parent = nullptr ); - ~ProjectModel() override; - - Q_INVOKABLE QVariant data( const QModelIndex &index, int role ) const override; - Q_INVOKABLE QModelIndex index( int row, int column = 0, const QModelIndex &parent = QModelIndex() ) const override; - Q_INVOKABLE int rowAccordingPath( QString path ) const; - Q_INVOKABLE void deleteProject( int row ); - - QHash roleNames() const override; - - int rowCount( const QModelIndex &parent = QModelIndex() ) const override; - - QString dataDir() const; - - QString searchExpression() const; - void setSearchExpression( const QString &searchExpression ); - - // Test function - bool containsProject( const QString &projectNamespace, const QString &projectName ); - - public slots: - void syncedProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully ); - void addLocalProject( const QString &projectDir ); - void findProjectFiles(); - - private: - void reloadProjectFiles( QString projectFolder, QString projectName, bool successful ); - - struct ProjectFile - { - QString projectName; //!< mergin project name (second part of "namespace/project"). empty for non-mergin project - QString projectNamespace; //!< mergin project namespace (first part of "namespace/project"). empty for non-mergin project - QString folderName; //!< name of the project folder (not the full path) - QString path; //!< path to the .qgs/.qgz project file - QString info; // Description! - bool isValid; - - /** - * Ordering of local projects: first non-mergin projects (using folder name), - * then mergin projects (sorted first by namespace, then project name) - */ - bool operator < ( const ProjectFile &other ) const - { - if ( projectNamespace.isEmpty() && other.projectNamespace.isEmpty() ) - { - return folderName.compare( other.folderName, Qt::CaseInsensitive ) < 0; - } - if ( !projectNamespace.isEmpty() && other.projectNamespace.isEmpty() ) - { - return false; - } - if ( projectNamespace.isEmpty() && !other.projectNamespace.isEmpty() ) - { - return true; - } - - if ( projectNamespace.compare( other.projectNamespace, Qt::CaseInsensitive ) == 0 ) - { - return projectName.compare( other.projectName, Qt::CaseInsensitive ) < 0; - } - if ( projectNamespace.compare( other.projectNamespace, Qt::CaseInsensitive ) < 0 ) - { - return true; - } - else - return false; - } - }; - LocalProjectsManager &mLocalProjects; - QList mProjectFiles; - QString mSearchExpression; - -}; - -#endif // PROJECTSMODEL_H diff --git a/app/sources.pri b/app/sources.pri index 3b28b1786..7581e7c8a 100644 --- a/app/sources.pri +++ b/app/sources.pri @@ -7,7 +7,6 @@ layersproxymodel.cpp \ localprojectsmanager.cpp \ main.cpp \ merginprojectmetadata.cpp \ -projectsmodel.cpp \ projectwizard.cpp \ loader.cpp \ digitizingcontroller.cpp \ @@ -17,7 +16,6 @@ merginapi.cpp \ merginapistatus.cpp \ merginsubscriptionstatus.cpp \ merginsubscriptiontype.cpp \ -merginprojectmodel.cpp \ merginprojectstatusmodel.cpp \ merginuserauth.cpp \ merginuserinfo.cpp \ @@ -53,7 +51,6 @@ layersmodel.h \ layersproxymodel.h \ localprojectsmanager.h \ merginprojectmetadata.h \ -projectsmodel.h \ projectwizard.h \ loader.h \ digitizingcontroller.h \ @@ -63,7 +60,6 @@ merginapi.h \ merginapistatus.h \ merginsubscriptionstatus.h \ merginsubscriptiontype.h \ -merginprojectmodel.h \ merginprojectstatusmodel.h \ merginuserauth.h \ merginuserinfo.h \ From 63fda47110e0d12f7848bc3b3ca76343291a8854 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Wed, 31 Mar 2021 12:47:05 +0200 Subject: [PATCH 23/53] rename symbols _future --- app/localprojectsmanager.cpp | 24 +-- app/localprojectsmanager.h | 16 +- app/main.cpp | 10 +- app/merginapi.cpp | 22 +-- app/merginapi.h | 6 +- app/merginprojectstatusmodel.cpp | 2 +- app/{project_future.cpp => project.cpp} | 8 +- app/{project_future.h => project.h} | 48 +++--- ...ectsmodel_future.cpp => projectsmodel.cpp} | 142 +++++++++--------- ...projectsmodel_future.h => projectsmodel.h} | 28 ++-- ...odel_future.cpp => projectsproxymodel.cpp} | 38 ++--- ...oxymodel_future.h => projectsproxymodel.h} | 26 ++-- app/sources.pri | 12 +- 13 files changed, 191 insertions(+), 191 deletions(-) rename app/{project_future.cpp => project.cpp} (93%) rename app/{project_future.h => project.h} (76%) rename app/{projectsmodel_future.cpp => projectsmodel.cpp} (75%) rename app/{projectsmodel_future.h => projectsmodel.h} (88%) rename app/{projectsproxymodel_future.cpp => projectsproxymodel.cpp} (57%) rename app/{projectsproxymodel_future.h => projectsproxymodel.h} (61%) diff --git a/app/localprojectsmanager.cpp b/app/localprojectsmanager.cpp index 277e20829..258ada02e 100644 --- a/app/localprojectsmanager.cpp +++ b/app/localprojectsmanager.cpp @@ -28,7 +28,7 @@ void LocalProjectsManager::reloadDataDir() QStringList entryList = QDir( mDataDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); for ( const QString &folderName : entryList ) { - LocalProject_future info; + LocalProject info; info.projectDir = mDataDir + "/" + folderName; info.qgisProjectFilePath = findQgisProjectFile( info.projectDir, info.projectError ); @@ -51,37 +51,37 @@ void LocalProjectsManager::reloadDataDir() emit dataDirReloaded(); } -LocalProject_future LocalProjectsManager::projectFromDirectory( const QString &projectDir ) const +LocalProject LocalProjectsManager::projectFromDirectory( const QString &projectDir ) const { - for ( const LocalProject_future &info : mProjects ) + for ( const LocalProject &info : mProjects ) { if ( info.projectDir == projectDir ) return info; } - return LocalProject_future(); + return LocalProject(); } -LocalProject_future LocalProjectsManager::projectFromProjectFilePath( const QString &projectFilePath ) const +LocalProject LocalProjectsManager::projectFromProjectFilePath( const QString &projectFilePath ) const { - for ( const LocalProject_future &info : mProjects ) + for ( const LocalProject &info : mProjects ) { if ( info.qgisProjectFilePath == projectFilePath ) return info; } - return LocalProject_future(); + return LocalProject(); } -LocalProject_future LocalProjectsManager::projectFromMerginName( const QString &projectFullName ) const +LocalProject LocalProjectsManager::projectFromMerginName( const QString &projectFullName ) const { - for ( const LocalProject_future &info : mProjects ) + for ( const LocalProject &info : mProjects ) { if ( info.id() == projectFullName ) return info; } - return LocalProject_future(); + return LocalProject(); } -LocalProject_future LocalProjectsManager::projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const +LocalProject LocalProjectsManager::projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const { return projectFromMerginName( MerginApi::getFullProjectName( projectNamespace, projectName ) ); } @@ -205,7 +205,7 @@ QString LocalProjectsManager::findQgisProjectFile( const QString &projectDir, QS void LocalProjectsManager::addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ) { - LocalProject_future project; + LocalProject project; project.projectDir = projectDir; project.qgisProjectFilePath = findQgisProjectFile( projectDir, project.projectError ); project.projectName = projectName; diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index f9af46f73..9ff18bd7a 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -11,7 +11,7 @@ #define LOCALPROJECTSMANAGER_H #include -#include +#include class LocalProjectsManager : public QObject { @@ -26,11 +26,11 @@ class LocalProjectsManager : public QObject LocalProjectsList projects() const { return mProjects; } - LocalProject_future projectFromDirectory( const QString &projectDir ) const; - LocalProject_future projectFromProjectFilePath( const QString &projectFilePath ) const; + LocalProject projectFromDirectory( const QString &projectDir ) const; + LocalProject projectFromProjectFilePath( const QString &projectFilePath ) const; - LocalProject_future projectFromMerginName( const QString &projectFullName ) const; - LocalProject_future projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const; + LocalProject projectFromMerginName( const QString &projectFullName ) const; + LocalProject projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const; //! Adds entry about newly created project void addLocalProject( const QString &projectDir, const QString &projectName ); @@ -57,9 +57,9 @@ class LocalProjectsManager : public QObject signals: void projectMetadataChanged( const QString &projectDir ); void localMerginProjectAdded( const QString &projectDir ); - void localProjectAdded( const LocalProject_future &project ); - void aboutToRemoveLocalProject( const LocalProject_future project ); - void localProjectDataChanged( const LocalProject_future &project ); + void localProjectAdded( const LocalProject &project ); + void aboutToRemoveLocalProject( const LocalProject project ); + void localProjectDataChanged( const LocalProject &project ); void dataDirReloaded(); private: diff --git a/app/main.cpp b/app/main.cpp index c912a609f..f457870c6 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -53,9 +53,9 @@ #include "codefilter.h" #include "inputexpressionfunctions.h" -#include "projectsmodel_future.h" -#include "projectsproxymodel_future.h" -#include "project_future.h" +#include "projectsmodel.h" +#include "projectsproxymodel.h" +#include "project.h" #ifdef INPUT_TEST #include "test/testmerginapi.h" @@ -233,8 +233,8 @@ void initDeclarative() qmlRegisterType( "lc", 1, 0, "FieldsModel" ); qmlRegisterType( "lc", 1, 0, "CodeFilter" ); - qmlRegisterType( "lc", 1, 0, "ProjectsModel" ); - qmlRegisterType( "lc", 1, 0, "ProjectsProxyModel" ); + qmlRegisterType( "lc", 1, 0, "ProjectsModel" ); + qmlRegisterType( "lc", 1, 0, "ProjectsProxyModel" ); qmlRegisterUncreatableMetaObject( ProjectStatus::staticMetaObject, "lc", 1, 0, "ProjectStatus", "ProjectStatus Enum" ); } diff --git a/app/merginapi.cpp b/app/merginapi.cpp index 37bed160b..0d2d7cb71 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -757,7 +757,7 @@ void MerginApi::createProjectFinished() extractProjectName( projectFullName, projectNamespace, projectName ); // Upload data if createProject has been called for a local project with empty namespace (case of migrating a project) - for ( const LocalProject_future &info : mLocalProjects.projects() ) + for ( const LocalProject &info : mLocalProjects.projects() ) { if ( info.projectName == projectName && info.projectNamespace.isEmpty() ) { @@ -937,7 +937,7 @@ QNetworkReply *MerginApi::getProjectInfo( const QString &projectFullName, bool w } int sinceVersion = -1; - LocalProject_future projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); if ( projectInfo.isValid() ) { // let's also fetch the recent history of diffable files @@ -1062,7 +1062,7 @@ QString MerginApi::extractServerErrorMsg( const QByteArray &data ) } -LocalProject_future MerginApi::getLocalProject( const QString &projectFullName ) +LocalProject MerginApi::getLocalProject( const QString &projectFullName ) { return mLocalProjects.projectFromMerginName( projectFullName ); } @@ -1136,7 +1136,7 @@ void MerginApi::detachProjectFromMergin( const QString &projectNamespace, const { // Remove mergin folder QString projectFullName = getFullProjectName( projectNamespace, projectName ); - LocalProject_future projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); if ( projectInfo.isValid() ) { @@ -1385,7 +1385,7 @@ void MerginApi::finalizeProjectUpdateApplyDiff( const QString &projectFullName, // not good... something went wrong in rebase - we need to save the local changes // let's put them into a conflict file and use the server version - LocalProject_future info = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject info = mLocalProjects.projectFromMerginName( projectFullName ); QString newDest = InputUtils::findUniquePath( generateConflictFileName( dest, info.localVersion ), false ); if ( !QFile::rename( dest, newDest ) ) { @@ -1439,7 +1439,7 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) { // move local file to conflict file QString origPath = projectDir + "/" + finalizationItem.filePath; - LocalProject_future info = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject info = mLocalProjects.projectFromMerginName( projectFullName ); QString newPath = InputUtils::findUniquePath( generateConflictFileName( origPath, info.localVersion ), false ); if ( !QFile::rename( origPath, newPath ) ) { @@ -1667,7 +1667,7 @@ void MerginApi::startProjectUpdate( const QString &projectFullName, const QByteA Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); TransactionStatus &transaction = mTransactionalStatus[projectFullName]; - LocalProject_future projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); if ( projectInfo.isValid() ) // If project is already downloaded { transaction.projectDir = projectInfo.projectDir; @@ -1844,7 +1844,7 @@ void MerginApi::uploadInfoReplyFinished() transaction.replyUploadProjectInfo->deleteLater(); transaction.replyUploadProjectInfo = nullptr; - LocalProject_future projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); transaction.projectDir = projectInfo.projectDir; Q_ASSERT( !transaction.projectDir.isEmpty() ); @@ -2262,9 +2262,9 @@ ProjectDiff MerginApi::compareProjectFiles( const QList &oldServerFi return diff; } -MerginProject_future MerginApi::parseProjectMetadata( const QJsonObject &proj ) +MerginProject MerginApi::parseProjectMetadata( const QJsonObject &proj ) { - MerginProject_future project; + MerginProject project; if ( proj.isEmpty() ) { @@ -2326,7 +2326,7 @@ MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc ) { for ( auto it = object.begin(); it != object.end(); ++it ) { - MerginProject_future project = parseProjectMetadata( it->toObject() ); + MerginProject project = parseProjectMetadata( it->toObject() ); if ( !project.remoteError.isEmpty() ) { // add project namespace/name from object name in case of error diff --git a/app/merginapi.h b/app/merginapi.h index 3cb52cfa7..d621da22c 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -27,7 +27,7 @@ #include "merginsubscriptionstatus.h" #include "merginprojectmetadata.h" #include "localprojectsmanager.h" -#include "project_future.h" +#include "project.h" class MerginUserAuth; class MerginUserInfo; @@ -309,7 +309,7 @@ class MerginApi: public QObject Q_INVOKABLE void detachProjectFromMergin( const QString &projectNamespace, const QString &projectName ); - LocalProject_future getLocalProject( const QString &projectFullName ); // Test function + LocalProject getLocalProject( const QString &projectFullName ); // Test function static const int MERGIN_API_VERSION_MAJOR = 2020; static const int MERGIN_API_VERSION_MINOR = 4; @@ -439,7 +439,7 @@ class MerginApi: public QObject void pingMerginReplyFinished(); private: - MerginProject_future parseProjectMetadata( const QJsonObject &project ); + MerginProject parseProjectMetadata( const QJsonObject &project ); MerginProjectsList parseProjectsFromJson( const QJsonDocument &object ); static QStringList generateChunkIdsForSize( qint64 fileSize ); QJsonArray prepareUploadChangesJSON( const QList &files ); diff --git a/app/merginprojectstatusmodel.cpp b/app/merginprojectstatusmodel.cpp index 335ec99a1..ff7097835 100644 --- a/app/merginprojectstatusmodel.cpp +++ b/app/merginprojectstatusmodel.cpp @@ -125,7 +125,7 @@ void MerginProjectStatusModel::infoProjectUpdated( const ProjectDiff &projectDif bool MerginProjectStatusModel::loadProjectInfo( const QString &projectFullName ) { - LocalProject_future projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); if ( !projectInfo.projectDir.isEmpty() ) { ProjectDiff diff = MerginApi::localProjectChanges( projectInfo.projectDir ); diff --git a/app/project_future.cpp b/app/project.cpp similarity index 93% rename from app/project_future.cpp rename to app/project.cpp index d7406850c..f95332d22 100644 --- a/app/project_future.cpp +++ b/app/project.cpp @@ -7,11 +7,11 @@ * * ***************************************************************************/ -#include "project_future.h" +#include "project.h" #include "merginapi.h" #include "inpututils.h" -QString LocalProject_future::id() const +QString LocalProject::id() const { if ( !projectName.isEmpty() && !projectNamespace.isEmpty() ) return MerginApi::getFullProjectName( projectNamespace, projectName ); @@ -20,12 +20,12 @@ QString LocalProject_future::id() const return dir.dirName(); } -QString MerginProject_future::id() const +QString MerginProject::id() const { return MerginApi::getFullProjectName( projectNamespace, projectName ); } -ProjectStatus::Status ProjectStatus::projectStatus( const std::shared_ptr project ) +ProjectStatus::Status ProjectStatus::projectStatus( const std::shared_ptr project ) { if ( !project || !project->isMergin() || !project->isLocal() ) // This is not a Mergin project or not downloaded project return ProjectStatus::NoVersion; diff --git a/app/project_future.h b/app/project.h similarity index 76% rename from app/project_future.h rename to app/project.h index 87c433158..2a9195d01 100644 --- a/app/project_future.h +++ b/app/project.h @@ -7,8 +7,8 @@ * * ***************************************************************************/ -#ifndef PROJECT_FUTURE_H -#define PROJECT_FUTURE_H +#ifndef PROJECT_H +#define PROJECT_H #include #include @@ -16,7 +16,7 @@ #include #include -struct Project_future; +struct Project; namespace ProjectStatus { Q_NAMESPACE @@ -30,13 +30,13 @@ namespace ProjectStatus { }; Q_ENUM_NS( Status ) - Status projectStatus( const std::shared_ptr project ); + Status projectStatus( const std::shared_ptr project ); } -struct LocalProject_future +struct LocalProject { - LocalProject_future() {}; // TODO: define copy constructor - ~LocalProject_future() {}; + LocalProject() {}; // TODO: define copy constructor + ~LocalProject() {}; QString projectName; QString projectNamespace; @@ -52,21 +52,21 @@ struct LocalProject_future bool isValid() { return !projectDir.isEmpty(); } - bool operator ==( const LocalProject_future &other ) + bool operator ==( const LocalProject &other ) { return ( this->id() == other.id() ); } - bool operator !=( const LocalProject_future &other ) + bool operator !=( const LocalProject &other ) { return !( *this == other ); } }; -struct MerginProject_future +struct MerginProject { - MerginProject_future() {}; - ~MerginProject_future() {}; + MerginProject() {}; + ~MerginProject() {}; QString projectName; QString projectNamespace; @@ -84,24 +84,24 @@ struct MerginProject_future // Maybe better use enum or int for error code QString remoteError; // Error leading to project not being able to sync - bool operator ==( const MerginProject_future &other ) + bool operator ==( const MerginProject &other ) { return ( this->id() == other.id() ); } - bool operator !=( const MerginProject_future &other ) + bool operator !=( const MerginProject &other ) { return !( *this == other ); } }; -struct Project_future +struct Project { - Project_future() {}; - ~Project_future() {}; + Project() {}; + ~Project() {}; - std::unique_ptr mergin; - std::unique_ptr local; + std::unique_ptr mergin; + std::unique_ptr local; bool isMergin() const { return mergin != nullptr; } bool isLocal() const { return local != nullptr; } @@ -127,7 +127,7 @@ struct Project_future return QString(); } - bool operator ==( const Project_future &other ) + bool operator ==( const Project &other ) { if ( this->isLocal() && other.isLocal() ) { @@ -140,13 +140,13 @@ struct Project_future return false; } - bool operator !=( const Project_future &other ) + bool operator !=( const Project &other ) { return !( *this == other ); } }; -typedef QList MerginProjectsList; -typedef QList LocalProjectsList; +typedef QList MerginProjectsList; +typedef QList LocalProjectsList; -#endif // PROJECT_FUTURE_H +#endif // PROJECT_H diff --git a/app/projectsmodel_future.cpp b/app/projectsmodel.cpp similarity index 75% rename from app/projectsmodel_future.cpp rename to app/projectsmodel.cpp index 042a1a8b2..1e23e1d8e 100644 --- a/app/projectsmodel_future.cpp +++ b/app/projectsmodel.cpp @@ -7,51 +7,51 @@ * * ***************************************************************************/ -#include "projectsmodel_future.h" +#include "projectsmodel.h" #include "localprojectsmanager.h" #include "inpututils.h" #include "merginuserauth.h" -ProjectsModel_future::ProjectsModel_future( QObject *parent ) : QAbstractListModel( parent ) +ProjectsModel::ProjectsModel( QObject *parent ) : QAbstractListModel( parent ) { qDebug() << "PMR: Instantiated ProjectsModel! " << this << "MerginAPI: " << mBackend << "LPM:" << mLocalProjectsManager << "Type: " << mModelType; } -void ProjectsModel_future::initializeProjectsModel() +void ProjectsModel::initializeProjectsModel() { if ( !mBackend || !mLocalProjectsManager || mModelType == EmptyProjectsModel ) // Model is not set up properly yet return; qDebug() << "PMR: initializing projects model " << this; - QObject::connect( mBackend, &MerginApi::syncProjectStatusChanged, this, &ProjectsModel_future::onProjectSyncProgressChanged ); - QObject::connect( mBackend, &MerginApi::syncProjectFinished, this, &ProjectsModel_future::onProjectSyncFinished ); - QObject::connect( mBackend, &MerginApi::projectDetached, this, &ProjectsModel_future::onProjectDetachedFromMergin ); - QObject::connect( mBackend, &MerginApi::projectAttachedToMergin, this, &ProjectsModel_future::onProjectAttachedToMergin ); + QObject::connect( mBackend, &MerginApi::syncProjectStatusChanged, this, &ProjectsModel::onProjectSyncProgressChanged ); + QObject::connect( mBackend, &MerginApi::syncProjectFinished, this, &ProjectsModel::onProjectSyncFinished ); + QObject::connect( mBackend, &MerginApi::projectDetached, this, &ProjectsModel::onProjectDetachedFromMergin ); + QObject::connect( mBackend, &MerginApi::projectAttachedToMergin, this, &ProjectsModel::onProjectAttachedToMergin ); if ( mModelType == ProjectModelTypes::LocalProjectsModel ) { - QObject::connect( mBackend, &MerginApi::listProjectsByNameFinished, this, &ProjectsModel_future::onListProjectsByNameFinished ); + QObject::connect( mBackend, &MerginApi::listProjectsByNameFinished, this, &ProjectsModel::onListProjectsByNameFinished ); loadLocalProjects(); } else if ( mModelType != ProjectModelTypes::RecentProjectsModel ) { - QObject::connect( mBackend, &MerginApi::listProjectsFinished, this, &ProjectsModel_future::onListProjectsFinished ); + QObject::connect( mBackend, &MerginApi::listProjectsFinished, this, &ProjectsModel::onListProjectsFinished ); } else { // Implement RecentProjectsModel type } - QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectAdded, this, &ProjectsModel_future::onProjectAdded ); - QObject::connect( mLocalProjectsManager, &LocalProjectsManager::aboutToRemoveLocalProject, this, &ProjectsModel_future::onAboutToRemoveProject ); - QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectDataChanged, this, &ProjectsModel_future::onProjectDataChanged ); - QObject::connect( mLocalProjectsManager, &LocalProjectsManager::dataDirReloaded, this, &ProjectsModel_future::loadLocalProjects ); + QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectAdded, this, &ProjectsModel::onProjectAdded ); + QObject::connect( mLocalProjectsManager, &LocalProjectsManager::aboutToRemoveLocalProject, this, &ProjectsModel::onAboutToRemoveProject ); + QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectDataChanged, this, &ProjectsModel::onProjectDataChanged ); + QObject::connect( mLocalProjectsManager, &LocalProjectsManager::dataDirReloaded, this, &ProjectsModel::loadLocalProjects ); emit modelInitialized(); } -QVariant ProjectsModel_future::data( const QModelIndex &index, int role ) const +QVariant ProjectsModel::data( const QModelIndex &index, int role ) const { if ( !index.isValid() ) return QVariant(); @@ -59,7 +59,7 @@ QVariant ProjectsModel_future::data( const QModelIndex &index, int role ) const if ( index.row() < 0 || index.row() >= mProjects.size() ) return QVariant(); - std::shared_ptr project = mProjects.at( index.row() ); + std::shared_ptr project = mProjects.at( index.row() ); switch ( role ) { case ProjectName: return QVariant( project->projectName() ); @@ -104,14 +104,14 @@ QVariant ProjectsModel_future::data( const QModelIndex &index, int role ) const } } -QModelIndex ProjectsModel_future::index( int row, int col, const QModelIndex &parent ) const +QModelIndex ProjectsModel::index( int row, int col, const QModelIndex &parent ) const { Q_UNUSED( col ) Q_UNUSED( parent ) return createIndex( row, 0, nullptr ); } -QHash ProjectsModel_future::roleNames() const +QHash ProjectsModel::roleNames() const { QHash roles; roles[Roles::ProjectName] = QStringLiteral( "ProjectName" ).toLatin1(); @@ -131,12 +131,12 @@ QHash ProjectsModel_future::roleNames() const return roles; } -int ProjectsModel_future::rowCount( const QModelIndex & ) const +int ProjectsModel::rowCount( const QModelIndex & ) const { return mProjects.count(); } -void ProjectsModel_future::listProjects( const QString &searchExpression, int page ) +void ProjectsModel::listProjects( const QString &searchExpression, int page ) { if ( mModelType == LocalProjectsModel ) { @@ -147,7 +147,7 @@ void ProjectsModel_future::listProjects( const QString &searchExpression, int pa mLastRequestId = mBackend->listProjects( searchExpression, modelTypeToFlag(), "", page ); } -void ProjectsModel_future::listProjectsByName() +void ProjectsModel::listProjectsByName() { if ( mModelType != LocalProjectsModel ) { @@ -157,7 +157,7 @@ void ProjectsModel_future::listProjectsByName() mLastRequestId = mBackend->listProjectsByName( projectNames() ); } -bool ProjectsModel_future::hasMoreProjects() const +bool ProjectsModel::hasMoreProjects() const { if ( mProjects.size() < mServerProjectsCount ) return true; @@ -165,18 +165,18 @@ bool ProjectsModel_future::hasMoreProjects() const return false; } -void ProjectsModel_future::fetchAnotherPage( const QString &searchExpression ) +void ProjectsModel::fetchAnotherPage( const QString &searchExpression ) { listProjects( searchExpression, mPaginatedPage + 1 ); } -QVariant ProjectsModel_future::dataFrom( int fromRole, QVariant fromValue, int desiredRole ) const +QVariant ProjectsModel::dataFrom( int fromRole, QVariant fromValue, int desiredRole ) const { switch ( fromRole ) { case ProjectId: { - std::shared_ptr project = projectFromId( fromValue.toString() ); + std::shared_ptr project = projectFromId( fromValue.toString() ); if ( project ) { QModelIndex ix = index( mProjects.indexOf( project ) ); @@ -202,7 +202,7 @@ QVariant ProjectsModel_future::dataFrom( int fromRole, QVariant fromValue, int d return QVariant(); } -void ProjectsModel_future::onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ) +void ProjectsModel::onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ) { qDebug() << "PMR: onListProjectsFinished(): received response with requestId = " << requestId; if ( mLastRequestId != requestId ) @@ -235,7 +235,7 @@ void ProjectsModel_future::onListProjectsFinished( const MerginProjectsList &mer emit hasMoreProjectsChanged(); } -void ProjectsModel_future::onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ) +void ProjectsModel::onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ) { qDebug() << "PMR: onListProjectsByNameFinished(): received response with requestId = " << requestId; if ( mLastRequestId != requestId ) @@ -250,7 +250,7 @@ void ProjectsModel_future::onListProjectsByNameFinished( const MerginProjectsLis endResetModel(); } -void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious ) +void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious ) { LocalProjectsList localProjects = mLocalProjectsManager->projects(); @@ -264,17 +264,17 @@ void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjec // Keep all local projects and ignore all not downloaded remote projects for ( const auto &localProject : localProjects ) { - std::shared_ptr project = std::shared_ptr( new Project_future() ); - project->local = std::unique_ptr( new LocalProject_future( localProject ) ); + std::shared_ptr project = std::shared_ptr( new Project() ); + project->local = std::unique_ptr( new LocalProject( localProject ) ); - MerginProject_future remoteEntry; + MerginProject remoteEntry; remoteEntry.projectName = project->local->projectName; remoteEntry.projectNamespace = project->local->projectNamespace; if ( merginProjects.contains( remoteEntry ) ) { int i = merginProjects.indexOf( remoteEntry ); - project->mergin = std::unique_ptr( new MerginProject_future( merginProjects[i] ) ); + project->mergin = std::unique_ptr( new MerginProject( merginProjects[i] ) ); if ( pendingProjects.contains( project->mergin->id() ) ) { @@ -287,7 +287,7 @@ void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjec else if ( project->local->localVersion > -1 ) { // this is indeed a Mergin project, it has metadata folder in it - project->mergin = std::unique_ptr( new MerginProject_future() ); + project->mergin = std::unique_ptr( new MerginProject() ); project->mergin->projectName = project->local->projectName; project->mergin->projectNamespace = project->local->projectNamespace; project->mergin->status = ProjectStatus::projectStatus( project ); @@ -301,8 +301,8 @@ void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjec // Keep all remote projects and ignore all non mergin projects from local projects for ( const auto &remoteEntry : merginProjects ) { - std::shared_ptr project = std::shared_ptr( new Project_future() ); - project->mergin = std::unique_ptr( new MerginProject_future( remoteEntry ) ); + std::shared_ptr project = std::shared_ptr( new Project() ); + project->mergin = std::unique_ptr( new MerginProject( remoteEntry ) ); if ( pendingProjects.contains( project->mergin->id() ) ) { @@ -312,14 +312,14 @@ void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjec } // find downloaded projects - LocalProject_future localProject; + LocalProject localProject; localProject.projectName = project->mergin->projectName; localProject.projectNamespace = project->mergin->projectNamespace; if ( localProjects.contains( localProject ) ) { int ix = localProjects.indexOf( localProject ); - project->local = std::unique_ptr( new LocalProject_future( localProjects[ix] ) ); + project->local = std::unique_ptr( new LocalProject( localProjects[ix] ) ); } project->mergin->status = ProjectStatus::projectStatus( project ); @@ -328,9 +328,9 @@ void ProjectsModel_future::mergeProjects( const MerginProjectsList &merginProjec } } -void ProjectsModel_future::syncProject( const QString &projectId ) +void ProjectsModel::syncProject( const QString &projectId ) { - std::shared_ptr project = projectFromId( projectId ); + std::shared_ptr project = projectFromId( projectId ); if ( project == nullptr ) { @@ -364,9 +364,9 @@ void ProjectsModel_future::syncProject( const QString &projectId ) } } -void ProjectsModel_future::stopProjectSync( const QString &projectId ) +void ProjectsModel::stopProjectSync( const QString &projectId ) { - std::shared_ptr project = projectFromId( projectId ); + std::shared_ptr project = projectFromId( projectId ); if ( project == nullptr ) { @@ -398,25 +398,25 @@ void ProjectsModel_future::stopProjectSync( const QString &projectId ) } } -void ProjectsModel_future::removeLocalProject( const QString &projectId ) +void ProjectsModel::removeLocalProject( const QString &projectId ) { mLocalProjectsManager->removeLocalProject( projectId ); } -void ProjectsModel_future::migrateProject( const QString &projectId ) +void ProjectsModel::migrateProject( const QString &projectId ) { // if it is indeed a local project - std::shared_ptr project = projectFromId( projectId ); + std::shared_ptr project = projectFromId( projectId ); if ( project->isLocal() ) mBackend->migrateProjectToMergin( project->local->projectName ); } -void ProjectsModel_future::onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully ) +void ProjectsModel::onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully ) { Q_UNUSED( projectDir ) - std::shared_ptr project = projectFromId( projectFullName ); + std::shared_ptr project = projectFromId( projectFullName ); if ( !project || !project->isMergin() || !successfully ) return; @@ -429,9 +429,9 @@ void ProjectsModel_future::onProjectSyncFinished( const QString &projectDir, con qDebug() << "PMR: Project " << projectFullName << " finished sync"; } -void ProjectsModel_future::onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ) +void ProjectsModel::onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ) { - std::shared_ptr project = projectFromId( projectFullName ); + std::shared_ptr project = projectFromId( projectFullName ); if ( !project || !project->isMergin() ) return; @@ -444,14 +444,14 @@ void ProjectsModel_future::onProjectSyncProgressChanged( const QString &projectF qDebug() << "PMR: Project " << projectFullName << " changed sync progress to " << progress; } -void ProjectsModel_future::onProjectAdded( const LocalProject_future &project ) +void ProjectsModel::onProjectAdded( const LocalProject &project ) { // Check if such project is already in project list - std::shared_ptr proj = projectFromId( project.id() ); + std::shared_ptr proj = projectFromId( project.id() ); if ( proj ) { // add local information ~ project downloaded - proj->local = std::unique_ptr( new LocalProject_future( project ) ); + proj->local = std::unique_ptr( new LocalProject( project ) ); if ( proj->isMergin() ) proj->mergin->status = ProjectStatus::projectStatus( proj ); @@ -461,8 +461,8 @@ void ProjectsModel_future::onProjectAdded( const LocalProject_future &project ) else if ( mModelType == LocalProjectsModel ) { // add project to project list ~ project created - std::shared_ptr newProject = std::shared_ptr( new Project_future() ); - newProject->local = std::unique_ptr( new LocalProject_future( project ) ); + std::shared_ptr newProject = std::shared_ptr( new Project() ); + newProject->local = std::unique_ptr( new LocalProject( project ) ); int insertIndex = mProjects.size(); @@ -474,9 +474,9 @@ void ProjectsModel_future::onProjectAdded( const LocalProject_future &project ) qDebug() << "PMR: Added project" << project.id(); } -void ProjectsModel_future::onAboutToRemoveProject( const LocalProject_future project ) +void ProjectsModel::onAboutToRemoveProject( const LocalProject project ) { - std::shared_ptr proj = projectFromId( project.id() ); + std::shared_ptr proj = projectFromId( project.id() ); if ( proj ) { @@ -504,13 +504,13 @@ void ProjectsModel_future::onAboutToRemoveProject( const LocalProject_future pro } } -void ProjectsModel_future::onProjectDataChanged( const LocalProject_future &project ) +void ProjectsModel::onProjectDataChanged( const LocalProject &project ) { - std::shared_ptr proj = projectFromId( project.id() ); + std::shared_ptr proj = projectFromId( project.id() ); if ( proj ) { - proj->local = std::unique_ptr( new LocalProject_future( project ) ); + proj->local = std::unique_ptr( new LocalProject( project ) ); if ( proj->isMergin() ) proj->mergin->status = ProjectStatus::projectStatus( proj ); @@ -521,9 +521,9 @@ void ProjectsModel_future::onProjectDataChanged( const LocalProject_future &proj qDebug() << "PMR: Data changed in project" << project.id(); } -void ProjectsModel_future::onProjectDetachedFromMergin( const QString &projectFullName ) +void ProjectsModel::onProjectDetachedFromMergin( const QString &projectFullName ) { - std::shared_ptr proj = projectFromId( projectFullName ); + std::shared_ptr proj = projectFromId( projectFullName ); if ( proj ) { @@ -538,7 +538,7 @@ void ProjectsModel_future::onProjectDetachedFromMergin( const QString &projectFu } } -void ProjectsModel_future::onProjectAttachedToMergin( const QString &projectFullName ) +void ProjectsModel::onProjectAttachedToMergin( const QString &projectFullName ) { // To ensure project will be in sync with server, send listProjectByName request. // In theory we could send that request only for this one project. @@ -547,7 +547,7 @@ void ProjectsModel_future::onProjectAttachedToMergin( const QString &projectFull qDebug() << "PMR: Project attached to mergin " << projectFullName; } -void ProjectsModel_future::setMerginApi( MerginApi *merginApi ) +void ProjectsModel::setMerginApi( MerginApi *merginApi ) { if ( !merginApi || mBackend == merginApi ) return; @@ -557,7 +557,7 @@ void ProjectsModel_future::setMerginApi( MerginApi *merginApi ) initializeProjectsModel(); } -void ProjectsModel_future::setLocalProjectsManager( LocalProjectsManager *localProjectsManager ) +void ProjectsModel::setLocalProjectsManager( LocalProjectsManager *localProjectsManager ) { if ( !localProjectsManager || mLocalProjectsManager == localProjectsManager ) return; @@ -567,7 +567,7 @@ void ProjectsModel_future::setLocalProjectsManager( LocalProjectsManager *localP initializeProjectsModel(); } -void ProjectsModel_future::setModelType( ProjectsModel_future::ProjectModelTypes modelType ) +void ProjectsModel::setModelType( ProjectsModel::ProjectModelTypes modelType ) { if ( mModelType == modelType ) return; @@ -577,7 +577,7 @@ void ProjectsModel_future::setModelType( ProjectsModel_future::ProjectModelTypes initializeProjectsModel(); } -QString ProjectsModel_future::modelTypeToFlag() const +QString ProjectsModel::modelTypeToFlag() const { switch ( mModelType ) { @@ -590,7 +590,7 @@ QString ProjectsModel_future::modelTypeToFlag() const } } -void ProjectsModel_future::printProjects() const // TODO: Helper function, remove after refactoring is done +void ProjectsModel::printProjects() const // TODO: Helper function, remove after refactoring is done { qDebug() << "PMR: Model " << this << " with type " << modelTypeToFlag() << " has projects: "; for ( const auto &proj : mProjects ) @@ -609,7 +609,7 @@ void ProjectsModel_future::printProjects() const // TODO: Helper function, remov } } -QStringList ProjectsModel_future::projectNames() const +QStringList ProjectsModel::projectNames() const { QStringList projectNames; LocalProjectsList projects = mLocalProjectsManager->projects(); @@ -623,7 +623,7 @@ QStringList ProjectsModel_future::projectNames() const return projectNames; } -void ProjectsModel_future::loadLocalProjects() +void ProjectsModel::loadLocalProjects() { if ( mModelType == LocalProjectsModel ) { @@ -634,13 +634,13 @@ void ProjectsModel_future::loadLocalProjects() } } -bool ProjectsModel_future::containsProject( QString projectId ) const +bool ProjectsModel::containsProject( QString projectId ) const { - std::shared_ptr proj = projectFromId( projectId ); + std::shared_ptr proj = projectFromId( projectId ); return proj != nullptr; } -std::shared_ptr ProjectsModel_future::projectFromId( QString projectId ) const +std::shared_ptr ProjectsModel::projectFromId( QString projectId ) const { for ( int ix = 0; ix < mProjects.size(); ++ix ) { @@ -653,7 +653,7 @@ std::shared_ptr ProjectsModel_future::projectFromId( QString pro return nullptr; } -ProjectsModel_future::ProjectModelTypes ProjectsModel_future::modelType() const +ProjectsModel::ProjectModelTypes ProjectsModel::modelType() const { return mModelType; } diff --git a/app/projectsmodel_future.h b/app/projectsmodel.h similarity index 88% rename from app/projectsmodel_future.h rename to app/projectsmodel.h index 2dbabfb32..178395941 100644 --- a/app/projectsmodel_future.h +++ b/app/projectsmodel.h @@ -7,21 +7,21 @@ * * ***************************************************************************/ -#ifndef PROJECTSMODEL_FUTURE_H -#define PROJECTSMODEL_FUTURE_H +#ifndef PROJECTSMODEL_H +#define PROJECTSMODEL_H #include #include -#include "project_future.h" +#include "project.h" #include "merginapi.h" class LocalProjectsManager; /** - * \brief The ProjectsModel_future class + * \brief The ProjectsModel class */ -class ProjectsModel_future : public QAbstractListModel +class ProjectsModel : public QAbstractListModel { Q_OBJECT @@ -60,8 +60,8 @@ class ProjectsModel_future : public QAbstractListModel }; Q_ENUM( ProjectModelTypes ) - ProjectsModel_future( QObject *parent = nullptr ); - ~ProjectsModel_future() override {}; + ProjectsModel( QObject *parent = nullptr ); + ~ProjectsModel() override {}; // From Qt 5.15 we can use REQUIRED keyword here that will ensure object will be always instantiated from QML with these mandatory properties Q_PROPERTY( MerginApi *merginApi READ merginApi WRITE setMerginApi ) @@ -101,7 +101,7 @@ class ProjectsModel_future : public QAbstractListModel //! Method merging local and remote projects based on the model type void mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious = false ); - ProjectsModel_future::ProjectModelTypes modelType() const; + ProjectsModel::ProjectModelTypes modelType() const; MerginApi *merginApi() const { return mBackend; } @@ -119,9 +119,9 @@ public slots: void onProjectAttachedToMergin( const QString &projectFullName ); // LocalProjectsManager signals - void onProjectAdded( const LocalProject_future &project ); - void onAboutToRemoveProject( const LocalProject_future project ); - void onProjectDataChanged( const LocalProject_future &project ); + void onProjectAdded( const LocalProject &project ); + void onAboutToRemoveProject( const LocalProject project ); + void onProjectDataChanged( const LocalProject &project ); void setMerginApi( MerginApi *merginApi ); void setLocalProjectsManager( LocalProjectsManager *localProjectsManager ); @@ -139,11 +139,11 @@ public slots: void initializeProjectsModel(); bool containsProject( QString projectId ) const; - std::shared_ptr projectFromId( QString projectId ) const; + std::shared_ptr projectFromId( QString projectId ) const; MerginApi *mBackend = nullptr; LocalProjectsManager *mLocalProjectsManager = nullptr; - QList> mProjects; + QList> mProjects; ProjectModelTypes mModelType = EmptyProjectsModel; @@ -155,4 +155,4 @@ public slots: QString mLastRequestId; }; -#endif // PROJECTSMODEL_FUTURE_H +#endif // PROJECTSMODEL_H diff --git a/app/projectsproxymodel_future.cpp b/app/projectsproxymodel.cpp similarity index 57% rename from app/projectsproxymodel_future.cpp rename to app/projectsproxymodel.cpp index 8c3332b9d..c355423c7 100644 --- a/app/projectsproxymodel_future.cpp +++ b/app/projectsproxymodel.cpp @@ -7,34 +7,34 @@ * * ***************************************************************************/ -#include "projectsproxymodel_future.h" +#include "projectsproxymodel.h" -ProjectsProxyModel_future::ProjectsProxyModel_future( QObject *parent ) : QSortFilterProxyModel( parent ) +ProjectsProxyModel::ProjectsProxyModel( QObject *parent ) : QSortFilterProxyModel( parent ) { } -void ProjectsProxyModel_future::initialize() +void ProjectsProxyModel::initialize() { setSourceModel( mModel ); mModelType = mModel->modelType(); - setFilterRole( ProjectsModel_future::ProjectFullName ); + setFilterRole( ProjectsModel::ProjectFullName ); setFilterCaseSensitivity( Qt::CaseInsensitive ); sort( 0, Qt::AscendingOrder ); } -QString ProjectsProxyModel_future::searchExpression() const +QString ProjectsProxyModel::searchExpression() const { return mSearchExpression; } -ProjectsModel_future *ProjectsProxyModel_future::projectSourceModel() const +ProjectsModel *ProjectsProxyModel::projectSourceModel() const { return mModel; } -void ProjectsProxyModel_future::setSearchExpression( QString searchExpression ) +void ProjectsProxyModel::setSearchExpression( QString searchExpression ) { if ( mSearchExpression == searchExpression ) return; @@ -44,22 +44,22 @@ void ProjectsProxyModel_future::setSearchExpression( QString searchExpression ) emit searchExpressionChanged( mSearchExpression ); } -void ProjectsProxyModel_future::setProjectSourceModel( ProjectsModel_future *sourceModel ) +void ProjectsProxyModel::setProjectSourceModel( ProjectsModel *sourceModel ) { if ( mModel == sourceModel ) return; mModel = sourceModel; - QObject::connect( mModel, &ProjectsModel_future::modelInitialized, this, &ProjectsProxyModel_future::initialize ); + QObject::connect( mModel, &ProjectsModel::modelInitialized, this, &ProjectsProxyModel::initialize ); } -bool ProjectsProxyModel_future::lessThan( const QModelIndex &left, const QModelIndex &right ) const +bool ProjectsProxyModel::lessThan( const QModelIndex &left, const QModelIndex &right ) const { - if ( mModelType == ProjectsModel_future::LocalProjectsModel ) + if ( mModelType == ProjectsModel::LocalProjectsModel ) { - bool lProjectIsMergin = mModel->data( left, ProjectsModel_future::ProjectIsMergin ).toBool(); - bool rProjectIsMergin = mModel->data( right, ProjectsModel_future::ProjectIsMergin ).toBool(); + bool lProjectIsMergin = mModel->data( left, ProjectsModel::ProjectIsMergin ).toBool(); + bool rProjectIsMergin = mModel->data( right, ProjectsModel::ProjectIsMergin ).toBool(); /** * Ordering of local projects: first non-mergin projects (using folder name), @@ -68,8 +68,8 @@ bool ProjectsProxyModel_future::lessThan( const QModelIndex &left, const QModelI if ( !lProjectIsMergin && !rProjectIsMergin ) { - QString lProjectFullName = mModel->data( left, ProjectsModel_future::ProjectFullName ).toString(); - QString rProjectFullName = mModel->data( right, ProjectsModel_future::ProjectFullName ).toString(); + QString lProjectFullName = mModel->data( left, ProjectsModel::ProjectFullName ).toString(); + QString rProjectFullName = mModel->data( right, ProjectsModel::ProjectFullName ).toString(); qDebug() << "Comparing " << lProjectFullName << rProjectFullName; return lProjectFullName.compare( rProjectFullName, Qt::CaseInsensitive ) < 0; @@ -83,10 +83,10 @@ bool ProjectsProxyModel_future::lessThan( const QModelIndex &left, const QModelI return false; } - QString lNamespace = mModel->data( left, ProjectsModel_future::ProjectNamespace ).toString(); - QString lProjectName = mModel->data( left, ProjectsModel_future::ProjectName ).toString(); - QString rNamespace = mModel->data( right, ProjectsModel_future::ProjectNamespace ).toString(); - QString rProjectName = mModel->data( right, ProjectsModel_future::ProjectName ).toString(); + QString lNamespace = mModel->data( left, ProjectsModel::ProjectNamespace ).toString(); + QString lProjectName = mModel->data( left, ProjectsModel::ProjectName ).toString(); + QString rNamespace = mModel->data( right, ProjectsModel::ProjectNamespace ).toString(); + QString rProjectName = mModel->data( right, ProjectsModel::ProjectName ).toString(); if ( lNamespace == rNamespace ) { diff --git a/app/projectsproxymodel_future.h b/app/projectsproxymodel.h similarity index 61% rename from app/projectsproxymodel_future.h rename to app/projectsproxymodel.h index bf35046ea..730ae1c48 100644 --- a/app/projectsproxymodel_future.h +++ b/app/projectsproxymodel.h @@ -7,34 +7,34 @@ * * ***************************************************************************/ -#ifndef PROJECTSPROXYMODEL_FUTURE_H -#define PROJECTSPROXYMODEL_FUTURE_H +#ifndef PROJECTSPROXYMODEL_H +#define PROJECTSPROXYMODEL_H #include #include -#include "projectsmodel_future.h" +#include "projectsmodel.h" /** - * @brief The ProjectsProxyModel_future class + * @brief The ProjectsProxyModel class */ -class ProjectsProxyModel_future : public QSortFilterProxyModel +class ProjectsProxyModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY( QString searchExpression READ searchExpression WRITE setSearchExpression NOTIFY searchExpressionChanged ) - Q_PROPERTY( ProjectsModel_future *projectSourceModel READ projectSourceModel WRITE setProjectSourceModel ) + Q_PROPERTY( ProjectsModel *projectSourceModel READ projectSourceModel WRITE setProjectSourceModel ) public: - explicit ProjectsProxyModel_future( QObject *parent = nullptr ); - ~ProjectsProxyModel_future() override {}; + explicit ProjectsProxyModel( QObject *parent = nullptr ); + ~ProjectsProxyModel() override {}; QString searchExpression() const; - ProjectsModel_future *projectSourceModel() const; + ProjectsModel *projectSourceModel() const; public slots: void setSearchExpression( QString searchExpression ); - void setProjectSourceModel( ProjectsModel_future *sourceModel ); + void setProjectSourceModel( ProjectsModel *sourceModel ); signals: void searchExpressionChanged( QString SearchExpression ); @@ -45,9 +45,9 @@ public slots: private: void initialize(); - ProjectsModel_future *mModel; - ProjectsModel_future::ProjectModelTypes mModelType = ProjectsModel_future::EmptyProjectsModel; + ProjectsModel *mModel; + ProjectsModel::ProjectModelTypes mModelType = ProjectsModel::EmptyProjectsModel; QString mSearchExpression; }; -#endif // PROJECTSPROXYMODEL_FUTURE_H +#endif // PROJECTSPROXYMODEL_H diff --git a/app/sources.pri b/app/sources.pri index 7581e7c8a..f0b9cc887 100644 --- a/app/sources.pri +++ b/app/sources.pri @@ -31,9 +31,9 @@ ios/iosutils.cpp \ inputprojutils.cpp \ codefilter.cpp \ qrdecoder.cpp \ -projectsproxymodel_future.cpp \ -projectsmodel_future.cpp \ -project_future.cpp \ +project.cpp \ +projectsmodel.cpp \ +projectsproxymodel.cpp \ exists(merginsecrets.cpp) { message("Using production Mergin API_KEYS") @@ -75,9 +75,9 @@ ios/iosutils.h \ inputprojutils.h \ codefilter.h \ qrdecoder.h \ -projectsproxymodel_future.h \ -projectsmodel_future.h \ -project_future.h \ +project.h \ +projectsmodel.h \ +projectsproxymodel.h \ contains(DEFINES, INPUT_TEST) { From 0baef1c58082d12a1a18017591716dcc1cb50c86 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Wed, 31 Mar 2021 12:56:09 +0200 Subject: [PATCH 24/53] remove more unused qml code --- app/qml/ProjectPanel.qml | 458 +-------------------------------------- app/qml/main.qml | 13 +- 2 files changed, 17 insertions(+), 454 deletions(-) diff --git a/app/qml/ProjectPanel.qml b/app/qml/ProjectPanel.qml index 477ad49fd..e0774f153 100644 --- a/app/qml/ProjectPanel.qml +++ b/app/qml/ProjectPanel.qml @@ -390,141 +390,7 @@ Item { } } -// SearchBar { -// id: searchBar -// y: pageHeader.height -// allowTimer: true - -// onSearchTextChanged: { -// if (toolbar.highlighted === homeBtn.text) { -// __localProjectsProxyModel.searchExpression = text -// } else if (toolbar.highlighted === exploreBtn.text) { -// // Filtered by request -// exploreBtn.activated() -// } else if (toolbar.highlighted === sharedProjectsBtn.text) { -// __merginProjectsModel.searchExpression = text -// } else if (toolbar.highlighted === myProjectsBtn.text) { -// __merginProjectsModel.searchExpression = text -// } -// } -// } - -// // Content -// ColumnLayout { -// id: contentLayout -// height: projectsPanel.height-pageHeader.height-searchBar.height-toolbar.height -// width: parent.width -// y: pageHeader.height + searchBar.height -// spacing: 0 - - // Info label -// Item { -// id: infoLabel -// width: parent.width -// height: toolbar.highlighted === exploreBtn.text ? projectsPanel.rowHeight * 2 : 0 -// visible: height - -// Text { -// anchors.horizontalCenter: parent.horizontalCenter -// anchors.verticalCenter: parent.verticalCenter -// horizontalAlignment: Text.AlignHCenter -// verticalAlignment: Text.AlignVCenter -// wrapMode: Text.WordWrap -// color: InputStyle.panelBackgroundDarker -// font.pixelSize: InputStyle.fontPixelSizeNormal -// text: qsTr("Explore public projects.") -// visible: parent.height -// } - -// // To not propagate click on canvas on background -// MouseArea { -// anchors.fill: parent -// } - -// Item { -// id: infoLabelHideBtn -// height: projectsPanel.iconSize -// width: height -// anchors.right: parent.right -// anchors.top: parent.top -// anchors.rightMargin: projectsPanel.panelMargin -// anchors.topMargin: projectsPanel.panelMargin - -// MouseArea { -// anchors.fill: parent -// onClicked: infoLabel.visible = false -// } - -// Image { -// id: infoLabelHide -// anchors.centerIn: infoLabelHideBtn -// source: 'no.svg' -// height: infoLabelHideBtn.height -// width: height -// sourceSize.width: width -// sourceSize.height: height -// fillMode: Image.PreserveAspectFit -// } - -// ColorOverlay { -// anchors.fill: infoLabelHide -// source: infoLabelHide -// color: InputStyle.panelBackgroundDark -// } -// } - -// Rectangle { -// id: borderLine -// color: InputStyle.panelBackground2 -// width: parent.width -// height: 1 * QgsQuick.Utils.dp -// anchors.bottom: parent.bottom -// } -// } - -// ProjectListPage { -// id: localProjectsList - - -// } - -// ListView { -// id: grid -// Layout.fillWidth: true -// Layout.fillHeight: true -// contentWidth: grid.width -// clip: true -// visible: !showMergin -// maximumFlickVelocity: __androidUtils.isAndroid ? InputStyle.scrollVelocityAndroid : maximumFlickVelocity - -// model: ProjectsProxyModel { -// projectSourceModel: ProjectsModel { -// id: myModel -// localProjectsManager: __localProjectsManager -// modelType: ProjectsModel.LocalProjectsModel -// merginApi: __merginApi -// } -// } - -// property int cellWidth: width -// property int cellHeight: projectsPanel.rowHeight -// property int borderWidth: 1 - -// delegate: delegateItem - -// footer: DelegateButton { -// height: projectsPanel.rowHeight -// width: parent.width -// text: qsTr("Create project") -// onClicked: { -// if (__inputUtils.hasStoragePermission()) { -// stackView.push(projectWizardComp) -// } else if (__inputUtils.acquireStoragePermission()) { -// restartAppDialog.open() -// } -// } -// } - +//------------------------------------------------- // Text { // id: noProjectsText // anchors.fill: parent @@ -571,29 +437,9 @@ Item { // padding: InputStyle.panelMargin/2 // } // } +//------------------------------------------------- -// ListView { -// id: merginProjectsList - -// property int paginatedPage: 0 - -// visible: showMergin && !busyIndicator.running -// Layout.fillWidth: true -// Layout.fillHeight: true -// contentWidth: grid.width -// clip: true -// maximumFlickVelocity: __androidUtils.isAndroid ? InputStyle.scrollVelocityAndroid : maximumFlickVelocity - -// onCountChanged: { -// if (merginProjectsList.visible || paginatedPage > 1) { -// merginProjectsList.positionViewAtIndex(merginProjectsList.currentIndex, ListView.End) -// } -// } - -// property int cellWidth: width -// property int cellHeight: projectsPanel.rowHeight -// property int borderWidth: 1 - +//------------------------------------------------- // TODO: unable to get list of the projects // Label { // anchors.fill: parent @@ -605,308 +451,20 @@ Item { // font.pixelSize: InputStyle.fontPixelSizeNormal // font.bold: true // } +//------------------------------------------------- -// delegate: delegateItemMergin - -// footer: Button { -// text: "ListProjects API" -// onClicked: { -// __myProjectsModel.listProjects(merginProjectsList.paginatedPage + 1) -// merginProjectsList.paginatedPage++ -// } -// } -// } - -// } - -// Component { -// id: delegateItem - -// ProjectDelegateItem { -// id: delegateItemContent - -// projectFullName: model.ProjectFullName -// projectId: model.ProjectId -// projectDescription: model.ProjectDescription -// projectStatus: model.ProjectSyncStatus -// projectIsValid: model.ProjectIsValid -// projectIsPending: model.ProjectPending ? true : false -// projectSyncProgress: model.ProjectSyncProgress ? ProjectSyncProgress : 0 -// projectIsLocal: model.ProjectIsLocal -// projectIsMergin: model.ProjectIsMergin - -// width: parent.width -// height: projectsPanel.rowHeight -// viewContentY: ListView.view.contentY -// viewHeight: ListView.view.height - -// onOpenRequested: console.log( "PMR: Open", projectId ) -// onSyncRequested: console.log( "PMR: Sync", projectId ) -// onMigrateRequested: console.log( "PMR: Upload", projectId ) -// onRemoveRequested: console.log( "PMR: Remove", projectId ) -// onStopSyncRequested: console.log( "PMR: Stop", projectId ) -// onShowChangesRequested: console.log( "PMR: Show changes", projectId ) -// } -// } - -// ProjectDelegateItem { -// id: delegateItemContent -// cellWidth: projectsPanel.width -// cellHeight: projectsPanel.rowHeight -// iconSize: projectsPanel.iconSize -// width: cellWidth -// height: cellHeight -// visible: height ? true : false -// statusIconSource: "more_menu.svg" -// itemMargin: projectsPanel.panelMargin -// projectFullName: ProjectFullName -// projectDescription: getStatusIcon( ProjectSyncStatus, ProjectPending ) -// disabled: !ProjectIsValid // invalid project -// highlight: { -// if (disabled) return true -// return ProjectFilePath === projectsPanel.activeProjectPath ? true : false -// } - -// Menu { -// property real menuItemHeight: projectsPanel.rowHeight * 0.8 -// id: contextMenu -// height: menuItemHeight * 2 -// width:Math.min( parent.width, 300 * QgsQuick.Utils.dp ) -// leftMargin: Math.max(parent.width - width, 0) - -// //! sets y-offset either above or below related item according relative position to end of the list -// onAboutToShow: { -// var itemRelativeY = parent.y - grid.contentY -// if (itemRelativeY + contextMenu.height >= grid.height) -// contextMenu.y = -contextMenu.height -// else -// contextMenu.y = parent.height -// } - -// MenuItem { -// height: ProjectIsMergin ? contextMenu.menuItemHeight : 0 -// ExtendedMenuItem { -// height: parent.height -// rowHeight: parent.height -// width: parent.width -// contentText: qsTr("Status") -// imageSource: InputStyle.infoIcon -// overlayImage: true -// } -// onClicked: { -// if (__merginProjectStatusModel.loadProjectInfo(delegateItemContent.projectFullName)) { -// stackView.push(statusPanelComp) -// } else __inputUtils.showNotification(qsTr("No Changes")) -// } -// } - -// MenuItem { -// height: ProjectIsMergin ? 0: contextMenu.menuItemHeight -// ExtendedMenuItem { -// height: parent.height -// rowHeight: parent.height -// width: parent.width -// contentText: qsTr("Upload to Mergin") -// imageSource: InputStyle.uploadIcon -// overlayImage: true -// } -// onClicked: { -// if (!ProjectIsMergin) { -// __merginApi.migrateProjectToMergin(projectName) -// } -// } -// } - -// MenuItem { -// height: contextMenu.menuItemHeight -// ExtendedMenuItem { -// height: parent.height -// rowHeight: parent.height -// width: parent.width -// contentText: qsTr("Remove from device") -// imageSource: InputStyle.removeIcon -// overlayImage: true -// } -// onClicked: { -// deleteDialog.relatedProjectDirectory = ProjectDirectory -// deleteDialog.open() -// } -// } -// } - -// onItemClicked: { -// if (showMergin) return - -// projectsPanel.activeProjectIndex = index -// projectsPanel.visible = false -// } - -// onMenuClicked:contextMenu.open() -// } - -// Component { -// id: delegateItemMergin - -// Rectangle { -// width: 20 -// height: 20 -// color: "green" -// } - -// ProjectDelegateItem { -// cellWidth: projectsPanel.width -// cellHeight: projectsPanel.rowHeight -// width: cellWidth -// height: cellHeight -// visible: height ? true : false -// pending: ProjectPending -// statusIconSource: getStatusIcon(ProjectSyncStatus, ProjectPending) -// iconSize: projectsPanel.iconSize -// projectFullName: ProjectFullName -// progressValue: ProjectSyncProgress -// projectDescription: ProjectDescription -// isAdditional: __myProjectsModel.canFetchMore() // TODO: replace with delegate button on listview footer - -// onMenuClicked: { -// if ( !__inputUtils.hasStoragePermission() ) { -// if ( __inputUtils.acquireStoragePermission() ) -// restartAppDialog.open() // TODO: replace with reload data! -// return -// } - -// if (ProjectSyncStatus === ProjectStatus.UpToDate) return - -// if ( ProjectPending ) { -// __myProjectsModel.stopProjectSync(ProjectNamespace, ProjectName) -// return -// } - -// __myProjectsModel.syncProject(ProjectNamespace, ProjectName) -// } - -// onDelegateButtonClicked: { // TODO: replace with footer property -// var searchText = searchBar.text - -// // Note that current index used to save last item position -// merginProjectsList.currentIndex = merginProjectsList.count - 1 // TODO: huh? - -// __myProjectsModel.listProjects(merginProjectsList.paginatedPage + 1, searchText) -// } - -// } -// } - - - // Toolbar -// Rectangle { -// property int itemSize: toolbar.height * 0.8 -// property string highlighted: homeBtn.text - -// id: toolbar -// height: InputStyle.rowHeightHeader -// width: parent.width -// anchors.bottom: parent.bottom -// color: InputStyle.clrPanelBackground -// visible: false - -// MouseArea { -// anchors.fill: parent -// onClicked: {} // dont do anything, just do not let click event propagate -// } +// Toolbar +//------------------------------------------------- // onHighlightedChanged: { //// searchBar.deactivate() // if (toolbar.highlighted === homeBtn.text) { //// __projectsModel.searchExpression = "" // } else { -// __merginApi.pingMergin() -// } -// } - -// Row { -// height: toolbar.height -// width: parent.width -// anchors.bottom: parent.bottom - -// Item { -// width: parent.width/parent.children.length -// height: parent.height - -// MainPanelButton { - -// id: homeBtn -// width: toolbar.itemSize -// text: qsTr("Home") -// imageSource: "home.svg" -// faded: toolbar.highlighted !== homeBtn.text - -// onActivated: { -// toolbar.highlighted = homeBtn.text; -// showMergin = false -//// stackView.pending = true -// myModel.listProjectsByName() -// } -// } -// } - -// Item { -// width: parent.width/parent.children.length -// height: parent.height -// MainPanelButton { -// id: myProjectsBtn -// width: toolbar.itemSize -// text: qsTr("My projects") -// imageSource: "account.svg" -// faded: toolbar.highlighted !== myProjectsBtn.text - -// onActivated: { -// toolbar.highlighted = myProjectsBtn.text -// stackView.pending = true -// showMergin = true -// __myProjectsModel.listProjects() -// } -// } -// } - -// Item { -// width: parent.width/parent.children.length -// height: parent.height -// MainPanelButton { -// id: sharedProjectsBtn -// width: toolbar.itemSize -// text: parent.width > sharedProjectsBtn.width * 2 ? qsTr("Shared with me") : qsTr("Shared") -// imageSource: "account-multi.svg" -// faded: toolbar.highlighted !== sharedProjectsBtn.text - -// onActivated: { -// toolbar.highlighted = sharedProjectsBtn.text -// stackView.pending = true -// showMergin = true -// __merginApi.listProjects("", "shared") -// } -// } -// } - -// Item { -// width: parent.width/parent.children.length -// height: parent.height -// MainPanelButton { -// id: exploreBtn -// width: toolbar.itemSize -// text: qsTr("Explore") -// imageSource: "explore.svg" -// faded: toolbar.highlighted !== exploreBtn.text - -// onActivated: { -// toolbar.highlighted = exploreBtn.text -// stackView.pending = true -// showMergin = true -// __merginApi.listProjects( searchBar.text ) -// } -// } +// __merginApi.pingMergin() <------------------!!! // } // } -// } +//------------------------------------------------- // Other components diff --git a/app/qml/main.qml b/app/qml/main.qml index ab52c57a4..5ff8a474d 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -276,6 +276,15 @@ ApplicationWindow { if ( __appSettings.activeProject ) mainPanel.forceActiveFocus() + // DO NOT COMMIT!!! vvv + if ( !__androidUtils.isAndroid ) + { + window.width = 423 + window.height = 601 + window.x = 1100 + window.y = 266 + } + console.log("Completed Running!") } @@ -686,10 +695,6 @@ ApplicationWindow { onNotify: showMessage(message) onProjectDataChanged: { -// var projectName = __projectsModel.data(__projectsModel.index(openProjectPanel.activeProjectIndex), ProjectModel.ProjectName) -// var projectNamespace = __projectsModel.data(__projectsModel.index(openProjectPanel.activeProjectIndex), ProjectModel.ProjectNamespace) -// var currentProjectFullName = __merginApi.getFullProjectName(projectNamespace, projectName) - //! if current project has been updated, refresh canvas if (projectFullName === projectPanel.activeProjectId) { mapCanvas.mapSettings.extentChanged() From 7d2f464ed5b8093d5b25a2bc296bdca525e05f84 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Wed, 31 Mar 2021 13:28:58 +0200 Subject: [PATCH 25/53] check all nit --- app/inpututils.cpp | 2 +- app/project.h | 3 ++- app/projectsmodel.cpp | 14 +++++++++----- app/projectsmodel.h | 6 +++--- app/projectsproxymodel.h | 24 ++++++++++++------------ 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index f4b75a75a..00759d7c2 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -490,7 +490,7 @@ QString InputUtils::createUniqueProjectDirectory( const QString &baseDataDir, co bool InputUtils::removeDir( const QString &dir ) { - if( dir.isEmpty() || dir == "/" ) + if ( dir.isEmpty() || dir == "/" ) return false; return QDir( dir ).removeRecursively(); diff --git a/app/project.h b/app/project.h index 2a9195d01..f23d752b0 100644 --- a/app/project.h +++ b/app/project.h @@ -18,7 +18,8 @@ struct Project; -namespace ProjectStatus { +namespace ProjectStatus +{ Q_NAMESPACE enum Status { diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index 1e23e1d8e..e65661915 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -61,7 +61,8 @@ QVariant ProjectsModel::data( const QModelIndex &index, int role ) const std::shared_ptr project = mProjects.at( index.row() ); - switch ( role ) { + switch ( role ) + { case ProjectName: return QVariant( project->projectName() ); case ProjectNamespace: return QVariant( project->projectNamespace() ); case ProjectFullName: return QVariant( project->projectId() ); @@ -71,12 +72,14 @@ QVariant ProjectsModel::data( const QModelIndex &index, int role ) const case ProjectSyncStatus: return QVariant( project->isMergin() ? project->mergin->status : ProjectStatus::NoVersion ); case ProjectFilePath: return QVariant( project->isLocal() ? project->local->qgisProjectFilePath : QString() ); case ProjectDirectory: return QVariant( project->isLocal() ? project->local->projectDir : QString() ); - case ProjectIsValid: { + case ProjectIsValid: + { if ( !project->isLocal() ) return true; // Mergin projects are by default valid, remote error only affects syncing, not opening of a project return project->local->projectError.isEmpty(); } - case ProjectDescription: { + case ProjectDescription: + { if ( project->isLocal() ) { if ( !project->local->projectError.isEmpty() ) @@ -92,7 +95,8 @@ QVariant ProjectsModel::data( const QModelIndex &index, int role ) const } return QVariant(); // This should not happen } - default: { + default: + { if ( !project->isMergin() ) return QVariant(); // Roles only for projects that has mergin part @@ -655,5 +659,5 @@ std::shared_ptr ProjectsModel::projectFromId( QString projectId ) const ProjectsModel::ProjectModelTypes ProjectsModel::modelType() const { - return mModelType; + return mModelType; } diff --git a/app/projectsmodel.h b/app/projectsmodel.h index 178395941..07814ac1c 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -109,7 +109,7 @@ class ProjectsModel : public QAbstractListModel bool hasMoreProjects() const; -public slots: + public slots: // MerginAPI - backend signals void onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ); void onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ); @@ -127,11 +127,11 @@ public slots: void setLocalProjectsManager( LocalProjectsManager *localProjectsManager ); void setModelType( ProjectModelTypes modelType ); -signals: + signals: void modelInitialized(); void hasMoreProjectsChanged(); -private: + private: QString modelTypeToFlag() const; void printProjects() const; QStringList projectNames() const; diff --git a/app/projectsproxymodel.h b/app/projectsproxymodel.h index 730ae1c48..3c9dd885f 100644 --- a/app/projectsproxymodel.h +++ b/app/projectsproxymodel.h @@ -20,26 +20,26 @@ */ class ProjectsProxyModel : public QSortFilterProxyModel { - Q_OBJECT + Q_OBJECT - Q_PROPERTY( QString searchExpression READ searchExpression WRITE setSearchExpression NOTIFY searchExpressionChanged ) - Q_PROPERTY( ProjectsModel *projectSourceModel READ projectSourceModel WRITE setProjectSourceModel ) + Q_PROPERTY( QString searchExpression READ searchExpression WRITE setSearchExpression NOTIFY searchExpressionChanged ) + Q_PROPERTY( ProjectsModel *projectSourceModel READ projectSourceModel WRITE setProjectSourceModel ) -public: + public: explicit ProjectsProxyModel( QObject *parent = nullptr ); ~ProjectsProxyModel() override {}; - QString searchExpression() const; - ProjectsModel *projectSourceModel() const; + QString searchExpression() const; + ProjectsModel *projectSourceModel() const; -public slots: - void setSearchExpression( QString searchExpression ); - void setProjectSourceModel( ProjectsModel *sourceModel ); + public slots: + void setSearchExpression( QString searchExpression ); + void setProjectSourceModel( ProjectsModel *sourceModel ); -signals: - void searchExpressionChanged( QString SearchExpression ); + signals: + void searchExpressionChanged( QString SearchExpression ); -protected: + protected: bool lessThan( const QModelIndex &left, const QModelIndex &right ) const override; private: From 5fc2c8fb9408ae096c7d2be807a2e152af353aaa Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Wed, 31 Mar 2021 15:05:50 +0200 Subject: [PATCH 26/53] revert description --- app/qml/components/ProjectDelegateItem.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/qml/components/ProjectDelegateItem.qml b/app/qml/components/ProjectDelegateItem.qml index f659bbc0c..c4964e2e9 100644 --- a/app/qml/components/ProjectDelegateItem.qml +++ b/app/qml/components/ProjectDelegateItem.qml @@ -183,8 +183,7 @@ Rectangle { visible: !projectIsPending height: textContainer.height/2 -// text: projectDescription - text: projectStatus + text: projectDescription anchors.right: parent.right anchors.bottom: parent.bottom anchors.left: parent.left From b8fc78b7fe650dc855fbf153f0d56e5b6c11bc19 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Wed, 31 Mar 2021 15:12:49 +0200 Subject: [PATCH 27/53] show only project name in created --- app/qml/components/ProjectDelegateItem.qml | 4 ++-- app/qml/components/ProjectList.qml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/qml/components/ProjectDelegateItem.qml b/app/qml/components/ProjectDelegateItem.qml index c4964e2e9..081bf3812 100644 --- a/app/qml/components/ProjectDelegateItem.qml +++ b/app/qml/components/ProjectDelegateItem.qml @@ -21,7 +21,7 @@ Rectangle { id: root // Required properties - property string projectFullName + property string projectDisplayName property string projectId property string projectDescription property int projectStatus @@ -168,7 +168,7 @@ Rectangle { Text { id: mainText - text: __inputUtils.formatProjectName( projectFullName ) + text: __inputUtils.formatProjectName( projectDisplayName ) height: textContainer.height/2 width: textContainer.width font.pixelSize: InputStyle.fontPixelSizeNormal diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml index 5ed62bb6c..499ddbefa 100644 --- a/app/qml/components/ProjectList.qml +++ b/app/qml/components/ProjectList.qml @@ -75,7 +75,7 @@ Item { width: parent.width height: InputStyle.rowHeightHeader * 1.2 - projectFullName: model.ProjectFullName + projectDisplayName: root.projectModelType === ProjectsModel.CreatedProjectsModel ? model.ProjectName : model.ProjectFullName projectId: model.ProjectId projectDescription: model.ProjectDescription projectStatus: model.ProjectSyncStatus ? model.ProjectSyncStatus : ProjectStatus.NoVersion From 5bbe53196a4a70057ff788113aa66168fffad426 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Tue, 6 Apr 2021 09:40:18 +0200 Subject: [PATCH 28/53] address comments --- app/main.cpp | 1 - app/merginapi.cpp | 2 +- app/merginapi.h | 5 ++--- app/qml/main.qml | 15 ++------------- 4 files changed, 5 insertions(+), 18 deletions(-) diff --git a/app/main.cpp b/app/main.cpp index f457870c6..6a9e8d3bd 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -232,7 +232,6 @@ void initDeclarative() qmlRegisterType( "lc", 1, 0, "PositionDirection" ); qmlRegisterType( "lc", 1, 0, "FieldsModel" ); qmlRegisterType( "lc", 1, 0, "CodeFilter" ); - qmlRegisterType( "lc", 1, 0, "ProjectsModel" ); qmlRegisterType( "lc", 1, 0, "ProjectsProxyModel" ); qmlRegisterUncreatableMetaObject( ProjectStatus::staticMetaObject, "lc", 1, 0, "ProjectStatus", "ProjectStatus Enum" ); diff --git a/app/merginapi.cpp b/app/merginapi.cpp index 0d2d7cb71..9468b8996 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -1668,7 +1668,7 @@ void MerginApi::startProjectUpdate( const QString &projectFullName, const QByteA TransactionStatus &transaction = mTransactionalStatus[projectFullName]; LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); - if ( projectInfo.isValid() ) // If project is already downloaded + if ( projectInfo.isValid() ) { transaction.projectDir = projectInfo.projectDir; } diff --git a/app/merginapi.h b/app/merginapi.h index d621da22c..338d22c0b 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -308,9 +308,6 @@ class MerginApi: public QObject */ Q_INVOKABLE void detachProjectFromMergin( const QString &projectNamespace, const QString &projectName ); - - LocalProject getLocalProject( const QString &projectFullName ); // Test function - static const int MERGIN_API_VERSION_MAJOR = 2020; static const int MERGIN_API_VERSION_MINOR = 4; static const QString sMetadataFile; @@ -347,6 +344,8 @@ class MerginApi: public QObject */ void deleteProject( const QString &projectNamespace, const QString &projectName ); + LocalProject getLocalProject( const QString &projectFullName ); + // Production and Test functions (therefore not private) /** diff --git a/app/qml/main.qml b/app/qml/main.qml index 5ff8a474d..d2791040a 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -248,12 +248,10 @@ ApplicationWindow { // load default project if ( __appSettings.defaultProject ) { let path = __appSettings.defaultProject - let isValid = __localProjectsManager.projectIsValid( path ) - let id = __localProjectsManager.projectId( path ) - if ( isValid && __loader.load( path ) ) { + if ( __localProjectsManager.projectIsValid( path ) && __loader.load( path ) ) { projectPanel.activeProjectPath = path - projectPanel.activeProjectId = id + projectPanel.activeProjectId = __localProjectsManager.projectId( path ) __appSettings.activeProject = path } else { @@ -276,15 +274,6 @@ ApplicationWindow { if ( __appSettings.activeProject ) mainPanel.forceActiveFocus() - // DO NOT COMMIT!!! vvv - if ( !__androidUtils.isAndroid ) - { - window.width = 423 - window.height = 601 - window.x = 1100 - window.y = 266 - } - console.log("Completed Running!") } From 3b67d6967c03dddcb8cf6794a68e94bfcfdcdd83 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Wed, 7 Apr 2021 08:38:30 +0200 Subject: [PATCH 29/53] fix tests and code accordingly (#1299) --- app/main.cpp | 4 +- app/merginapi.cpp | 3 +- app/merginapi.h | 2 +- app/project.h | 4 +- app/projectsmodel.cpp | 4 +- app/projectsmodel.h | 9 +- app/test/testmerginapi.cpp | 351 ++++++++++++++++++++++++------------- app/test/testmerginapi.h | 21 ++- 8 files changed, 260 insertions(+), 138 deletions(-) diff --git a/app/main.cpp b/app/main.cpp index 6a9e8d3bd..53dce01d9 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -240,7 +240,7 @@ void initDeclarative() #ifdef INPUT_TEST void initTestDeclarative() { - qRegisterMetaType( "MerginProjectList" ); + qRegisterMetaType( "MerginProjectsList" ); } #endif @@ -421,7 +421,7 @@ int main( int argc, char *argv[] ) int nFailed = 0; if ( IS_MERGIN_API_TEST ) { - TestMerginApi merginApiTest( ma.get(), &mpm, &pm ); + TestMerginApi merginApiTest( ma.get() ); nFailed = QTest::qExec( &merginApiTest, args.count(), args.data() ); } else if ( IS_LINKS_TEST ) diff --git a/app/merginapi.cpp b/app/merginapi.cpp index 9468b8996..72feef615 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -2422,6 +2422,7 @@ void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSucc bool updateBeforeUpload = transaction.updateBeforeUpload; QString projectDir = transaction.projectDir; // keep it before the transaction gets removed ProjectDiff diff = transaction.diff; + int newVersion = syncSuccessful ? transaction.version : -1; mTransactionalStatus.remove( projectFullName ); if ( updateBeforeUpload ) @@ -2434,7 +2435,7 @@ void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSucc } else { - emit syncProjectFinished( projectDir, projectFullName, syncSuccessful ); + emit syncProjectFinished( projectDir, projectFullName, syncSuccessful, newVersion ); if ( syncSuccessful ) { diff --git a/app/merginapi.h b/app/merginapi.h index 338d22c0b..9d53d9e0a 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -384,7 +384,7 @@ class MerginApi: public QObject void listProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ); void listProjectsFailed(); void listProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ); - void syncProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); + void syncProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully, int version ); /** * Emitted when sync starts/finishes or the progress changes - useful to give a clue in the GUI about the status. * Normally progress is in interval [0, 1] as data get uploaded or downloaded. diff --git a/app/project.h b/app/project.h index f23d752b0..3a97ec0bc 100644 --- a/app/project.h +++ b/app/project.h @@ -85,6 +85,8 @@ struct MerginProject // Maybe better use enum or int for error code QString remoteError; // Error leading to project not being able to sync + bool isValid() const { return !projectName.isEmpty() && !projectNamespace.isEmpty(); } + bool operator ==( const MerginProject &other ) { return ( this->id() == other.id() ); @@ -149,5 +151,5 @@ struct Project typedef QList MerginProjectsList; typedef QList LocalProjectsList; - +Q_DECLARE_METATYPE( MerginProjectsList ) #endif // PROJECT_H diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index e65661915..72ff4ec75 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -416,7 +416,7 @@ void ProjectsModel::migrateProject( const QString &projectId ) mBackend->migrateProjectToMergin( project->local->projectName ); } -void ProjectsModel::onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully ) +void ProjectsModel::onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully, int newVersion ) { Q_UNUSED( projectDir ) @@ -426,6 +426,8 @@ void ProjectsModel::onProjectSyncFinished( const QString &projectDir, const QStr project->mergin->pending = false; project->mergin->progress = 0; + project->mergin->serverVersion = newVersion; + project->mergin->status = ProjectStatus::projectStatus( project ); QModelIndex ix = index( mProjects.indexOf( project ) ); emit dataChanged( ix, ix ); diff --git a/app/projectsmodel.h b/app/projectsmodel.h index 07814ac1c..3f6288f09 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -109,11 +109,15 @@ class ProjectsModel : public QAbstractListModel bool hasMoreProjects() const; + bool containsProject( QString projectId ) const; + + std::shared_ptr projectFromId( QString projectId ) const; + public slots: // MerginAPI - backend signals void onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ); void onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ); - void onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); + void onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully, int newVersion ); void onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ); void onProjectDetachedFromMergin( const QString &projectFullName ); void onProjectAttachedToMergin( const QString &projectFullName ); @@ -138,9 +142,6 @@ class ProjectsModel : public QAbstractListModel void loadLocalProjects(); void initializeProjectsModel(); - bool containsProject( QString projectId ) const; - std::shared_ptr projectFromId( QString projectId ) const; - MerginApi *mBackend = nullptr; LocalProjectsManager *mLocalProjectsManager = nullptr; QList> mProjects; diff --git a/app/test/testmerginapi.cpp b/app/test/testmerginapi.cpp index 539ebc258..bc944ace2 100644 --- a/app/test/testmerginapi.cpp +++ b/app/test/testmerginapi.cpp @@ -1,6 +1,5 @@ #include #include -#include #define STR1(x) #x #define STR(x) STR1(x) @@ -16,23 +15,31 @@ const QString TestMerginApi::TEST_PROJECT_NAME = "TEMPORARY_TEST_PROJECT"; const QString TestMerginApi::TEST_EMPTY_FILE_NAME = "test_empty_file.md"; -static MerginProjectListEntry _findProjectByName( const QString &projectNamespace, const QString &projectName, const MerginProjectList &projects ) +static MerginProject _findProjectByName( const QString &projectNamespace, const QString &projectName, const MerginProjectsList &projects ) { - for ( MerginProjectListEntry project : projects ) + for ( MerginProject project : projects ) { if ( project.projectName == projectName && project.projectNamespace == projectNamespace ) return project; } - return MerginProjectListEntry(); + return MerginProject(); } -TestMerginApi::TestMerginApi( MerginApi *api, MerginProjectModel *mpm, ProjectModel *pm ) +TestMerginApi::TestMerginApi( MerginApi *api ) { mApi = api; Q_ASSERT( mApi ); // does not make sense to run without API - mMerginProjectModel = mpm; - mProjectModel = pm; + + mLocalProjectsModel = std::unique_ptr( new ProjectsModel ); + mLocalProjectsModel->setModelType( ProjectsModel::LocalProjectsModel ); + mLocalProjectsModel->setMerginApi( mApi ); + mLocalProjectsModel->setLocalProjectsManager( &mApi->localProjectsManager() ); + + mCreatedProjectsModel = std::unique_ptr( new ProjectsModel ); + mCreatedProjectsModel->setModelType( ProjectsModel::CreatedProjectsModel ); + mCreatedProjectsModel->setMerginApi( mApi ); + mCreatedProjectsModel->setLocalProjectsManager( &mApi->localProjectsManager() ); } void TestMerginApi::initTestCase() @@ -119,8 +126,10 @@ void TestMerginApi::testListProject() mApi->listProjects( QString() ); QVERIFY( spy0.wait( TestUtils::SHORT_REPLY ) ); QCOMPARE( spy0.count(), 1 ); - QVERIFY( !_findProjectByName( mUsername, projectName, mApi->projects() ).isValid() ); - QVERIFY( !mApi->localProjectsManager().hasMerginProject( mUsername, projectName ) ); + MerginProjectsList projects = projectListFromSpy( spy0 ); + + QVERIFY( !_findProjectByName( mUsername, projectName, projects ).isValid() ); + QVERIFY( !mApi->localProjectsManager().projectFromMerginName( mUsername, projectName ).isValid() ); // create the project on the server (the content is not important) createRemoteProject( mApiExtra, mUsername, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); @@ -130,10 +139,12 @@ void TestMerginApi::testListProject() mApi->listProjects( QString() ); QVERIFY( spy.wait( TestUtils::SHORT_REPLY ) ); QCOMPARE( spy.count(), 1 ); - QVERIFY( _findProjectByName( mUsername, projectName, mApi->projects() ).isValid() ); + projects = projectListFromSpy( spy ); + + QVERIFY( _findProjectByName( mUsername, projectName, projects ).isValid() ); // project is not available locally, so it has no entry - QVERIFY( !mApi->localProjectsManager().hasMerginProject( mUsername, projectName ) ); + QVERIFY( !mApi->localProjectsManager().projectFromMerginName( mUsername, projectName ).isValid() ); } /** @@ -159,19 +170,23 @@ void TestMerginApi::testDownloadProject() QCOMPARE( mApi->transactions().count(), 0 ); // check that the local projects are updated - QVERIFY( mApi->localProjectsManager().hasMerginProject( mUsername, projectName ) ); - LocalProjectInfo project = mApi->localProjectsManager().projectFromFullName( projectNamespace, projectName ); - QVERIFY( project.isValid() ); - QCOMPARE( project.projectDir, mApi->projectsPath() + "/" + projectName ); - QCOMPARE( project.serverVersion, 1 ); - QCOMPARE( project.localVersion, 1 ); - QCOMPARE( project.status, UpToDate ); + QVERIFY( mApi->localProjectsManager().projectFromMerginName( mUsername, projectName ).isValid() ); - bool downloadSuccessful = mProjectModel->containsProject( projectNamespace, projectName ); + // update model to have latest info + refreshProjectsModel( ProjectsModel::LocalProjectsModel ); + + std::shared_ptr project = mLocalProjectsModel->projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + QVERIFY( project && project->isLocal() && project->isMergin() ); + QCOMPARE( project->local->projectDir, mApi->projectsPath() + "/" + projectName ); + QCOMPARE( project->local->localVersion, 1 ); + QCOMPARE( project->mergin->serverVersion, 1 ); + QCOMPARE( project->mergin->status, ProjectStatus::UpToDate ); + + bool downloadSuccessful = mApi->localProjectsManager().projectFromMerginName( projectNamespace, projectName ).isValid(); QVERIFY( downloadSuccessful ); // there should be something in the directory - QStringList projectDirEntries = QDir( project.projectDir ).entryList( QDir::AllEntries | QDir::NoDotAndDotDot ); + QStringList projectDirEntries = QDir( project->local->projectDir ).entryList( QDir::AllEntries | QDir::NoDotAndDotDot ); QCOMPARE( projectDirEntries.count(), 2 ); // verify that download in progress file is erased @@ -253,7 +268,7 @@ void TestMerginApi::createRemoteProject( MerginApi *api, const QString &projectN QCOMPARE( info.size(), 0 ); QVERIFY( dir.isEmpty() ); - api->localProjectsManager().removeLocalProject( projectDir ); + api->localProjectsManager().removeLocalProject( MerginApi::getFullProjectName( projectNamespace, projectName ) ); QCOMPARE( QFileInfo( projectDir ).size(), 0 ); QVERIFY( QDir( projectDir ).isEmpty() ); @@ -311,7 +326,7 @@ void TestMerginApi::testCreateProjectTwice() QString projectName = "testCreateProjectTwice"; QString projectNamespace = mUsername; - MerginProjectList projects = getProjectList(); + MerginProjectsList projects = getProjectList(); QVERIFY( !_findProjectByName( projectNamespace, projectName, projects ).isValid() ); QSignalSpy spy( mApi, &MerginApi::projectCreated ); @@ -321,7 +336,9 @@ void TestMerginApi::testCreateProjectTwice() QCOMPARE( spy.takeFirst().at( 1 ).toBool(), true ); projects = getProjectList(); - QVERIFY( !mMerginProjectModel->projects().isEmpty() ); + refreshProjectsModel( ProjectsModel::CreatedProjectsModel ); + + QVERIFY( mCreatedProjectsModel->rowCount() ); QVERIFY( _findProjectByName( projectNamespace, projectName, projects ).isValid() ); // Create again, expecting error @@ -351,7 +368,7 @@ void TestMerginApi::testDeleteNonExistingProject() // Checks if projects doesn't exist QString projectName = "testDeleteNonExistingProject"; QString projectNamespace = mUsername; - MerginProjectList projects = getProjectList(); + MerginProjectsList projects = getProjectList(); QVERIFY( !_findProjectByName( projectNamespace, projectName, projects ).isValid() ); // Try to delete non-existing project @@ -370,7 +387,7 @@ void TestMerginApi::testCreateDeleteProject() // Create a project QString projectName = "testCreateDeleteProject"; QString projectNamespace = mUsername; - MerginProjectList projects = getProjectList(); + MerginProjectsList projects = getProjectList(); QVERIFY( !_findProjectByName( projectNamespace, projectName, projects ).isValid() ); QSignalSpy spy( mApi, &MerginApi::projectCreated ); @@ -380,7 +397,9 @@ void TestMerginApi::testCreateDeleteProject() QCOMPARE( spy.takeFirst().at( 1 ).toBool(), true ); projects = getProjectList(); - QVERIFY( !mMerginProjectModel->projects().isEmpty() ); + refreshProjectsModel( ProjectsModel::CreatedProjectsModel ); + + QVERIFY( mCreatedProjectsModel->rowCount() ); Q_ASSERT( _findProjectByName( projectNamespace, projectName, projects ).isValid() ); // Delete created project @@ -405,7 +424,7 @@ void TestMerginApi::testUploadProject() QCOMPARE( spy0.count(), 1 ); QCOMPARE( spy0.takeFirst().at( 1 ).toBool(), true ); - MerginProjectList projects = getProjectList(); + MerginProjectsList projects = getProjectList(); QVERIFY( _findProjectByName( projectNamespace, projectName, projects ).isValid() ); // copy project's test data to the new project directory @@ -413,9 +432,9 @@ void TestMerginApi::testUploadProject() mApi->localProjectsManager().addMerginProject( projectDir, projectNamespace, projectName ); // project info does not have any version information yet - LocalProjectInfo project0 = mApi->getLocalProject( MerginApi::getFullProjectName( projectNamespace, projectName ) ); - QCOMPARE( project0.serverVersion, -1 ); - QCOMPARE( project0.localVersion, -1 ); + std::shared_ptr project0 = mLocalProjectsModel->projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + QVERIFY( project0 && project0->isLocal() && !project0->isMergin() ); + QCOMPARE( project0->local->localVersion, -1 ); // // try to upload, but cancel it immediately afterwards @@ -432,9 +451,9 @@ void TestMerginApi::testUploadProject() QVERIFY( !arguments.at( 2 ).toBool() ); // server version is still not available (cancelled before project info) - LocalProjectInfo project1 = mApi->getLocalProject( MerginApi::getFullProjectName( projectNamespace, projectName ) ); - QCOMPARE( project1.serverVersion, -1 ); - QCOMPARE( project1.localVersion, -1 ); + std::shared_ptr project1 = mLocalProjectsModel->projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + QVERIFY( project1 && project1->isLocal() && !project1->isMergin() ); + QCOMPARE( project1->local->localVersion, -1 ); // // try to upload, but cancel it after started to upload files @@ -457,10 +476,17 @@ void TestMerginApi::testUploadProject() QList argumentsX = spyX.takeFirst(); QVERIFY( !argumentsX.at( 2 ).toBool() ); - // server version is now available (cancelled after project info) - LocalProjectInfo project2 = mApi->getLocalProject( MerginApi::getFullProjectName( projectNamespace, projectName ) ); - QCOMPARE( project2.serverVersion, 0 ); - QCOMPARE( project2.localVersion, -1 ); + // server version is now available (cancelled after project info), but after projects model refresh + std::shared_ptr project2 = mLocalProjectsModel->projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + QVERIFY( project2 && project2->isLocal() && !project2->isMergin() ); + QCOMPARE( project2->local->localVersion, -1 ); + + refreshProjectsModel( ProjectsModel::LocalProjectsModel ); + + project2 = mLocalProjectsModel->projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + QVERIFY( project2 && project2->isLocal() && project2->isMergin() ); + QCOMPARE( project2->local->localVersion, -1 ); + QCOMPARE( project2->mergin->serverVersion, 0 ); // // try to upload - and let the upload finish successfully @@ -472,10 +498,11 @@ void TestMerginApi::testUploadProject() QVERIFY( spy2.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( spy2.count(), 1 ); - LocalProjectInfo project3 = mApi->getLocalProject( MerginApi::getFullProjectName( projectNamespace, projectName ) ); - QCOMPARE( project3.serverVersion, 1 ); - QCOMPARE( project3.localVersion, 1 ); - QCOMPARE( project3.status, UpToDate ); + std::shared_ptr project3 = mLocalProjectsModel->projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + QVERIFY( project3 && project3->isLocal() && project3->isMergin() ); + QCOMPARE( project3->local->localVersion, 1 ); + QCOMPARE( project3->mergin->serverVersion, 1 ); + QCOMPARE( project3->mergin->status, ProjectStatus::UpToDate ); } void TestMerginApi::testMultiChunkUploadDownload() @@ -553,13 +580,15 @@ void TestMerginApi::testPushAddedFile() QString projectName = "testPushAddedFile"; createRemoteProject( mApiExtra, mUsername, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + refreshProjectsModel( ProjectsModel::CreatedProjectsModel ); downloadRemoteProject( mApi, mUsername, projectName ); - LocalProjectInfo project0 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project0.serverVersion, 1 ); - QCOMPARE( project0.localVersion, 1 ); - QCOMPARE( project0.status, UpToDate ); + std::shared_ptr project0 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project0 && project0->isLocal() && project0->isMergin() ); + QCOMPARE( project0->local->localVersion, 1 ); + QCOMPARE( project0->mergin->serverVersion, 1 ); + QCOMPARE( project0->mergin->status, ProjectStatus::UpToDate ); // add a single file QString newFilePath = mApi->projectsPath() + "/" + projectName + "/added.txt"; @@ -569,28 +598,32 @@ void TestMerginApi::testPushAddedFile() file.close(); // check that the status is "modified" - mApi->localProjectsManager().updateProjectStatus( mApi->projectsPath() + "/" + projectName ); // force update of status - LocalProjectInfo project1 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project1.serverVersion, 1 ); - QCOMPARE( project1.localVersion, 1 ); - QCOMPARE( project1.status, Modified ); + refreshProjectsModel( ProjectsModel::CreatedProjectsModel ); // force update of status + + std::shared_ptr project1 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project1 && project1->isLocal() && project1->isMergin() ); + QCOMPARE( project1->local->localVersion, 1 ); + QCOMPARE( project1->mergin->serverVersion, 1 ); + QCOMPARE( project1->mergin->status, ProjectStatus::Modified ); // upload uploadRemoteProject( mApi, mUsername, projectName ); - LocalProjectInfo project2 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project2.serverVersion, 2 ); - QCOMPARE( project2.localVersion, 2 ); - QCOMPARE( project2.status, UpToDate ); + std::shared_ptr project2 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project2 && project2->isLocal() && project2->isMergin() ); + QCOMPARE( project2->local->localVersion, 2 ); + QCOMPARE( project2->mergin->serverVersion, 2 ); + QCOMPARE( project2->mergin->status, ProjectStatus::UpToDate ); deleteLocalProject( mApi, mUsername, projectName ); downloadRemoteProject( mApi, mUsername, projectName ); - LocalProjectInfo project3 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project3.serverVersion, 2 ); - QCOMPARE( project3.localVersion, 2 ); - QCOMPARE( project3.status, UpToDate ); + std::shared_ptr project3 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project3 && project3->isLocal() && project3->isMergin() ); + QCOMPARE( project3->local->localVersion, 2 ); + QCOMPARE( project3->mergin->serverVersion, 2 ); + QCOMPARE( project3->mergin->status, ProjectStatus::UpToDate ); // check it has the new file QFileInfo fi( newFilePath ); @@ -605,13 +638,15 @@ void TestMerginApi::testPushRemovedFile() QString projectName = "testPushRemovedFile"; createRemoteProject( mApiExtra, mUsername, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + refreshProjectsModel( ProjectsModel::CreatedProjectsModel ); downloadRemoteProject( mApi, mUsername, projectName ); - LocalProjectInfo project0 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project0.serverVersion, 1 ); - QCOMPARE( project0.localVersion, 1 ); - QCOMPARE( project0.status, UpToDate ); + std::shared_ptr project0 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project0 && project0->isLocal() && project0->isMergin() ); + QCOMPARE( project0->local->localVersion, 1 ); + QCOMPARE( project0->mergin->serverVersion, 1 ); + QCOMPARE( project0->mergin->status, ProjectStatus::UpToDate ); // Remove file QString removedFilePath = mApi->projectsPath() + "/" + projectName + "/test1.txt"; @@ -621,29 +656,33 @@ void TestMerginApi::testPushRemovedFile() QVERIFY( !file.exists() ); // check that it is considered as modified now - mApi->localProjectsManager().updateProjectStatus( mApi->projectsPath() + "/" + projectName ); // force update of status - LocalProjectInfo project1 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project1.serverVersion, 1 ); - QCOMPARE( project1.localVersion, 1 ); - QCOMPARE( project1.status, Modified ); + refreshProjectsModel( ProjectsModel::CreatedProjectsModel ); // force update of status + + std::shared_ptr project1 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project1 && project1->isLocal() && project1->isMergin() ); + QCOMPARE( project1->local->localVersion, 1 ); + QCOMPARE( project1->mergin->serverVersion, 1 ); + QCOMPARE( project1->mergin->status, ProjectStatus::Modified ); // upload changes uploadRemoteProject( mApi, mUsername, projectName ); - LocalProjectInfo project2 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project2.serverVersion, 2 ); - QCOMPARE( project2.localVersion, 2 ); - QCOMPARE( project2.status, UpToDate ); + std::shared_ptr project2 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project2 && project2->isLocal() && project2->isMergin() ); + QCOMPARE( project2->local->localVersion, 2 ); + QCOMPARE( project2->mergin->serverVersion, 2 ); + QCOMPARE( project2->mergin->status, ProjectStatus::UpToDate ); deleteLocalProject( mApi, mUsername, projectName ); downloadRemoteProject( mApi, mUsername, projectName ); - LocalProjectInfo project3 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project3.serverVersion, 2 ); - QCOMPARE( project3.localVersion, 2 ); - QCOMPARE( project3.status, UpToDate ); + std::shared_ptr project3 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project3 && project3->isLocal() && project3->isMergin() ); + QCOMPARE( project3->local->localVersion, 2 ); + QCOMPARE( project3->mergin->serverVersion, 2 ); + QCOMPARE( project3->mergin->status, ProjectStatus::UpToDate ); // check it has the new file QFileInfo fi( removedFilePath ); @@ -658,6 +697,7 @@ void TestMerginApi::testPushModifiedFile() QString projectName = "testPushModifiedFile"; createRemoteProject( mApiExtra, mUsername, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + refreshProjectsModel( ProjectsModel::CreatedProjectsModel ); downloadRemoteProject( mApi, mUsername, projectName ); @@ -673,19 +713,21 @@ void TestMerginApi::testPushModifiedFile() file.close(); // check that the status is "modified" - mApi->localProjectsManager().updateProjectStatus( mApi->projectsPath() + "/" + projectName ); // force update of status - LocalProjectInfo project1 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project1.serverVersion, 1 ); - QCOMPARE( project1.localVersion, 1 ); - QCOMPARE( project1.status, Modified ); + refreshProjectsModel( ProjectsModel::CreatedProjectsModel ); // force update of status + std::shared_ptr project1 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project1 && project1->isLocal() && project1->isMergin() ); + QCOMPARE( project1->local->localVersion, 1 ); + QCOMPARE( project1->mergin->serverVersion, 1 ); + QCOMPARE( project1->mergin->status, ProjectStatus::Modified ); // upload uploadRemoteProject( mApi, mUsername, projectName ); - LocalProjectInfo project2 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project2.serverVersion, 2 ); - QCOMPARE( project2.localVersion, 2 ); - QCOMPARE( project2.status, UpToDate ); + std::shared_ptr project2 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project2 && project2->isLocal() && project2->isMergin() ); + QCOMPARE( project2->local->localVersion, 2 ); + QCOMPARE( project2->mergin->serverVersion, 2 ); + QCOMPARE( project2->mergin->status, ProjectStatus::UpToDate ); // verify the remote project has updated file @@ -695,10 +737,11 @@ void TestMerginApi::testPushModifiedFile() downloadRemoteProject( mApi, mUsername, projectName ); - LocalProjectInfo project3 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project3.serverVersion, 2 ); - QCOMPARE( project3.localVersion, 2 ); - QCOMPARE( project3.status, UpToDate ); + std::shared_ptr project3 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project3 && project3->isLocal() && project3->isMergin() ); + QCOMPARE( project3->local->localVersion, 2 ); + QCOMPARE( project3->mergin->serverVersion, 2 ); + QCOMPARE( project3->mergin->status, ProjectStatus::UpToDate ); QVERIFY( file.open( QIODevice::ReadOnly ) ); QCOMPARE( file.readAll(), QByteArray( "v2" ) ); @@ -711,23 +754,26 @@ void TestMerginApi::testPushNoChanges() QString projectDir = mApi->projectsPath() + "/" + projectName; createRemoteProject( mApiExtra, mUsername, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + refreshProjectsModel( ProjectsModel::CreatedProjectsModel ); downloadRemoteProject( mApi, mUsername, projectName ); // check that the status is still "up-to-date" - mApi->localProjectsManager().updateProjectStatus( projectDir ); // force update of status - LocalProjectInfo project1 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project1.serverVersion, 1 ); - QCOMPARE( project1.localVersion, 1 ); - QCOMPARE( project1.status, UpToDate ); + std::shared_ptr project1 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project1 && project1->isLocal() && project1->isMergin() ); + QCOMPARE( project1->local->localVersion, 1 ); + QCOMPARE( project1->mergin->serverVersion, 1 ); + QCOMPARE( project1->mergin->status, ProjectStatus::UpToDate ); // upload - should do nothing uploadRemoteProject( mApi, mUsername, projectName ); - LocalProjectInfo project2 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project2.serverVersion, 1 ); - QCOMPARE( project2.localVersion, 1 ); - QCOMPARE( project2.status, UpToDate ); + + std::shared_ptr project2 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project2 && project2->isLocal() && project2->isMergin() ); + QCOMPARE( project2->local->localVersion, 1 ); + QCOMPARE( project2->mergin->serverVersion, 1 ); + QCOMPARE( project2->mergin->status, ProjectStatus::UpToDate ); QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); } @@ -742,15 +788,17 @@ void TestMerginApi::testUpdateAddedFile() QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; createRemoteProject( mApiExtra, mUsername, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + refreshProjectsModel( ProjectsModel::CreatedProjectsModel ); // download initial version downloadRemoteProject( mApi, mUsername, projectName ); QVERIFY( !QFile::exists( projectDir + "/test-remote-new.txt" ) ); - LocalProjectInfo project0 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project0.serverVersion, 1 ); - QCOMPARE( project0.localVersion, 1 ); - QCOMPARE( project0.status, UpToDate ); + std::shared_ptr project0 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project0 && project0->isLocal() && project0->isMergin() ); + QCOMPARE( project0->local->localVersion, 1 ); + QCOMPARE( project0->mergin->serverVersion, 1 ); + QCOMPARE( project0->mergin->status, ProjectStatus::UpToDate ); // remove a file on the server downloadRemoteProject( mApiExtra, mUsername, projectName ); @@ -759,20 +807,23 @@ void TestMerginApi::testUpdateAddedFile() QVERIFY( QFile::exists( extraProjectDir + "/test-remote-new.txt" ) ); // list projects - just so that we can figure out we are behind - getProjectList(); + refreshProjectsModel( ProjectsModel::CreatedProjectsModel ); - LocalProjectInfo project1 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project1.serverVersion, 2 ); - QCOMPARE( project1.localVersion, 1 ); - QCOMPARE( project1.status, OutOfDate ); + std::shared_ptr project1 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project1 && project1->isLocal() && project1->isMergin() ); + QCOMPARE( project1->local->localVersion, 1 ); + QCOMPARE( project1->mergin->serverVersion, 2 ); + QCOMPARE( project1->mergin->status, ProjectStatus::OutOfDate ); // now try to update downloadRemoteProject( mApi, mUsername, projectName ); - LocalProjectInfo project2 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project2.serverVersion, 2 ); - QCOMPARE( project2.localVersion, 2 ); - QCOMPARE( project2.status, UpToDate ); + std::shared_ptr project2 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project2 && project2->isLocal() && project2->isMergin() ); + QCOMPARE( project2->local->localVersion, 2 ); + QCOMPARE( project2->mergin->serverVersion, 2 ); + QCOMPARE( project2->mergin->status, ProjectStatus::UpToDate ); + // check that the added file is there QVERIFY( QFile::exists( projectDir + "/project.qgs" ) ); @@ -967,6 +1018,7 @@ void TestMerginApi::testUploadWithUpdate() QString extraFilenameRemote = extraProjectDir + "/test-new-remote-file.txt"; createRemoteProject( mApiExtra, mUsername, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + refreshProjectsModel( ProjectsModel::CreatedProjectsModel ); downloadRemoteProject( mApi, mUsername, projectName ); @@ -986,10 +1038,11 @@ void TestMerginApi::testUploadWithUpdate() deleteLocalProject( mApi, mUsername, projectName ); downloadRemoteProject( mApi, mUsername, projectName ); - LocalProjectInfo project3 = mApi->getLocalProject( MerginApi::getFullProjectName( mUsername, projectName ) ); - QCOMPARE( project3.serverVersion, 3 ); - QCOMPARE( project3.localVersion, 3 ); - QCOMPARE( project3.status, UpToDate ); + std::shared_ptr project1 = mCreatedProjectsModel->projectFromId( MerginApi::getFullProjectName( mUsername, projectName ) ); + QVERIFY( project1 && project1->isLocal() && project1->isMergin() ); + QCOMPARE( project1->local->localVersion, 3 ); + QCOMPARE( project1->mergin->serverVersion, 3 ); + QCOMPARE( project1->mergin->status, ProjectStatus::UpToDate ); QCOMPARE( readFileContent( filenameLocal ), QByteArray( "new local content" ) ); QCOMPARE( readFileContent( filenameRemote ), QByteArray( "new remote content" ) ); @@ -1491,15 +1544,38 @@ void TestMerginApi::testRegister() //////// HELPER FUNCTIONS //////// -MerginProjectList TestMerginApi::getProjectList() +MerginProjectsList TestMerginApi::getProjectList( QString tag ) { QSignalSpy spy( mApi, &MerginApi::listProjectsFinished ); - mApi->listProjects( QString(), "created", QString() ); + mApi->listProjects( QString(), tag, QString() ); spy.wait( TestUtils::SHORT_REPLY ); - return mApi->projects(); + return projectListFromSpy( spy ); +} + +MerginProjectsList TestMerginApi::projectListFromSpy( QSignalSpy &spy ) +{ + QList response = spy.takeFirst(); + + // get projects emited from MerginAPI, it is first argument in listProjectsFinished signal + MerginProjectsList projects; + if ( response.length() > 0 ) + projects = qvariant_cast( response.at( 0 ) ); + + return projects; } +int TestMerginApi::serverVersionFromSpy( QSignalSpy &spy ) +{ + QList response = spy.takeFirst(); + + // get version number emited from MerginApi::syncProjectFinished, it is third argument + int serverVersion = -1; + if ( response.length() >= 4 ) + serverVersion = response.at( 3 ).toInt(); + + return serverVersion; +} void TestMerginApi::deleteRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ) { @@ -1510,29 +1586,43 @@ void TestMerginApi::deleteRemoteProject( MerginApi *api, const QString &projectN void TestMerginApi::deleteLocalProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ) { - LocalProjectInfo project = api->getLocalProject( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + LocalProject project = api->getLocalProject( MerginApi::getFullProjectName( projectNamespace, projectName ) ); QVERIFY( project.isValid() ); QVERIFY( project.projectDir.startsWith( api->projectsPath() ) ); // just to make sure we don't delete something wrong (-: - QDir projectDir( project.projectDir ); - projectDir.removeRecursively(); +// QDir projectDir( project.projectDir ); +// projectDir.removeRecursively(); - api->localProjectsManager().removeLocalProject( project.projectDir ); + api->localProjectsManager().removeLocalProject( project.id() ); } void TestMerginApi::downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ) +{ + int serverVersion; + downloadRemoteProject( api, projectNamespace, projectName, serverVersion ); +} + +void TestMerginApi::downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, int &serverVersion ) { QSignalSpy spy( api, &MerginApi::syncProjectFinished ); api->updateProject( projectNamespace, projectName ); QCOMPARE( api->transactions().count(), 1 ); QVERIFY( spy.wait( TestUtils::LONG_REPLY * 5 ) ); + serverVersion = serverVersionFromSpy( spy ); } void TestMerginApi::uploadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ) +{ + int serverVersion; + uploadRemoteProject( api, projectNamespace, projectName, serverVersion ); +} + +void TestMerginApi::uploadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, int &serverVersion ) { api->uploadProject( projectNamespace, projectName ); QSignalSpy spy( api, &MerginApi::syncProjectFinished ); QVERIFY( spy.wait( TestUtils::LONG_REPLY * 30 ) ); QCOMPARE( spy.count(), 1 ); + serverVersion = serverVersionFromSpy( spy ); } void TestMerginApi::writeFileContent( const QString &filename, const QByteArray &data ) @@ -1560,3 +1650,22 @@ void TestMerginApi::createLocalProject( const QString projectDir ) QVERIFY( r0 ); } + +void TestMerginApi::refreshProjectsModel( const ProjectsModel::ProjectModelTypes modelType ) +{ + + if ( modelType == ProjectsModel::LocalProjectsModel ) + { + QSignalSpy spy( mApi, &MerginApi::listProjectsByNameFinished ); + mLocalProjectsModel->listProjects(); + QVERIFY( spy.wait( TestUtils::SHORT_REPLY ) ); + QCOMPARE( spy.count(), 1 ); + } + else if ( modelType == ProjectsModel::CreatedProjectsModel ) + { + QSignalSpy spy( mApi, &MerginApi::listProjectsFinished ); + mCreatedProjectsModel->listProjects(); + QVERIFY( spy.wait( TestUtils::SHORT_REPLY ) ); + QCOMPARE( spy.count(), 1 ); + } +} diff --git a/app/test/testmerginapi.h b/app/test/testmerginapi.h index fadef21e6..6dbc82043 100644 --- a/app/test/testmerginapi.h +++ b/app/test/testmerginapi.h @@ -11,11 +11,12 @@ #define TESTMERGINAPI_H #include +#include #include #include #include -#include +#include "project.h" #include @@ -23,7 +24,7 @@ class TestMerginApi: public QObject { Q_OBJECT public: - explicit TestMerginApi( MerginApi *api, MerginProjectModel *mpm, ProjectModel *pm ); + explicit TestMerginApi( MerginApi *api ); ~TestMerginApi() = default; static const QString TEST_PROJECT_NAME; @@ -70,25 +71,29 @@ class TestMerginApi: public QObject private: MerginApi *mApi; - MerginProjectModel *mMerginProjectModel; - ProjectModel *mProjectModel; + std::unique_ptr mLocalProjectsModel; + std::unique_ptr mCreatedProjectsModel; QString mUsername; QString mTestDataPath; //! extra API to do requests we are not testing (as if some other user did those) MerginApi *mApiExtra = nullptr; LocalProjectsManager *mLocalProjectsExtra = nullptr; - MerginProjectList getProjectList(); + MerginProjectsList getProjectList( QString tag = "created" ); + MerginProjectsList projectListFromSpy( QSignalSpy &spy ); + int serverVersionFromSpy( QSignalSpy &spy ); //! Creates a project on the server and pushes an initial version and removes the local copy. void createRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, const QString &sourcePath ); //! Deletes a project on the server void deleteRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ); - //! Downloads a remote project to the local drive + //! Downloads a remote project to the local drive, extended version also sets server version + void downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, int &serverVersion ); void downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ); - //! Uploads any local changes in the local project to the remote project + //! Uploads any local changes in the local project to the remote project, extended version also sets server version + void uploadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, int &serverVersion ); void uploadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ); //! Deletes a project from the local drive @@ -101,6 +106,8 @@ class TestMerginApi: public QObject //! Creates local project in given project directory void createLocalProject( const QString projectDir ); + + void refreshProjectsModel( const ProjectsModel::ProjectModelTypes modelType = ProjectsModel::LocalProjectsModel ); }; # endif // TESTMERGINAPI_H From 3567b68a7a3f7dddd38d190e6045ac18ef21ab9c Mon Sep 17 00:00:00 2001 From: Peter Petrik Date: Fri, 9 Apr 2021 09:27:48 +0200 Subject: [PATCH 30/53] [Feature] Separate MerginAPI to core folder (#1295) --- .github/workflows/android.yml | 2 +- .github/workflows/autotests.yml | 2 +- .github/workflows/ios.yml | 2 +- .gitignore | 2 +- app/input.pro | 4 + app/inputhelp.cpp | 13 +- app/inputprojutils.cpp | 5 +- app/inpututils.cpp | 180 +------------- app/inpututils.h | 49 +--- app/ios/iospurchasing.mm | 3 +- app/loader.cpp | 5 +- app/main.cpp | 15 +- app/projectwizard.cpp | 3 +- app/purchasing.cpp | 15 +- app/sources.pri | 32 +-- app/test/testmerginapi.cpp | 5 +- core/core.pri | 36 +++ core/coreutils.cpp | 187 ++++++++++++++ core/coreutils.h | 75 ++++++ {app => core}/geodiffutils.cpp | 32 +-- {app => core}/geodiffutils.h | 9 +- {app => core}/localprojectsmanager.cpp | 6 +- {app => core}/localprojectsmanager.h | 0 {app => core}/merginapi.cpp | 270 +++++++++++---------- {app => core}/merginapi.h | 17 +- {app => core}/merginapistatus.cpp | 0 {app => core}/merginapistatus.h | 0 {app => core}/merginprojectmetadata.cpp | 0 {app => core}/merginprojectmetadata.h | 0 {app => core}/merginprojectstatusmodel.cpp | 4 +- {app => core}/merginprojectstatusmodel.h | 0 {app => core}/merginsecrets.cpp.enc | Bin {app => core}/merginsubscriptionstatus.cpp | 0 {app => core}/merginsubscriptionstatus.h | 0 {app => core}/merginsubscriptiontype.cpp | 0 {app => core}/merginsubscriptiontype.h | 0 {app => core}/merginuserauth.cpp | 0 {app => core}/merginuserauth.h | 0 {app => core}/merginuserinfo.cpp | 6 +- {app => core}/merginuserinfo.h | 0 {app => core}/project.cpp | 6 +- {app => core}/project.h | 0 docs/developers/index.md | 3 + scripts/check_all.bash | 2 +- 44 files changed, 537 insertions(+), 453 deletions(-) create mode 100644 core/core.pri create mode 100644 core/coreutils.cpp create mode 100644 core/coreutils.h rename {app => core}/geodiffutils.cpp (79%) rename {app => core}/geodiffutils.h (89%) rename {app => core}/localprojectsmanager.cpp (97%) rename {app => core}/localprojectsmanager.h (100%) rename {app => core}/merginapi.cpp (85%) rename {app => core}/merginapi.h (99%) rename {app => core}/merginapistatus.cpp (100%) rename {app => core}/merginapistatus.h (100%) rename {app => core}/merginprojectmetadata.cpp (100%) rename {app => core}/merginprojectmetadata.h (100%) rename {app => core}/merginprojectstatusmodel.cpp (96%) rename {app => core}/merginprojectstatusmodel.h (100%) rename {app => core}/merginsecrets.cpp.enc (100%) rename {app => core}/merginsubscriptionstatus.cpp (100%) rename {app => core}/merginsubscriptionstatus.h (100%) rename {app => core}/merginsubscriptiontype.cpp (100%) rename {app => core}/merginsubscriptiontype.h (100%) rename {app => core}/merginuserauth.cpp (100%) rename {app => core}/merginuserauth.h (100%) rename {app => core}/merginuserinfo.cpp (96%) rename {app => core}/merginuserinfo.h (100%) rename {app => core}/project.cpp (91%) rename {app => core}/project.h (100%) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 8ec46ae45..4c5f81a9c 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -24,7 +24,7 @@ jobs: env: MERGINSECRETS_DECRYPT_KEY: ${{ secrets.MERGINSECRETS_DECRYPT_KEY }} run: | - openssl aes-256-cbc -k $MERGINSECRETS_DECRYPT_KEY -in app/merginsecrets.cpp.enc -out app/merginsecrets.cpp -d -md md5 + openssl aes-256-cbc -k $MERGINSECRETS_DECRYPT_KEY -in core/merginsecrets.cpp.enc -out core/merginsecrets.cpp -d -md md5 - name: Extract GPS keystore diff --git a/.github/workflows/autotests.yml b/.github/workflows/autotests.yml index ea8e63bfb..9fc67a186 100644 --- a/.github/workflows/autotests.yml +++ b/.github/workflows/autotests.yml @@ -27,7 +27,7 @@ jobs: env: MERGINSECRETS_DECRYPT_KEY: ${{ secrets.MERGINSECRETS_DECRYPT_KEY }} run: | - cd input/app/ + cd input/core/ /usr/local/opt/openssl@1.1/bin/openssl \ aes-256-cbc -d \ -in merginsecrets.cpp.enc \ diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index b9ffd4e84..ae250c132 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -30,7 +30,7 @@ jobs: env: MERGINSECRETS_DECRYPT_KEY: ${{ secrets.MERGINSECRETS_DECRYPT_KEY }} run: | - cd app/ + cd core/ /usr/local/opt/openssl@1.1/bin/openssl \ aes-256-cbc -d \ -in merginsecrets.cpp.enc \ diff --git a/.gitignore b/.gitignore index e7cf903ed..f49bcd55b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ *.user *build*/ app/config.pri -app/merginsecrets.cpp +core/merginsecrets.cpp *.idea test/temp_projects/ test/temp_extra_projects/ diff --git a/app/input.pro b/app/input.pro index 4f1f8d371..4cfdf34fb 100644 --- a/app/input.pro +++ b/app/input.pro @@ -15,7 +15,11 @@ include(linux.pri) include(macx.pri) include(win32.pri) include(sources.pri) +include($$PWD/../core/core.pri) +INCLUDEPATH += $$PWD/../core + +DEFINES += INPUT_APP DEFINES += "QGIS_QUICK_DATA_PATH=$${QGIS_QUICK_DATA_PATH}" CONFIG(debug, debug|release) { DEFINES += "QGIS_PREFIX_PATH=$${QGIS_PREFIX_PATH}" diff --git a/app/inputhelp.cpp b/app/inputhelp.cpp index 915ce05b5..782fd4458 100644 --- a/app/inputhelp.cpp +++ b/app/inputhelp.cpp @@ -13,6 +13,7 @@ #include "merginsubscriptionstatus.h" #include "merginapi.h" #include "inpututils.h" +#include "coreutils.h" #include "qgsquickutils.h" @@ -91,7 +92,7 @@ QString InputHelp::fullLog( bool isHtml ) qint64 limit = 500000; QVector retLines = logHeader( isHtml ); - QFile file( InputUtils::logFilename() ); + QFile file( CoreUtils::logFilename() ); if ( file.open( QIODevice::ReadOnly ) ) { qint64 fileSize = file.size(); @@ -109,7 +110,7 @@ QString InputHelp::fullLog( bool isHtml ) } else { - retLines.push_back( QString( "Unable to open log file %1" ).arg( InputUtils::logFilename() ) ); + retLines.push_back( QString( "Unable to open log file %1" ).arg( CoreUtils::logFilename() ) ); } QString ret; @@ -129,7 +130,7 @@ QString InputHelp::fullLog( bool isHtml ) QVector InputHelp::logHeader( bool isHtml ) { QVector retLines; - retLines.push_back( QStringLiteral( "Input App: %1 - %2" ).arg( InputUtils::appVersion() ).arg( InputUtils::appPlatform() ) ); + retLines.push_back( QStringLiteral( "Input App: %1 - %2" ).arg( CoreUtils::appVersion() ).arg( InputUtils::appPlatform() ) ); retLines.push_back( QStringLiteral( "System: %1" ).arg( QSysInfo::prettyProductName() ) ); retLines.push_back( QStringLiteral( "Mergin URL: %1" ).arg( mMerginApi->apiRoot() ) ); retLines.push_back( QStringLiteral( "Mergin User: %1" ).arg( mMerginApi->userAuth()->username() ) ); @@ -157,7 +158,7 @@ void InputHelp::submitReport() // There is a limit of 1MB on the remote service, send less, let say half of that QString log = fullLog( false ); QByteArray logArr = log.toUtf8(); - QString app = QStringLiteral( "input-%1-%2" ).arg( InputUtils::appPlatform() ).arg( InputUtils::appVersion() ); + QString app = QStringLiteral( "input-%1-%2" ).arg( InputUtils::appPlatform() ).arg( CoreUtils::appVersion() ); QString username = mMerginApi->userAuth()->username().toHtmlEscaped(); if ( username.isEmpty() ) username = "unknown"; @@ -183,12 +184,12 @@ void InputHelp::onSubmitReportReplyFinished() if ( r->error() == QNetworkReply::NoError ) { - InputUtils::log( "submit report", "Report submitted!" ); + CoreUtils::log( "submit report", "Report submitted!" ); emit mInputUtils->showNotification( tr( "Report submitted.%1Please contact us on%1%2" ).arg( "
" ).arg( helpDeskMail ) ); } else { - InputUtils::log( "submit report", QStringLiteral( "FAILED - %1" ).arg( r->errorString() ) ); + CoreUtils::log( "submit report", QStringLiteral( "FAILED - %1" ).arg( r->errorString() ) ); emit mInputUtils->showNotification( tr( "Failed to submit report.%1Please check your internet connection." ).arg( "
" ) ); } } diff --git a/app/inputprojutils.cpp b/app/inputprojutils.cpp index 5d0558e8c..28aa96168 100644 --- a/app/inputprojutils.cpp +++ b/app/inputprojutils.cpp @@ -14,6 +14,7 @@ #include #include "inpututils.h" +#include "coreutils.h" #include "proj.h" #include "qgsprojutils.h" #include "inputhelp.h" @@ -37,7 +38,7 @@ void InputProjUtils::logUser( const QString &message, bool &variable ) { if ( !variable ) { - InputUtils::log( "InputPROJ", message ); + CoreUtils::log( "InputPROJ", message ); variable = true; } } @@ -151,7 +152,7 @@ void InputProjUtils::setProjDir( const QString &appBundleDir ) QFile projdb( projFilePath ); if ( !projdb.exists() ) { - InputUtils::log( QStringLiteral( "PROJ6 error" ), QStringLiteral( "The Input has failed to load PROJ6 database." ) + projFilePath ); + CoreUtils::log( QStringLiteral( "PROJ6 error" ), QStringLiteral( "The Input has failed to load PROJ6 database." ) + projFilePath ); } } diff --git a/app/inpututils.cpp b/app/inpututils.cpp index 00759d7c2..b8452f1eb 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -17,8 +17,8 @@ #include "qgsquickutils.h" #include "qgsquickmaptransform.h" -#include "inpututils.h" #include "inputexpressionfunctions.h" +#include "coreutils.h" #include #include @@ -29,7 +29,6 @@ #include #include -QString InputUtils::sLogFile = QStringLiteral(); static const QString DATE_TIME_FORMAT = QStringLiteral( "yyMMdd-hhmmss" ); InputUtils::InputUtils( QObject *parent ): QObject( parent ) @@ -306,26 +305,6 @@ QVector InputUtils::extractGeometryCoordinates( const QgsQuickFeatureLay return data; } -void InputUtils::setLogFilename( const QString &value ) -{ - sLogFile = value; -} - -QString InputUtils::logFilename() -{ - return sLogFile; -} - -bool InputUtils::createEmptyFile( const QString &filePath ) -{ - QFile newFile( filePath ); - if ( !newFile.open( QIODevice::WriteOnly ) ) - return false; - - newFile.close(); - return true; -} - QString InputUtils::filesToString( QList files ) { QStringList resultList; @@ -336,12 +315,6 @@ QString InputUtils::filesToString( QList files ) return resultList.join( ", " ); } -QString InputUtils::appInfo() -{ - return QString( "%1/%2 (%3/%4)" ).arg( QCoreApplication::applicationName() ).arg( QCoreApplication::applicationVersion() ) - .arg( QSysInfo::productType() ).arg( QSysInfo::productVersion() ); -} - QString InputUtils::bytesToHumanSize( double bytes ) { const int precision = 1; @@ -399,43 +372,6 @@ void InputUtils::quitApp() QCoreApplication::quit(); } -QString InputUtils::uuidWithoutBraces( const QUuid &uuid ) -{ -#if QT_VERSION >= QT_VERSION_CHECK( 5, 11, 0 ) - return uuid.toString( QUuid::WithoutBraces ); -#else - QString str = uuid.toString(); - str = str.mid( 1, str.length() - 2 ); // remove braces - return str; -#endif -} - -QString InputUtils::localizedDateFromUTFString( QString timestamp ) -{ - if ( timestamp.isEmpty() ) - return QString(); - - QDateTime dateTime = QDateTime::fromString( timestamp, Qt::ISODate ); - if ( dateTime.isValid() ) - { - return dateTime.date().toString( Qt::DefaultLocaleShortDate ); - } - else - { - qDebug() << "Unable to convert UTF " << timestamp << " to QDateTime"; - return QString(); - } -} - -QString InputUtils::appVersion() -{ - QString version; -#ifdef INPUT_VERSION - version = STR( INPUT_VERSION ); -#endif - return version; -} - QString InputUtils::appPlatform() { #if defined( ANDROID ) @@ -454,48 +390,6 @@ QString InputUtils::appPlatform() return platform; } - -QString InputUtils::findUniquePath( const QString &path, bool isPathDir ) -{ - QFileInfo pathInfo( path ); - if ( pathInfo.exists() ) - { - int i = 0; - QFileInfo info( path + QString::number( i ) ); - while ( info.exists() && ( info.isDir() || !isPathDir ) ) - { - ++i; - info.setFile( path + QString::number( i ) ); - } - return path + QString::number( i ); - } - else - { - return path; - } -} - - -QString InputUtils::createUniqueProjectDirectory( const QString &baseDataDir, const QString &projectName ) -{ - QString projectDirPath = findUniquePath( baseDataDir + "/" + projectName ); - QDir projectDir( projectDirPath ); - if ( !projectDir.exists() ) - { - QDir dir( "" ); - dir.mkdir( projectDirPath ); - } - return projectDirPath; -} - -bool InputUtils::removeDir( const QString &dir ) -{ - if ( dir.isEmpty() || dir == "/" ) - return false; - - return QDir( dir ).removeRecursively(); -} - void InputUtils::onQgsLogMessageReceived( const QString &message, const QString &tag, Qgis::MessageLevel level ) { QString levelStr; @@ -511,7 +405,7 @@ void InputUtils::onQgsLogMessageReceived( const QString &message, const QString break; } - log( "QGIS " + tag, levelStr + ": " + message ); + CoreUtils::log( "QGIS " + tag, levelStr + ": " + message ); } bool InputUtils::cpDir( const QString &srcPath, const QString &dstPath, bool onlyDiffable ) @@ -520,7 +414,7 @@ bool InputUtils::cpDir( const QString &srcPath, const QString &dstPath, bool onl QDir parentDstDir( QFileInfo( dstPath ).path() ); if ( !parentDstDir.mkpath( dstPath ) ) { - log( "cpDir", QString( "Cannot make path %1" ).arg( dstPath ) ); + CoreUtils::log( "cpDir", QString( "Cannot make path %1" ).arg( dstPath ) ); return false; } @@ -534,7 +428,7 @@ bool InputUtils::cpDir( const QString &srcPath, const QString &dstPath, bool onl { if ( !cpDir( srcItemPath, dstItemPath ) ) { - log( "cpDir", QString( "Cannot copy a dir from %1 to %2" ).arg( srcItemPath ).arg( dstItemPath ) ); + CoreUtils::log( "cpDir", QString( "Cannot copy a dir from %1 to %2" ).arg( srcItemPath ).arg( dstItemPath ) ); result = false; } } @@ -547,12 +441,12 @@ bool InputUtils::cpDir( const QString &srcPath, const QString &dstPath, bool onl { if ( !QFile::remove( dstItemPath ) ) { - log( "cpDir", QString( "Cannot remove a file from %1" ).arg( dstItemPath ) ); + CoreUtils::log( "cpDir", QString( "Cannot remove a file from %1" ).arg( dstItemPath ) ); result = false; } if ( !QFile::copy( srcItemPath, dstItemPath ) ) { - log( "cpDir", QString( "Cannot overwrite a file %1 with %2" ).arg( dstItemPath ).arg( dstItemPath ) ); + CoreUtils::log( "cpDir", QString( "Cannot overwrite a file %1 with %2" ).arg( dstItemPath ).arg( dstItemPath ) ); result = false; } } @@ -560,7 +454,7 @@ bool InputUtils::cpDir( const QString &srcPath, const QString &dstPath, bool onl } else { - log( "cpDir", QString( "Unhandled item %1 in cpDir" ).arg( info.filePath() ) ); + CoreUtils::log( "cpDir", QString( "Unhandled item %1 in cpDir" ).arg( info.filePath() ) ); } } return result; @@ -581,44 +475,6 @@ QString InputUtils::renameWithDateTime( const QString &srcPath, const QDateTime return QString(); } -QDateTime InputUtils::getLastModifiedFileDateTime( const QString &path ) -{ - QDateTime lastModified; - QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); - while ( it.hasNext() ) - { - it.next(); - if ( !MerginApi::isInIgnore( it.fileInfo() ) ) - { - if ( it.fileInfo().lastModified() > lastModified ) - { - lastModified = it.fileInfo().lastModified(); - } - } - } - return lastModified.toUTC(); -} - -int InputUtils::getProjectFilesCount( const QString &path ) -{ - int count = 0; - QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); - while ( it.hasNext() ) - { - it.next(); - if ( !MerginApi::isInIgnore( it.fileInfo() ) ) - { - count++; - } - } - return count; -} - -QString InputUtils::downloadInProgressFilePath( const QString &projectDir ) -{ - return projectDir + "/.mergin/.project.downloading"; -} - void InputUtils::showNotification( const QString &message ) { emit showNotificationRequested( message ); @@ -635,28 +491,6 @@ qreal InputUtils::groundSpeedFromSource( QgsQuickPositionKit *positionKit ) return 0; } -void InputUtils::log( const QString &topic, const QString &info ) -{ - QString logFilePath; - QByteArray data; - data.append( QString( "%1 %2: %3\n" ).arg( QDateTime().currentDateTimeUtc().toString( Qt::ISODateWithMs ) ).arg( topic ).arg( info ) ); - - qDebug() << data; - appendLog( data, sLogFile ); -} - -void InputUtils::appendLog( const QByteArray &data, const QString &path ) -{ - QFile file( path ); - if ( !file.open( QIODevice::Append ) ) - { - return; - } - - file.write( data ); - file.close(); -} - double InputUtils::ratherZeroThanNaN( double d ) { return ( isnan( d ) ) ? 0.0 : d; diff --git a/app/inpututils.h b/app/inpututils.h index 42895011d..04eb1e912 100644 --- a/app/inpututils.h +++ b/app/inpututils.h @@ -73,12 +73,6 @@ class InputUtils: public QObject */ Q_INVOKABLE static QString renameWithDateTime( const QString &srcPath, const QDateTime &dateTime = QDateTime() ); - /** - * Returns name of temporary file indicating first time download of project is in progress - * \param projectName - */ - static QString downloadInProgressFilePath( const QString &projectDir ); - /** * Shows notification */ @@ -113,46 +107,11 @@ class InputUtils: public QObject */ static bool cpDir( const QString &srcPath, const QString &dstPath, bool onlyDiffable = false ); - /** - * Add a log entry to internal log text file - * - * \see setLogFilename() - */ - static void log( const QString &topic, const QString &info ); - - /** - * Sets the filename of the internal text log file - */ - static void setLogFilename( const QString &value ); - - static QString logFilename(); - - static bool createEmptyFile( const QString &filePath ); - static QString filesToString( QList files ); - static QString appInfo(); - - static QString uuidWithoutBraces( const QUuid &uuid ); - - static QString localizedDateFromUTFString( QString timestamp ); - - /** InputApp version */ - static QString appVersion(); - /** InputApp platform */ static QString appPlatform(); - /** - * Returns given path if doesn't exists, otherwise the slightly modified non-existing path by adding a number to given path. - * \param QString path - * \param QString isPathDir True if the result path suppose to be a folder - */ - static QString findUniquePath( const QString &path, bool isPathDir = true ); - - //! Creates a unique project directory for given project name (used for initial download of a project) - static QString createUniqueProjectDirectory( const QString &baseDataDir, const QString &projectName ); - /** * Converts string in rational number format to double. * @param rationalValue String - expecting value in format "numerator/denominator" (e.g "123/100"). @@ -169,11 +128,6 @@ class InputUtils: public QObject //! Creates and registers custom expression functions to Input, so they can be used in default value definitions. static void registerInputExpressionFunctions(); - static bool removeDir( const QString &projectDir ); - - static QDateTime getLastModifiedFileDateTime( const QString &path ); - - static int getProjectFilesCount( const QString &path ); signals: Q_INVOKABLE void showNotificationRequested( const QString &message ); @@ -187,8 +141,7 @@ class InputUtils: public QObject // file:assets-library://asset/asset.PNG%3Fid=A53AB989-6354-433A-9CB9-958179B7C14D&ext=PNG // we need to change it to something more readable QString sanitizeName( const QString &path ); - static QString sLogFile; - static void appendLog( const QByteArray &data, const QString &path ); + static double ratherZeroThanNaN( double d ); std::unique_ptr mAndroidUtils; }; diff --git a/app/ios/iospurchasing.mm b/app/ios/iospurchasing.mm index d451d86fa..cf84f67ff 100644 --- a/app/ios/iospurchasing.mm +++ b/app/ios/iospurchasing.mm @@ -8,6 +8,7 @@ ***************************************************************************/ #include #include "inpututils.h" +#include "coreutils.h" #import "iospurchasing.h" #import @@ -303,7 +304,7 @@ - ( void )paymentQueue:( SKPaymentQueue * )queue updatedTransactions:( NSArraystatus() == IosPurchasingTransaction::PurchaseFailed ) { - InputUtils::log( "transaction creation", QStringLiteral( "Failed: " ) + transaction->errMsg() ); + CoreUtils::log( "transaction creation", QStringLiteral( "Failed: " ) + transaction->errMsg() ); emit transactionCreationFailed(); transaction->finalizeTransaction(); } diff --git a/app/loader.cpp b/app/loader.cpp index 9f48972fb..a1f7553b7 100644 --- a/app/loader.cpp +++ b/app/loader.cpp @@ -15,6 +15,7 @@ #include "loader.h" #include "inpututils.h" +#include "coreutils.h" #include "qgsvectorlayer.h" #include "qgslayertree.h" #include "qgslayertreelayer.h" @@ -316,7 +317,7 @@ void Loader::appStateChanged( Qt::ApplicationState state ) QDebug logHelper( &msg ); logHelper << "Application changed state to: " << state; - InputUtils::log( "Input", msg ); + CoreUtils::log( "Input", msg ); if ( !mRecording && mPositionKit ) { @@ -333,7 +334,7 @@ void Loader::appStateChanged( Qt::ApplicationState state ) void Loader::appAboutToQuit() { - InputUtils::log( "Input", "Application has quit" ); + CoreUtils::log( "Input", "Application has quit" ); } QList Loader::globalProjectLayerScopes( QgsMapLayer *layer ) diff --git a/app/main.cpp b/app/main.cpp index 53dce01d9..265f67be6 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -31,6 +31,7 @@ #include "androidutils.h" #include "ios/iosutils.h" #include "inpututils.h" +#include "coreutils.h" #include "positiondirection.h" #include "mapthemesmodel.h" #include "digitizingcontroller.h" @@ -189,7 +190,7 @@ static void copy_demo_projects( const QString &demoDir, const QString &projectDi if ( demoFile.exists() ) qDebug() << "DEMO projects initialized"; else - InputUtils::log( QStringLiteral( "DEMO" ), QStringLiteral( "The Input has failed to initialize demo projects" ) ); + CoreUtils::log( QStringLiteral( "DEMO" ), QStringLiteral( "The Input has failed to initialize demo projects" ) ); } static void init_qgis( const QString &pkgPath ) @@ -271,7 +272,7 @@ int main( int argc, char *argv[] ) { QgsApplication app( argc, argv, true ); - const QString version = InputUtils::appVersion(); + const QString version = CoreUtils::appVersion(); // Set up the QSettings environment must be done after qapp is created QCoreApplication::setOrganizationName( "Lutra Consulting" ); @@ -329,7 +330,7 @@ int main( int argc, char *argv[] ) } #endif - InputUtils::setLogFilename( projectDir + "/.logs" ); + CoreUtils::setLogFilename( projectDir + "/.logs" ); setEnvironmentQgisPrefixPath(); QString appBundleDir; @@ -402,7 +403,7 @@ int main( int argc, char *argv[] ) // Cleaning default project due to a project loading has crashed during the last run. as.setDefaultProject( QString() ); projectLoadingFile.remove(); - InputUtils::log( QStringLiteral( "Loading project error" ), QStringLiteral( "The Input has been unexpectedly finished during the last run." ) ); + CoreUtils::log( QStringLiteral( "Loading project error" ), QStringLiteral( "The Input has been unexpectedly finished during the last run." ) ); } #ifdef INPUT_TEST @@ -560,15 +561,15 @@ int main( int argc, char *argv[] ) } catch ( QgsException &e ) { - iu.log( "Error", QStringLiteral( "Caught unhandled QgsException %1" ).arg( e.what() ) ); + CoreUtils::log( "Error", QStringLiteral( "Caught unhandled QgsException %1" ).arg( e.what() ) ); } catch ( std::exception &e ) { - iu.log( "Error", QStringLiteral( "Caught unhandled std::exception %1" ).arg( e.what() ) ); + CoreUtils::log( "Error", QStringLiteral( "Caught unhandled std::exception %1" ).arg( e.what() ) ); } catch ( ... ) { - iu.log( "Error", QStringLiteral( "Caught unhandled unknown exception" ) ); + CoreUtils::log( "Error", QStringLiteral( "Caught unhandled unknown exception" ) ); } return ret; } diff --git a/app/projectwizard.cpp b/app/projectwizard.cpp index 2d2c132d5..ad058d249 100644 --- a/app/projectwizard.cpp +++ b/app/projectwizard.cpp @@ -1,5 +1,6 @@ #include "projectwizard.h" #include "inpututils.h" +#include "coreutils.h" #include "qgsproject.h" #include "qgsrasterlayer.h" @@ -71,7 +72,7 @@ QgsVectorLayer *ProjectWizard::createGpkgLayer( QString const &projectDir, QList void ProjectWizard::createProject( QString const &projectName, FieldsModel *fieldsModel ) { - QString projectDir = InputUtils::createUniqueProjectDirectory( mDataDir, projectName ); + QString projectDir = CoreUtils::createUniqueProjectDirectory( mDataDir, projectName ); QString projectFilepath( QString( "%1/%2.%3" ).arg( projectDir ).arg( projectName ).arg( "qgs" ) ); QString gpkgName( QStringLiteral( "data" ) ); QString projectGpkgPath( QString( "%1/%2.%3" ).arg( projectDir ).arg( gpkgName ).arg( "gpkg" ) ); diff --git a/app/purchasing.cpp b/app/purchasing.cpp index e4f946245..bb4734c9e 100644 --- a/app/purchasing.cpp +++ b/app/purchasing.cpp @@ -14,6 +14,7 @@ #include "merginapi.h" #include "inpututils.h" #include "merginuserinfo.h" +#include "coreutils.h" #if defined (APPLE_PURCHASING) #include "ios/iospurchasing.h" @@ -160,7 +161,7 @@ void PurchasingTransaction::verificationFinished() if ( r->error() == QNetworkReply::NoError ) { - InputUtils::log( "purchase successful", QStringLiteral( "Payment success" ) ); + CoreUtils::log( "purchase successful", QStringLiteral( "Payment success" ) ); mPlan->purchasing()->onTransactionVerificationSucceeded( this ); } else @@ -168,7 +169,7 @@ void PurchasingTransaction::verificationFinished() QString serverMsg = api->extractServerErrorMsg( r->readAll() ); QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "purchase" ), r->errorString(), serverMsg ); emit api->networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: purchase" ) ); - InputUtils::log( "purchase", QStringLiteral( "FAILED - %1" ).arg( message ) ); + CoreUtils::log( "purchase", QStringLiteral( "FAILED - %1" ).arg( message ) ); mPlan->purchasing()->onTransactionVerificationFailed( this ); } r->deleteLater(); @@ -377,7 +378,7 @@ void Purchasing::fetchPurchasingPlans( ) request.setUrl( url ); QNetworkReply *reply = mMerginApi->mManager.get( request ); connect( reply, &QNetworkReply::finished, this, &Purchasing::onFetchPurchasingPlansFinished ); - InputUtils::log( "request plan", QStringLiteral( "Requesting purchasing plans for provider %1" ).arg( MerginSubscriptionType::toString( mBackend->provider() ) ) ); + CoreUtils::log( "request plan", QStringLiteral( "Requesting purchasing plans for provider %1" ).arg( MerginSubscriptionType::toString( mBackend->provider() ) ) ); } void Purchasing::onFetchPurchasingPlansFinished() @@ -387,7 +388,7 @@ void Purchasing::onFetchPurchasingPlansFinished() QString serverMsg; if ( r->error() == QNetworkReply::NoError ) { - InputUtils::log( "fetch plans", QStringLiteral( "Success" ) ); + CoreUtils::log( "fetch plans", QStringLiteral( "Success" ) ); QByteArray data = r->readAll(); const QJsonDocument doc = QJsonDocument::fromJson( data ); if ( doc.isArray() ) @@ -419,7 +420,7 @@ void Purchasing::onFetchPurchasingPlansFinished() else { serverMsg = mMerginApi->extractServerErrorMsg( r->readAll() ); - InputUtils::log( "fetch plans", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); + CoreUtils::log( "fetch plans", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); } r->deleteLater(); } @@ -449,7 +450,7 @@ void Purchasing::onPlanRegistrationFailed( const QString &id ) if ( mPlansWithPendingRegistration.empty() && mRegisteredPlans.empty() ) { - InputUtils::log( "Plan Registration", QStringLiteral( "Failed to register any plans" ) ); + CoreUtils::log( "Plan Registration", QStringLiteral( "Failed to register any plans" ) ); } } @@ -509,7 +510,7 @@ void Purchasing::onTransactionCreationSucceeded( QSharedPointermManager.post( request, json ); connect( reply, &QNetworkReply::finished, transaction.get(), &PurchasingTransaction::verificationFinished ); - InputUtils::log( "process transaction", QStringLiteral( "Requesting processing of in-app transaction: " ) + url.toString() ); + CoreUtils::log( "process transaction", QStringLiteral( "Requesting processing of in-app transaction: " ) + url.toString() ); } void Purchasing::onTransactionCreationFailed() diff --git a/app/sources.pri b/app/sources.pri index f0b9cc887..2d3f255c6 100644 --- a/app/sources.pri +++ b/app/sources.pri @@ -4,25 +4,15 @@ activelayer.cpp \ fieldsmodel.cpp \ layersmodel.cpp \ layersproxymodel.cpp \ -localprojectsmanager.cpp \ main.cpp \ -merginprojectmetadata.cpp \ projectwizard.cpp \ loader.cpp \ digitizingcontroller.cpp \ mapthemesmodel.cpp \ appsettings.cpp \ -merginapi.cpp \ -merginapistatus.cpp \ -merginsubscriptionstatus.cpp \ -merginsubscriptiontype.cpp \ -merginprojectstatusmodel.cpp \ -merginuserauth.cpp \ -merginuserinfo.cpp \ androidutils.cpp \ inputexpressionfunctions.cpp \ inpututils.cpp \ -geodiffutils.cpp \ positiondirection.cpp \ purchasing.cpp \ variablesmanager.cpp \ @@ -31,17 +21,8 @@ ios/iosutils.cpp \ inputprojutils.cpp \ codefilter.cpp \ qrdecoder.cpp \ -project.cpp \ projectsmodel.cpp \ -projectsproxymodel.cpp \ - -exists(merginsecrets.cpp) { - message("Using production Mergin API_KEYS") - SOURCES += merginsecrets.cpp -} else { - message("Using development (dummy) Mergin API_KEY") - DEFINES += USE_MERGIN_DUMMY_API_KEY -} +projectsproxymodel.cpp HEADERS += \ inputhelp.h \ @@ -49,24 +30,14 @@ activelayer.h \ fieldsmodel.h \ layersmodel.h \ layersproxymodel.h \ -localprojectsmanager.h \ -merginprojectmetadata.h \ projectwizard.h \ loader.h \ digitizingcontroller.h \ mapthemesmodel.h \ appsettings.h \ -merginapi.h \ -merginapistatus.h \ -merginsubscriptionstatus.h \ -merginsubscriptiontype.h \ -merginprojectstatusmodel.h \ -merginuserauth.h \ -merginuserinfo.h \ androidutils.h \ inputexpressionfunctions.h \ inpututils.h \ -geodiffutils.h \ positiondirection.h \ purchasing.h \ variablesmanager.h \ @@ -75,7 +46,6 @@ ios/iosutils.h \ inputprojutils.h \ codefilter.h \ qrdecoder.h \ -project.h \ projectsmodel.h \ projectsproxymodel.h \ diff --git a/app/test/testmerginapi.cpp b/app/test/testmerginapi.cpp index bc944ace2..e5554fc84 100644 --- a/app/test/testmerginapi.cpp +++ b/app/test/testmerginapi.cpp @@ -6,6 +6,7 @@ #include "testmerginapi.h" #include "inpututils.h" +#include "coreutils.h" #include "geodiffutils.h" #include "testutils.h" #include "merginuserauth.h" @@ -190,7 +191,7 @@ void TestMerginApi::testDownloadProject() QCOMPARE( projectDirEntries.count(), 2 ); // verify that download in progress file is erased - QVERIFY( !QFile::exists( InputUtils::downloadInProgressFilePath( mTestDataPath + "/" + TEST_PROJECT_NAME ) ) ); + QVERIFY( !QFile::exists( CoreUtils::downloadInProgressFilePath( mTestDataPath + "/" + TEST_PROJECT_NAME ) ) ); } void TestMerginApi::testDownloadProjectSpecChars() @@ -1529,7 +1530,7 @@ void TestMerginApi::testRegister() // we do not have a method to delete existing user in the mApi, so for now just make sure // the name does not exists - QString quiteRandom = InputUtils::uuidWithoutBraces( QUuid::createUuid() ).right( 15 ).replace( "-", "" ); + QString quiteRandom = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ).right( 15 ).replace( "-", "" ); QString username = "test_" + quiteRandom; QString email = username + "@nonexistant.email.com"; diff --git a/core/core.pri b/core/core.pri new file mode 100644 index 000000000..70d0330eb --- /dev/null +++ b/core/core.pri @@ -0,0 +1,36 @@ + +SOURCES += \ + $$PWD/coreutils.cpp \ + $$PWD/merginapi.cpp \ + $$PWD/merginapistatus.cpp \ + $$PWD/merginsubscriptionstatus.cpp \ + $$PWD/merginsubscriptiontype.cpp \ + $$PWD/merginprojectstatusmodel.cpp \ + $$PWD/merginuserauth.cpp \ + $$PWD/merginuserinfo.cpp \ + $$PWD/localprojectsmanager.cpp \ + $$PWD/merginprojectmetadata.cpp \ + $$PWD/project.cpp \ + $$PWD/geodiffutils.cpp + +HEADERS += \ + $$PWD/coreutils.h \ + $$PWD/merginapi.h \ + $$PWD/merginapistatus.h \ + $$PWD/merginsubscriptionstatus.h \ + $$PWD/merginsubscriptiontype.h \ + $$PWD/merginprojectstatusmodel.h \ + $$PWD/merginuserauth.h \ + $$PWD/merginuserinfo.h \ + $$PWD/localprojectsmanager.h \ + $$PWD/merginprojectmetadata.h \ + $$PWD/project.h \ + $$PWD/geodiffutils.h + +exists($$PWD/merginsecrets.cpp) { + message("Using production Mergin API_KEYS") + SOURCES += $$PWD/merginsecrets.cpp +} else { + message("Using development (dummy) Mergin API_KEY") + DEFINES += USE_MERGIN_DUMMY_API_KEY +} diff --git a/core/coreutils.cpp b/core/coreutils.cpp new file mode 100644 index 000000000..f1d25e66f --- /dev/null +++ b/core/coreutils.cpp @@ -0,0 +1,187 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "coreutils.h" + +#include +#include +#include +#include +#include + +#include "qcoreapplication.h" +#include "merginapi.h" + +QString CoreUtils::sLogFile = QStringLiteral(); + +QString CoreUtils::appInfo() +{ + return QString( "%1/%2 (%3/%4)" ).arg( QCoreApplication::applicationName() ).arg( QCoreApplication::applicationVersion() ) + .arg( QSysInfo::productType() ).arg( QSysInfo::productVersion() ); +} + +QString CoreUtils::appVersion() +{ + QString version; +#ifdef INPUT_VERSION + version = STR( INPUT_VERSION ); +#endif + return version; +} + + +QString CoreUtils::localizedDateFromUTFString( QString timestamp ) +{ + if ( timestamp.isEmpty() ) + return QString(); + + QDateTime dateTime = QDateTime::fromString( timestamp, Qt::ISODate ); + if ( dateTime.isValid() ) + { + return dateTime.date().toString( Qt::DefaultLocaleShortDate ); + } + else + { + qDebug() << "Unable to convert UTF " << timestamp << " to QDateTime"; + return QString(); + } +} + +QString CoreUtils::uuidWithoutBraces( const QUuid &uuid ) +{ +#if QT_VERSION >= QT_VERSION_CHECK( 5, 11, 0 ) + return uuid.toString( QUuid::WithoutBraces ); +#else + QString str = uuid.toString(); + str = str.mid( 1, str.length() - 2 ); // remove braces + return str; +#endif +} + +bool CoreUtils::removeDir( const QString &dir ) +{ + if ( dir.isEmpty() || dir == "/" ) + return false; + + return QDir( dir ).removeRecursively(); +} + +QString CoreUtils::downloadInProgressFilePath( const QString &projectDir ) +{ + return projectDir + "/.mergin/.project.downloading"; +} + + +void CoreUtils::setLogFilename( const QString &value ) +{ + sLogFile = value; +} + +QString CoreUtils::logFilename() +{ + return sLogFile; +} + +void CoreUtils::log( const QString &topic, const QString &info ) +{ + QString logFilePath; + QByteArray data; + data.append( QString( "%1 %2: %3\n" ).arg( QDateTime().currentDateTimeUtc().toString( Qt::ISODateWithMs ) ).arg( topic ).arg( info ) ); + + qDebug() << data; + appendLog( data, sLogFile ); +} + +void CoreUtils::appendLog( const QByteArray &data, const QString &path ) +{ + QFile file( path ); + + if ( path.isEmpty() || !file.open( QIODevice::Append ) ) + { + return; + } + + file.write( data ); + file.close(); +} + +QDateTime CoreUtils::getLastModifiedFileDateTime( const QString &path ) +{ + QDateTime lastModified; + QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); + while ( it.hasNext() ) + { + it.next(); + if ( !MerginApi::isInIgnore( it.fileInfo() ) ) + { + if ( it.fileInfo().lastModified() > lastModified ) + { + lastModified = it.fileInfo().lastModified(); + } + } + } + return lastModified.toUTC(); +} + +int CoreUtils::getProjectFilesCount( const QString &path ) +{ + int count = 0; + QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); + while ( it.hasNext() ) + { + it.next(); + if ( !MerginApi::isInIgnore( it.fileInfo() ) ) + { + count++; + } + } + return count; +} + +QString CoreUtils::findUniquePath( const QString &path, bool isPathDir ) +{ + QFileInfo pathInfo( path ); + if ( pathInfo.exists() ) + { + int i = 0; + QFileInfo info( path + QString::number( i ) ); + while ( info.exists() && ( info.isDir() || !isPathDir ) ) + { + ++i; + info.setFile( path + QString::number( i ) ); + } + return path + QString::number( i ); + } + else + { + return path; + } +} + +QString CoreUtils::createUniqueProjectDirectory( const QString &baseDataDir, const QString &projectName ) +{ + QString projectDirPath = findUniquePath( baseDataDir + "/" + projectName ); + QDir projectDir( projectDirPath ); + if ( !projectDir.exists() ) + { + QDir dir( "" ); + dir.mkdir( projectDirPath ); + } + return projectDirPath; +} + +bool CoreUtils::createEmptyFile( const QString &filePath ) +{ + QFile newFile( filePath ); + if ( !newFile.open( QIODevice::WriteOnly ) ) + return false; + + newFile.close(); + return true; +} diff --git a/core/coreutils.h b/core/coreutils.h new file mode 100644 index 000000000..8003db4b2 --- /dev/null +++ b/core/coreutils.h @@ -0,0 +1,75 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef COREUTILS_H +#define COREUTILS_H + +#define STR1(x) #x +#define STR(x) STR1(x) + +#include +#include +#include + + +class CoreUtils +{ + public: + explicit CoreUtils( ) = default; + ~CoreUtils() = default; + + static QString appInfo(); + static QString appVersion(); + + static QString localizedDateFromUTFString( QString timestamp ); + static bool removeDir( const QString &projectDir ); + + /** + * Returns name of temporary file indicating first time download of project is in progress + * \param projectName + */ + static QString downloadInProgressFilePath( const QString &projectDir ); + + static QString uuidWithoutBraces( const QUuid &uuid ); + static QDateTime getLastModifiedFileDateTime( const QString &path ); + static int getProjectFilesCount( const QString &path ); + + /** + * Returns given path if doesn't exists, otherwise the slightly modified non-existing path by adding a number to given path. + * \param QString path + * \param QString isPathDir True if the result path suppose to be a folder + */ + static QString findUniquePath( const QString &path, bool isPathDir = true ); + + //! Creates a unique project directory for given project name (used for initial download of a project) + static QString createUniqueProjectDirectory( const QString &baseDataDir, const QString &projectName ); + + + static bool createEmptyFile( const QString &filePath ); + + /** + * Sets the filename of the internal text log file + */ + static void setLogFilename( const QString &value ); + + static QString logFilename(); + + /** + * Add a log entry to internal log text file + * + * \see setLogFilename() + */ + static void log( const QString &topic, const QString &info ); + + private: + static QString sLogFile; + static void appendLog( const QByteArray &data, const QString &path ); +}; + +#endif // COREUTILS_H diff --git a/app/geodiffutils.cpp b/core/geodiffutils.cpp similarity index 79% rename from app/geodiffutils.cpp rename to core/geodiffutils.cpp index 164e51e58..88d61695e 100644 --- a/app/geodiffutils.cpp +++ b/core/geodiffutils.cpp @@ -16,14 +16,16 @@ #include #include - -#include "inpututils.h" +#include "coreutils.h" QString GeodiffUtils::diffableFilePendingChanges( const QString &projectDir, const QString &filePath, bool onlySummary ) { - QString diffPath, basePath; - int res = createChangeset( projectDir, filePath, diffPath, basePath ); + QString diffName; + int res = createChangeset( projectDir, filePath, diffName ); + QString diffPath = projectDir + "/.mergin/" + diffName; + QString basePath = projectDir + "/.mergin/" + filePath; + if ( res == GEODIFF_SUCCESS ) { QTemporaryFile f; @@ -51,14 +53,14 @@ QString GeodiffUtils::diffableFilePendingChanges( const QString &projectDir, con } -int GeodiffUtils::createChangeset( const QString &projectDir, const QString &filePath, QString &diffPath, QString &basePath ) +int GeodiffUtils::createChangeset( const QString &projectDir, const QString &fileName, QString &diffName ) { - QString uuid = InputUtils::uuidWithoutBraces( QUuid::createUuid() ); - QString diffName = filePath + "-diff-" + uuid; - QString modifiedPath = projectDir + "/" + filePath; - basePath = projectDir + "/.mergin/" + filePath; - diffPath = projectDir + "/.mergin/" + diffName; - return GEODIFF_createChangeset( basePath.toUtf8(), modifiedPath.toUtf8(), diffPath.toUtf8() ); + QString uuid = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); + diffName = fileName + "-diff-" + uuid; + QString modifiedAbsPath = projectDir + "/" + fileName; + QString baseAbsPath = projectDir + "/.mergin/" + fileName; + QString diffAbsPath = projectDir + "/.mergin/" + diffName; + return GEODIFF_createChangeset( baseAbsPath.toUtf8(), modifiedAbsPath.toUtf8(), diffAbsPath.toUtf8() ); } @@ -94,7 +96,7 @@ bool GeodiffUtils::applyDiffs( const QString &src, const QStringList &diffFiles { if ( diffFiles.isEmpty() ) { - InputUtils::log( "GEODIFF", "assemble server file fail: no input diff files!" ); + CoreUtils::log( "GEODIFF", "assemble server file fail: no input diff files!" ); return false; } @@ -103,7 +105,7 @@ bool GeodiffUtils::applyDiffs( const QString &src, const QStringList &diffFiles int res = GEODIFF_applyChangeset( src.toUtf8().constData(), diffFile.toUtf8().constData() ); if ( res != GEODIFF_SUCCESS ) { - InputUtils::log( "GEODIFF", "assemble server file fail: apply changeset failed " + diffFile ); + CoreUtils::log( "GEODIFF", "assemble server file fail: apply changeset failed " + diffFile ); return false; } } @@ -121,5 +123,5 @@ void GeodiffUtils::log( GEODIFF_LoggerLevel level, const char *msg ) case LevelDebug: prefix = "GEODIFF debug"; break; default: break; } - InputUtils::log( prefix, msg ); -} \ No newline at end of file + CoreUtils::log( prefix, msg ); +} diff --git a/app/geodiffutils.h b/core/geodiffutils.h similarity index 89% rename from app/geodiffutils.h rename to core/geodiffutils.h index 7d636c79c..19e5bad95 100644 --- a/app/geodiffutils.h +++ b/core/geodiffutils.h @@ -48,9 +48,12 @@ class GeodiffUtils //! Returns JSON with local pending changes o a diffable file static QString diffableFilePendingChanges( const QString &projectDir, const QString &filePath, bool onlySummary ); - //! Runs geodiff on a local project's file, compares it to locally cached original and creates a diff file - //! (diff file path returned in the argument, returns geodiff return value - zero on success) - static int createChangeset( const QString &projectDir, const QString &filePath, QString &diffPath, QString &basePath ); + /** + * Runs geodiff on a local project's file, compares it to locally cached original and creates a diff file + * (diff file path returned in the argument is RELATIVE to "projectDir/.mergin/" DIR) + * \returns geodiff return value - zero on success + */ + static int createChangeset( const QString &projectDir, const QString &fileName, QString &diffName ); //! Takes "src" file and applies a sequence of changesets for the list in "diffFiles" static bool applyDiffs( const QString &src, const QStringList &diffFiles ); diff --git a/app/localprojectsmanager.cpp b/core/localprojectsmanager.cpp similarity index 97% rename from app/localprojectsmanager.cpp rename to core/localprojectsmanager.cpp index 258ada02e..58c9dcc87 100644 --- a/app/localprojectsmanager.cpp +++ b/core/localprojectsmanager.cpp @@ -11,7 +11,7 @@ #include "merginapi.h" #include "merginprojectmetadata.h" -#include "inpututils.h" +#include "coreutils.h" #include #include @@ -104,7 +104,7 @@ void LocalProjectsManager::removeLocalProject( const QString &projectId ) { emit aboutToRemoveLocalProject( mProjects[i] ); - InputUtils::removeDir( mProjects[i].projectDir ); + CoreUtils::removeDir( mProjects[i].projectDir ); mProjects.removeAt( i ); return; @@ -167,7 +167,7 @@ void LocalProjectsManager::updateNamespace( const QString &projectDir, const QSt QString LocalProjectsManager::findQgisProjectFile( const QString &projectDir, QString &err ) { - if ( QFile::exists( InputUtils::downloadInProgressFilePath( projectDir ) ) ) + if ( QFile::exists( CoreUtils::downloadInProgressFilePath( projectDir ) ) ) { // if this is a mergin project and file indicating download in progress is still there // download failed or copying from .temp to project dir failed (app was probably closed meanwhile) diff --git a/app/localprojectsmanager.h b/core/localprojectsmanager.h similarity index 100% rename from app/localprojectsmanager.h rename to core/localprojectsmanager.h diff --git a/app/merginapi.cpp b/core/merginapi.cpp similarity index 85% rename from app/merginapi.cpp rename to core/merginapi.cpp index 72feef615..56bc31e63 100644 --- a/app/merginapi.cpp +++ b/core/merginapi.cpp @@ -19,13 +19,12 @@ #include #include -#include "inpututils.h" +#include "coreutils.h" #include "geodiffutils.h" -#include "qgsquickutils.h" #include "localprojectsmanager.h" #include "merginuserauth.h" #include "merginuserinfo.h" -#include "purchasing.h" +// #include "purchasing.h" #include @@ -100,10 +99,10 @@ QString MerginApi::listProjects( const QString &searchExpression, const QString QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() ); request.setUrl( url ); - QString requestId = InputUtils::uuidWithoutBraces( QUuid::createUuid() ); + QString requestId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); QNetworkReply *reply = mManager.get( request ); - InputUtils::log( "list projects", QStringLiteral( "Requesting: " ) + url.toString() ); + CoreUtils::log( "list projects", QStringLiteral( "Requesting: " ) + url.toString() ); connect( reply, &QNetworkReply::finished, this, [this, requestId]() {this->listProjectsReplyFinished( requestId );} ); return requestId; @@ -125,10 +124,10 @@ QString MerginApi::listProjectsByName( const QStringList &projectNames ) request.setUrl( url ); request.setRawHeader( "Content-type", "application/json" ); - QString requestId = InputUtils::uuidWithoutBraces( QUuid::createUuid() ); + QString requestId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); QNetworkReply *reply = mManager.post( request, body.toJson() ); - InputUtils::log( "list projects by name", QStringLiteral( "Requesting: " ) + url.toString() ); + CoreUtils::log( "list projects by name", QStringLiteral( "Requesting: " ) + url.toString() ); connect( reply, &QNetworkReply::finished, this, [this, requestId]() {this->listProjectsByNameReplyFinished( requestId );} ); return requestId; @@ -174,8 +173,8 @@ void MerginApi::downloadNextItem( const QString &projectFullName ) transaction.replyDownloadItem = mManager.get( request ); connect( transaction.replyDownloadItem, &QNetworkReply::finished, this, &MerginApi::downloadItemReplyFinished ); - InputUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting item: " ) + url.toString() + - ( !range.isEmpty() ? " Range: " + range : QString() ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting item: " ) + url.toString() + + ( !range.isEmpty() ? " Range: " + range : QString() ) ); } void MerginApi::removeProjectsTempFolder( const QString &projectNamespace, const QString &projectName ) @@ -190,7 +189,7 @@ void MerginApi::removeProjectsTempFolder( const QString &projectNamespace, const QNetworkRequest MerginApi::getDefaultRequest( bool withAuth ) { QNetworkRequest request; - QString info = InputUtils::appInfo(); + QString info = CoreUtils::appInfo(); request.setRawHeader( "User-Agent", QByteArray( info.toUtf8() ) ); if ( withAuth ) request.setRawHeader( "Authorization", QByteArray( "Bearer " + mUserAuth->authToken() ) ); @@ -266,7 +265,7 @@ void MerginApi::downloadItemReplyFinished() { QByteArray data = r->readAll(); - InputUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded item (%1 bytes)" ).arg( data.size() ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded item (%1 bytes)" ).arg( data.size() ) ); QString tempFolder = getTempProjectDir( projectFullName ); QString tempFilePath = tempFolder + "/" + tempFileName; @@ -281,7 +280,7 @@ void MerginApi::downloadItemReplyFinished() } else { - InputUtils::log( "pull " + projectFullName, "Failed to open for writing: " + file.fileName() ); + CoreUtils::log( "pull " + projectFullName, "Failed to open for writing: " + file.fileName() ); } transaction.transferedSize += data.size(); @@ -300,7 +299,7 @@ void MerginApi::downloadItemReplyFinished() { serverMsg = r->errorString(); } - InputUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); transaction.replyDownloadItem->deleteLater(); transaction.replyDownloadItem = nullptr; @@ -358,7 +357,7 @@ void MerginApi::uploadFile( const QString &projectFullName, const QString &trans transaction.replyUploadFile = mManager.post( request, data ); connect( transaction.replyUploadFile, &QNetworkReply::finished, this, &MerginApi::uploadFileReplyFinished ); - InputUtils::log( "push " + projectFullName, QStringLiteral( "Uploading item: " ) + url.toString() ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploading item: " ) + url.toString() ); } void MerginApi::uploadStart( const QString &projectFullName, const QByteArray &json ) @@ -381,7 +380,7 @@ void MerginApi::uploadStart( const QString &projectFullName, const QByteArray &j transaction.replyUploadStart = mManager.post( request, json ); connect( transaction.replyUploadStart, &QNetworkReply::finished, this, &MerginApi::uploadStartReplyFinished ); - InputUtils::log( "push " + projectFullName, QStringLiteral( "Starting push request: " ) + url.toString() ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Starting push request: " ) + url.toString() ); } void MerginApi::uploadCancel( const QString &projectFullName ) @@ -394,25 +393,25 @@ void MerginApi::uploadCancel( const QString &projectFullName ) if ( !mTransactionalStatus.contains( projectFullName ) ) return; - InputUtils::log( "push " + projectFullName, QStringLiteral( "User requested cancel" ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "User requested cancel" ) ); TransactionStatus &transaction = mTransactionalStatus[projectFullName]; // There is an open transaction, abort it followed by calling cancelUpload again. if ( transaction.replyUploadProjectInfo ) { - InputUtils::log( "push " + projectFullName, QStringLiteral( "Aborting project info request" ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting project info request" ) ); transaction.replyUploadProjectInfo->abort(); // will trigger uploadInfoReplyFinished slot and emit sync finished } else if ( transaction.replyUploadStart ) { - InputUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload start" ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload start" ) ); transaction.replyUploadStart->abort(); // will trigger uploadStartReplyFinished slot and emit sync finished } else if ( transaction.replyUploadFile ) { QString transactionUUID = transaction.transactionUUID; // copy transaction uuid as the transaction object will be gone after abort - InputUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload file" ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload file" ) ); transaction.replyUploadFile->abort(); // will trigger uploadFileReplyFinished slot and emit sync finished // also need to cancel the transaction @@ -421,7 +420,7 @@ void MerginApi::uploadCancel( const QString &projectFullName ) else if ( transaction.replyUploadFinish ) { QString transactionUUID = transaction.transactionUUID; // copy transaction uuid as the transaction object will be gone after abort - InputUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload finish" ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload finish" ) ); transaction.replyUploadFinish->abort(); // will trigger uploadFinishReplyFinished slot and emit sync finished sendUploadCancelRequest( projectFullName, transactionUUID ); @@ -443,7 +442,7 @@ void MerginApi::sendUploadCancelRequest( const QString &projectFullName, const Q QNetworkReply *reply = mManager.post( request, QByteArray() ); connect( reply, &QNetworkReply::finished, this, &MerginApi::uploadCancelReplyFinished ); - InputUtils::log( "push " + projectFullName, QStringLiteral( "Requesting upload transaction cancel: " ) + url.toString() ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting upload transaction cancel: " ) + url.toString() ); } void MerginApi::updateCancel( const QString &projectFullName ) @@ -451,20 +450,20 @@ void MerginApi::updateCancel( const QString &projectFullName ) if ( !mTransactionalStatus.contains( projectFullName ) ) return; - InputUtils::log( "pull " + projectFullName, QStringLiteral( "User requested cancel" ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "User requested cancel" ) ); TransactionStatus &transaction = mTransactionalStatus[projectFullName]; if ( transaction.replyProjectInfo ) { // we're still fetching project info - InputUtils::log( "pull " + projectFullName, QStringLiteral( "Aborting project info request" ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Aborting project info request" ) ); transaction.replyProjectInfo->abort(); // abort will trigger updateInfoReplyFinished() slot } else if ( transaction.replyDownloadItem ) { // we're already downloading some files - InputUtils::log( "pull " + projectFullName, QStringLiteral( "Aborting pending download" ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Aborting pending download" ) ); transaction.replyDownloadItem->abort(); // abort will trigger downloadItemReplyFinished slot } else @@ -493,19 +492,19 @@ void MerginApi::uploadFinish( const QString &projectFullName, const QString &tra transaction.replyUploadFinish = mManager.post( request, QByteArray() ); connect( transaction.replyUploadFinish, &QNetworkReply::finished, this, &MerginApi::uploadFinishReplyFinished ); - InputUtils::log( "push " + projectFullName, QStringLiteral( "Requesting transaction finish: " ) + transactionUUID ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting transaction finish: " ) + transactionUUID ); } void MerginApi::updateProject( const QString &projectNamespace, const QString &projectName, bool withoutAuth ) { QString projectFullName = getFullProjectName( projectNamespace, projectName ); - InputUtils::log( "pull " + projectFullName, "### Starting ###" ); + CoreUtils::log( "pull " + projectFullName, "### Starting ###" ); QNetworkReply *reply = getProjectInfo( projectFullName, withoutAuth ); if ( reply ) { - InputUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() ); Q_ASSERT( !mTransactionalStatus.contains( projectFullName ) ); mTransactionalStatus.insert( projectFullName, TransactionStatus() ); @@ -517,7 +516,7 @@ void MerginApi::updateProject( const QString &projectNamespace, const QString &p } else { - InputUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED to create project info request!" ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED to create project info request!" ) ); } } @@ -525,12 +524,12 @@ void MerginApi::uploadProject( const QString &projectNamespace, const QString &p { QString projectFullName = getFullProjectName( projectNamespace, projectName ); - InputUtils::log( "push " + projectFullName, "### Starting ###" ); + CoreUtils::log( "push " + projectFullName, "### Starting ###" ); QNetworkReply *reply = getProjectInfo( projectFullName ); if ( reply ) { - InputUtils::log( "push " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() ); // create entry about pending upload for the project Q_ASSERT( !mTransactionalStatus.contains( projectFullName ) ); @@ -543,7 +542,7 @@ void MerginApi::uploadProject( const QString &projectNamespace, const QString &p } else { - InputUtils::log( "push " + projectFullName, QStringLiteral( "FAILED to create project info request!" ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED to create project info request!" ) ); } } @@ -556,7 +555,9 @@ void MerginApi::authorize( const QString &login, const QString &password ) return; } - whileBlocking( mUserAuth )->setPassword( password ); + mUserAuth->blockSignals( true ); + mUserAuth->setPassword( password ); + mUserAuth->blockSignals( false ); QNetworkRequest request = getDefaultRequest( false ); QString urlString = mApiRoot + QStringLiteral( "v1/auth/login2" ); @@ -573,7 +574,7 @@ void MerginApi::authorize( const QString &login, const QString &password ) QNetworkReply *reply = mManager.post( request, json ); connect( reply, &QNetworkReply::finished, this, &MerginApi::authorizeFinished ); - InputUtils::log( "auth", QStringLiteral( "Requesting authorization: " ) + url.toString() ); + CoreUtils::log( "auth", QStringLiteral( "Requesting authorization: " ) + url.toString() ); } void MerginApi::registerUser( const QString &username, @@ -641,7 +642,7 @@ void MerginApi::registerUser( const QString &username, QByteArray json = jsonDoc.toJson( QJsonDocument::Compact ); QNetworkReply *reply = mManager.post( request, json ); connect( reply, &QNetworkReply::finished, this, [ = ]() { this->registrationFinished( username, password ); } ); - InputUtils::log( "auth", QStringLiteral( "Requesting registration: " ) + url.toString() ); + CoreUtils::log( "auth", QStringLiteral( "Requesting registration: " ) + url.toString() ); } void MerginApi::getUserInfo( ) @@ -657,7 +658,7 @@ void MerginApi::getUserInfo( ) request.setUrl( url ); QNetworkReply *reply = mManager.get( request ); - InputUtils::log( "user info", QStringLiteral( "Requesting user info: " ) + url.toString() ); + CoreUtils::log( "user info", QStringLiteral( "Requesting user info: " ) + url.toString() ); connect( reply, &QNetworkReply::finished, this, &MerginApi::getUserInfoFinished ); } @@ -710,7 +711,7 @@ void MerginApi::createProject( const QString &projectNamespace, const QString &p QNetworkReply *reply = mManager.post( request, json ); connect( reply, &QNetworkReply::finished, this, &MerginApi::createProjectFinished ); - InputUtils::log( "create " + projectFullName, QStringLiteral( "Requesting project creation: " ) + url.toString() ); + CoreUtils::log( "create " + projectFullName, QStringLiteral( "Requesting project creation: " ) + url.toString() ); } void MerginApi::deleteProject( const QString &projectNamespace, const QString &projectName ) @@ -728,7 +729,7 @@ void MerginApi::deleteProject( const QString &projectNamespace, const QString &p request.setAttribute( static_cast( AttrProjectFullName ), projectFullName ); QNetworkReply *reply = mManager.deleteResource( request ); connect( reply, &QNetworkReply::finished, this, &MerginApi::deleteProjectFinished ); - InputUtils::log( "delete " + projectFullName, QStringLiteral( "Requesting project deletion: " ) + url.toString() ); + CoreUtils::log( "delete " + projectFullName, QStringLiteral( "Requesting project deletion: " ) + url.toString() ); } void MerginApi::saveAuthData() @@ -750,7 +751,7 @@ void MerginApi::createProjectFinished() if ( r->error() == QNetworkReply::NoError ) { - InputUtils::log( "create " + projectFullName, QStringLiteral( "Success" ) ); + CoreUtils::log( "create " + projectFullName, QStringLiteral( "Success" ) ); emit projectCreated( projectFullName, true ); QString projectNamespace, projectName; @@ -776,7 +777,7 @@ void MerginApi::createProjectFinished() { QString serverMsg = extractServerErrorMsg( r->readAll() ); QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg ); - InputUtils::log( "create " + projectFullName, message ); + CoreUtils::log( "create " + projectFullName, message ); emit projectCreated( projectFullName, false ); emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: createProject" ) ); } @@ -792,7 +793,7 @@ void MerginApi::deleteProjectFinished() if ( r->error() == QNetworkReply::NoError ) { - InputUtils::log( "delete " + projectFullName, QStringLiteral( "Success" ) ); + CoreUtils::log( "delete " + projectFullName, QStringLiteral( "Success" ) ); emit notify( QStringLiteral( "Project deleted" ) ); emit serverProjectDeleted( projectFullName, true ); @@ -800,7 +801,7 @@ void MerginApi::deleteProjectFinished() else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - InputUtils::log( "delete " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); + CoreUtils::log( "delete " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); emit serverProjectDeleted( projectFullName, false ); emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: deleteProject" ) ); } @@ -814,7 +815,7 @@ void MerginApi::authorizeFinished() if ( r->error() == QNetworkReply::NoError ) { - InputUtils::log( "auth", QStringLiteral( "Success" ) ); + CoreUtils::log( "auth", QStringLiteral( "Success" ) ); const QByteArray data = r->readAll(); QJsonDocument doc = QJsonDocument::fromJson( data ); if ( doc.isObject() ) @@ -825,11 +826,14 @@ void MerginApi::authorizeFinished() } else { - whileBlocking( mUserAuth )->setUsername( QString() ); //clearTokenData emits the authChanged - whileBlocking( mUserAuth )->setPassword( QString() ); //clearTokenData emits the authChanged + mUserAuth->blockSignals( true ); + mUserAuth->setUsername( QString() ); //clearTokenData emits the authChanged + mUserAuth->setPassword( QString() ); //clearTokenData emits the authChanged + mUserAuth->blockSignals( false ); + mUserAuth->clearTokenData(); emit authFailed(); - InputUtils::log( "auth", QStringLiteral( "FAILED - invalid JSON response" ) ); + CoreUtils::log( "auth", QStringLiteral( "FAILED - invalid JSON response" ) ); qDebug() << data; emit notify( "Internal server error during authorization" ); } @@ -837,7 +841,7 @@ void MerginApi::authorizeFinished() else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - InputUtils::log( "auth", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); + CoreUtils::log( "auth", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ); int status = statusCode.toInt(); if ( status == 401 || status == 400 ) @@ -867,7 +871,7 @@ void MerginApi::registrationFinished( const QString &username, const QString &pa if ( r->error() == QNetworkReply::NoError ) { - InputUtils::log( "register", QStringLiteral( "Success" ) ); + CoreUtils::log( "register", QStringLiteral( "Success" ) ); emit registrationSucceeded(); QString msg = tr( "Registration successful" ); emit notify( msg ); @@ -878,7 +882,7 @@ void MerginApi::registrationFinished( const QString &username, const QString &pa else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - InputUtils::log( "register", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); + CoreUtils::log( "register", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ); int status = statusCode.toInt(); if ( status == 401 || status == 400 ) @@ -911,7 +915,7 @@ void MerginApi::pingMerginReplyFinished() if ( r->error() == QNetworkReply::NoError ) { - InputUtils::log( "ping", QStringLiteral( "Success" ) ); + CoreUtils::log( "ping", QStringLiteral( "Success" ) ); QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); if ( doc.isObject() ) { @@ -923,7 +927,7 @@ void MerginApi::pingMerginReplyFinished() else { serverMsg = extractServerErrorMsg( r->readAll() ); - InputUtils::log( "ping", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); + CoreUtils::log( "ping", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); } r->deleteLater(); emit pingMerginFinished( apiVersion, serverSupportsSubscriptions, serverMsg ); @@ -1115,13 +1119,13 @@ void MerginApi::pingMergin() request.setUrl( url ); QNetworkReply *reply = mManager.get( request ); - InputUtils::log( "ping", QStringLiteral( "Requesting: " ) + url.toString() ); + CoreUtils::log( "ping", QStringLiteral( "Requesting: " ) + url.toString() ); connect( reply, &QNetworkReply::finished, this, &MerginApi::pingMerginReplyFinished ); } void MerginApi::migrateProjectToMergin( const QString &projectName, const QString &projectNamespace ) { - InputUtils::log( "migrate project", projectName ); + CoreUtils::log( "migrate project", projectName ); if ( projectNamespace.isEmpty() ) { createProject( mUserAuth->username(), projectName ); @@ -1140,7 +1144,7 @@ void MerginApi::detachProjectFromMergin( const QString &projectNamespace, const if ( projectInfo.isValid() ) { - InputUtils::removeDir( projectInfo.projectDir + "/.mergin" ); + CoreUtils::removeDir( projectInfo.projectDir + "/.mergin" ); } // Update localProject @@ -1228,14 +1232,14 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) projectList = parseProjectsFromJson( doc ); } - InputUtils::log( "list projects", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) ); + CoreUtils::log( "list projects", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) ); } else { QString serverMsg = extractServerErrorMsg( r->readAll() ); QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjects" ), r->errorString(), serverMsg ); emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjects" ) ); - InputUtils::log( "list projects", QStringLiteral( "FAILED - %1" ).arg( message ) ); + CoreUtils::log( "list projects", QStringLiteral( "FAILED - %1" ).arg( message ) ); emit listProjectsFailed(); } @@ -1258,14 +1262,14 @@ void MerginApi::listProjectsByNameReplyFinished( QString requestId ) QByteArray data = r->readAll(); QJsonDocument json = QJsonDocument::fromJson( data ); projectList = parseProjectsFromJson( json ); - InputUtils::log( "list projects by name", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) ); + CoreUtils::log( "list projects by name", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) ); } else { QString serverMsg = extractServerErrorMsg( r->readAll() ); QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjectsByName" ), r->errorString(), serverMsg ); emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjectsByName" ) ); - InputUtils::log( "list projects by name", QStringLiteral( "FAILED - %1" ).arg( message ) ); + CoreUtils::log( "list projects by name", QStringLiteral( "FAILED - %1" ).arg( message ) ); } r->deleteLater(); @@ -1276,7 +1280,7 @@ void MerginApi::listProjectsByNameReplyFinished( QString requestId ) void MerginApi::finalizeProjectUpdateCopy( const QString &projectFullName, const QString &projectDir, const QString &tempDir, const QString &filePath, const QList &items ) { - InputUtils::log( "pull " + projectFullName, QStringLiteral( "Copying new content of " ) + filePath ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Copying new content of " ) + filePath ); QString dest = projectDir + "/" + filePath; createPathIfNotExists( dest ); @@ -1284,7 +1288,7 @@ void MerginApi::finalizeProjectUpdateCopy( const QString &projectFullName, const QFile f( dest ); if ( !f.open( QIODevice::WriteOnly ) ) { - InputUtils::log( "pull " + projectFullName, "Failed to open file for writing " + dest ); + CoreUtils::log( "pull " + projectFullName, "Failed to open file for writing " + dest ); return; } @@ -1294,7 +1298,7 @@ void MerginApi::finalizeProjectUpdateCopy( const QString &projectFullName, const QFile fTmp( tempDir + "/" + item.tempFileName ); if ( !fTmp.open( QIODevice::ReadOnly ) ) { - InputUtils::log( "pull " + projectFullName, "Failed to open temp file for reading " + item.tempFileName ); + CoreUtils::log( "pull " + projectFullName, "Failed to open temp file for reading " + item.tempFileName ); return; } f.write( fTmp.readAll() ); @@ -1310,11 +1314,11 @@ void MerginApi::finalizeProjectUpdateCopy( const QString &projectFullName, const if ( !QFile::remove( basefile ) ) { - InputUtils::log( "pull " + projectFullName, "failed to remove old basefile for: " + filePath ); + CoreUtils::log( "pull " + projectFullName, "failed to remove old basefile for: " + filePath ); } if ( !QFile::copy( dest, basefile ) ) { - InputUtils::log( "pull " + projectFullName, "failed to copy new basefile for: " + filePath ); + CoreUtils::log( "pull " + projectFullName, "failed to copy new basefile for: " + filePath ); } } } @@ -1322,13 +1326,13 @@ void MerginApi::finalizeProjectUpdateCopy( const QString &projectFullName, const void MerginApi::finalizeProjectUpdateApplyDiff( const QString &projectFullName, const QString &projectDir, const QString &tempDir, const QString &filePath, const QList &items ) { - InputUtils::log( "pull " + projectFullName, QStringLiteral( "Applying diff to " ) + filePath ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Applying diff to " ) + filePath ); // update diffable files that have been modified on the server // - if they were not modified locally, the server changes will be simply applied // - if they were modified locally, local changes will be rebased on top of server changes - QString src = tempDir + "/" + InputUtils::uuidWithoutBraces( QUuid::createUuid() ); + QString src = tempDir + "/" + CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); QString dest = projectDir + "/" + filePath; QString basefile = projectDir + "/.mergin/" + filePath; @@ -1349,21 +1353,21 @@ void MerginApi::finalizeProjectUpdateApplyDiff( const QString &projectFullName, if ( !QFile::copy( basefile, src ) ) { - InputUtils::log( "pull " + projectFullName, "assemble server file fail: copying failed " + basefile + " to " + src ); + CoreUtils::log( "pull " + projectFullName, "assemble server file fail: copying failed " + basefile + " to " + src ); // TODO: this is a critical failure - we should abort pull } if ( !GeodiffUtils::applyDiffs( src, diffFiles ) ) { - InputUtils::log( "pull " + projectFullName, "server file assembly failed: " + filePath ); + CoreUtils::log( "pull " + projectFullName, "server file assembly failed: " + filePath ); // TODO: this is a critical failure - we should abort pull // TODO: we could try to delete the basefile and re-download it from scratch on next sync } else { - InputUtils::log( "pull " + projectFullName, "server file assembly successful: " + filePath ); + CoreUtils::log( "pull " + projectFullName, "server file assembly successful: " + filePath ); } // @@ -1377,23 +1381,23 @@ void MerginApi::finalizeProjectUpdateApplyDiff( const QString &projectFullName, ); if ( res == GEODIFF_SUCCESS ) { - InputUtils::log( "pull " + projectFullName, "geodiff rebase successful: " + filePath ); + CoreUtils::log( "pull " + projectFullName, "geodiff rebase successful: " + filePath ); } else { - InputUtils::log( "pull " + projectFullName, "geodiff rebase failed! " + filePath ); + CoreUtils::log( "pull " + projectFullName, "geodiff rebase failed! " + filePath ); // not good... something went wrong in rebase - we need to save the local changes // let's put them into a conflict file and use the server version LocalProject info = mLocalProjects.projectFromMerginName( projectFullName ); - QString newDest = InputUtils::findUniquePath( generateConflictFileName( dest, info.localVersion ), false ); + QString newDest = CoreUtils::findUniquePath( generateConflictFileName( dest, info.localVersion ), false ); if ( !QFile::rename( dest, newDest ) ) { - InputUtils::log( "pull " + projectFullName, "failed rename of conflicting file after failed geodiff rebase: " + filePath ); + CoreUtils::log( "pull " + projectFullName, "failed rename of conflicting file after failed geodiff rebase: " + filePath ); } if ( !QFile::copy( src, dest ) ) { - InputUtils::log( "pull " + projectFullName, "failed to update local conflicting file after failed geodiff rebase: " + filePath ); + CoreUtils::log( "pull " + projectFullName, "failed to update local conflicting file after failed geodiff rebase: " + filePath ); } } @@ -1403,13 +1407,13 @@ void MerginApi::finalizeProjectUpdateApplyDiff( const QString &projectFullName, if ( !QFile::remove( basefile ) ) { - InputUtils::log( "pull " + projectFullName, "failed removal of old basefile: " + filePath ); + CoreUtils::log( "pull " + projectFullName, "failed removal of old basefile: " + filePath ); // TODO: this is a critical failure - we should abort pull } if ( !QFile::rename( src, basefile ) ) { - InputUtils::log( "pull " + projectFullName, "failed rename of basefile using new server content: " + filePath ); + CoreUtils::log( "pull " + projectFullName, "failed rename of basefile using new server content: " + filePath ); // TODO: this is a critical failure - we should abort pull } @@ -1423,7 +1427,7 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) QString projectDir = transaction.projectDir; QString tempProjectDir = getTempProjectDir( projectFullName ); - InputUtils::log( "pull " + projectFullName, "Running update tasks" ); + CoreUtils::log( "pull " + projectFullName, "Running update tasks" ); for ( const UpdateTask &finalizationItem : transaction.updateTasks ) { @@ -1440,14 +1444,14 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) // move local file to conflict file QString origPath = projectDir + "/" + finalizationItem.filePath; LocalProject info = mLocalProjects.projectFromMerginName( projectFullName ); - QString newPath = InputUtils::findUniquePath( generateConflictFileName( origPath, info.localVersion ), false ); + QString newPath = CoreUtils::findUniquePath( generateConflictFileName( origPath, info.localVersion ), false ); if ( !QFile::rename( origPath, newPath ) ) { - InputUtils::log( "pull " + projectFullName, "failed rename of conflicting file: " + finalizationItem.filePath ); + CoreUtils::log( "pull " + projectFullName, "failed rename of conflicting file: " + finalizationItem.filePath ); } else { - InputUtils::log( "pull " + projectFullName, "Local file renamed due to conflict with server: " + finalizationItem.filePath ); + CoreUtils::log( "pull " + projectFullName, "Local file renamed due to conflict with server: " + finalizationItem.filePath ); } finalizeProjectUpdateCopy( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data ); break; @@ -1461,7 +1465,7 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) case UpdateTask::Delete: { - InputUtils::log( "pull " + projectFullName, "Removing local file: " + finalizationItem.filePath ); + CoreUtils::log( "pull " + projectFullName, "Removing local file: " + finalizationItem.filePath ); QFile file( projectDir + "/" + finalizationItem.filePath ); file.remove(); break; @@ -1472,7 +1476,7 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) for ( const auto &downloadItem : finalizationItem.data ) { if ( !QFile::remove( tempProjectDir + "/" + downloadItem.tempFileName ) ) - InputUtils::log( "pull " + projectFullName, "Failed to remove temporary file " + downloadItem.tempFileName ); + CoreUtils::log( "pull " + projectFullName, "Failed to remove temporary file " + downloadItem.tempFileName ); } } @@ -1480,7 +1484,7 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) int tmpFilesLeft = QDir( tempProjectDir ).entryList( QDir::NoDotAndDotDot ).count(); if ( tmpFilesLeft ) { - InputUtils::log( "pull " + projectFullName, "Some temporary files were left - this should not happen..." ); + CoreUtils::log( "pull " + projectFullName, "Some temporary files were left - this should not happen..." ); } QDir( tempProjectDir ).removeRecursively(); @@ -1492,8 +1496,8 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) extractProjectName( projectFullName, projectNamespace, projectName ); // remove download in progress file - if ( !QFile::remove( InputUtils::downloadInProgressFilePath( transaction.projectDir ) ) ) - InputUtils::log( QStringLiteral( "sync %1" ).arg( projectFullName ), QStringLiteral( "Failed to remove download in progress file for project name %1" ).arg( projectName ) ); + if ( !QFile::remove( CoreUtils::downloadInProgressFilePath( transaction.projectDir ) ) ) + CoreUtils::log( QStringLiteral( "sync %1" ).arg( projectFullName ), QStringLiteral( "Failed to remove download in progress file for project name %1" ).arg( projectName ) ); mLocalProjects.addMerginProject( projectDir, projectNamespace, projectName ); } @@ -1532,7 +1536,7 @@ void MerginApi::uploadStartReplyFinished() transaction.transactionUUID = transactionUUID; } - InputUtils::log( "push " + projectFullName, QStringLiteral( "Push request accepted. Transaction ID: " ) + transactionUUID ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Push request accepted. Transaction ID: " ) + transactionUUID ); MerginFile file = files.first(); uploadFile( projectFullName, transactionUUID, file ); @@ -1543,7 +1547,7 @@ void MerginApi::uploadStartReplyFinished() // we are done here - no upload of chunks, no request to "finish" // because server immediatelly creates a new version without starting a transaction to upload chunks - InputUtils::log( "push " + projectFullName, QStringLiteral( "Push request accepted and no files to upload" ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Push request accepted and no files to upload" ) ); transaction.projectMetadata = data; transaction.version = MerginProjectMetadata::fromJson( data ).version; @@ -1559,7 +1563,7 @@ void MerginApi::uploadStartReplyFinished() QString errorMsg = r->errorString(); bool showAsDialog = status == 400 && serverMsg == QStringLiteral( "You have reached a data limit" ); - InputUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); transaction.replyUploadStart->deleteLater(); transaction.replyUploadStart = nullptr; @@ -1587,7 +1591,7 @@ void MerginApi::uploadFileReplyFinished() if ( r->error() == QNetworkReply::NoError ) { - InputUtils::log( "push " + projectFullName, QStringLiteral( "Uploaded successfully: " ) + chunkID ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploaded successfully: " ) + chunkID ); transaction.replyUploadFile->deleteLater(); transaction.replyUploadFile = nullptr; @@ -1619,7 +1623,7 @@ void MerginApi::uploadFileReplyFinished() else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - InputUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: uploadFile" ) ); transaction.replyUploadFile->deleteLater(); @@ -1643,7 +1647,7 @@ void MerginApi::updateInfoReplyFinished() if ( r->error() == QNetworkReply::NoError ) { QByteArray data = r->readAll(); - InputUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded project info." ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded project info." ) ); transaction.replyProjectInfo->deleteLater(); transaction.replyProjectInfo = nullptr; @@ -1653,7 +1657,7 @@ void MerginApi::updateInfoReplyFinished() else { QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() ); - InputUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); transaction.replyProjectInfo->deleteLater(); transaction.replyProjectInfo = nullptr; @@ -1682,16 +1686,16 @@ void MerginApi::startProjectUpdate( const QString &projectFullName, const QByteA removeProjectsTempFolder( projectNamespace, projectName ); // project has not been downloaded yet - we need to create a directory for it - transaction.projectDir = InputUtils::createUniqueProjectDirectory( mDataDir, projectName ); + transaction.projectDir = CoreUtils::createUniqueProjectDirectory( mDataDir, projectName ); transaction.firstTimeDownload = true; // create file indicating first time download in progress - QString downloadInProgressFilePath = InputUtils::downloadInProgressFilePath( transaction.projectDir ); + QString downloadInProgressFilePath = CoreUtils::downloadInProgressFilePath( transaction.projectDir ); createPathIfNotExists( downloadInProgressFilePath ); - if ( !InputUtils::createEmptyFile( downloadInProgressFilePath ) ) - InputUtils::log( QStringLiteral( "pull %1" ).arg( projectFullName ), "Unable to create temporary download in progress file" ); + if ( !CoreUtils::createEmptyFile( downloadInProgressFilePath ) ) + CoreUtils::log( QStringLiteral( "pull %1" ).arg( projectFullName ), "Unable to create temporary download in progress file" ); - InputUtils::log( "pull " + projectFullName, QStringLiteral( "First time download - new directory: " ) + transaction.projectDir ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "First time download - new directory: " ) + transaction.projectDir ); } Q_ASSERT( !transaction.projectDir.isEmpty() ); // that would mean we do not have entry -> fail getting local files @@ -1700,13 +1704,13 @@ void MerginApi::startProjectUpdate( const QString &projectFullName, const QByteA MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data ); MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile ); - InputUtils::log( "pull " + projectFullName, QStringLiteral( "Updating from version %1 to version %2" ) - .arg( oldServerProject.version ).arg( serverProject.version ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Updating from version %1 to version %2" ) + .arg( oldServerProject.version ).arg( serverProject.version ) ); transaction.projectMetadata = data; transaction.version = serverProject.version; transaction.diff = compareProjectFiles( oldServerProject.files, serverProject.files, localFiles, transaction.projectDir ); - InputUtils::log( "pull " + projectFullName, transaction.diff.dump() ); + CoreUtils::log( "pull " + projectFullName, transaction.diff.dump() ); for ( QString filePath : transaction.diff.remoteAdded ) { @@ -1777,10 +1781,10 @@ void MerginApi::startProjectUpdate( const QString &projectFullName, const QByteA } transaction.totalSize = totalSize; - InputUtils::log( "pull " + projectFullName, QStringLiteral( "%1 update tasks, %2 items to download (total size %3 bytes)" ) - .arg( transaction.updateTasks.count() ) - .arg( transaction.downloadQueue.count() ) - .arg( transaction.totalSize ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "%1 update tasks, %2 items to download (total size %3 bytes)" ) + .arg( transaction.updateTasks.count() ) + .arg( transaction.downloadQueue.count() ) + .arg( transaction.totalSize ) ); emit pullFilesStarted(); downloadNextItem( projectFullName ); @@ -1838,7 +1842,7 @@ void MerginApi::uploadInfoReplyFinished() if ( r->error() == QNetworkReply::NoError ) { QString url = r->url().toString(); - InputUtils::log( "push " + projectFullName, QStringLiteral( "Downloaded project info." ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Downloaded project info." ) ); QByteArray data = r->readAll(); transaction.replyUploadProjectInfo->deleteLater(); @@ -1855,8 +1859,8 @@ void MerginApi::uploadInfoReplyFinished() // if we're about to do upload? because if not, we need to do local update first if ( projectInfo.isValid() && projectInfo.localVersion != -1 && projectInfo.localVersion < serverProject.version ) { - InputUtils::log( "push " + projectFullName, QStringLiteral( "Need pull first: local version %1 | server version %2" ) - .arg( projectInfo.localVersion ).arg( serverProject.version ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Need pull first: local version %1 | server version %2" ) + .arg( projectInfo.localVersion ).arg( serverProject.version ) ); transaction.updateBeforeUpload = true; startProjectUpdate( projectFullName, data ); return; @@ -1866,7 +1870,7 @@ void MerginApi::uploadInfoReplyFinished() MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile ); transaction.diff = compareProjectFiles( oldServerProject.files, serverProject.files, localFiles, transaction.projectDir ); - InputUtils::log( "push " + projectFullName, transaction.diff.dump() ); + CoreUtils::log( "push " + projectFullName, transaction.diff.dump() ); // TODO: make sure there are no remote files to add/update/remove nor conflicts @@ -1888,8 +1892,11 @@ void MerginApi::uploadInfoReplyFinished() if ( MerginApi::isFileDiffable( filePath ) ) { // try to create a diff - QString diffPath, basePath; - int geodiffRes = GeodiffUtils::createChangeset( transaction.projectDir, filePath, diffPath, basePath ); + QString diffName; + int geodiffRes = GeodiffUtils::createChangeset( transaction.projectDir, filePath, diffName ); + QString diffPath = transaction.projectDir + "/.mergin/" + diffName; + QString basePath = transaction.projectDir + "/.mergin/" + filePath; + if ( geodiffRes == GEODIFF_SUCCESS ) { QByteArray checksumDiff = getChecksum( diffPath ); @@ -1898,7 +1905,7 @@ void MerginApi::uploadInfoReplyFinished() // basefile (because each of them have applied the diff independently) so we have to fake it QByteArray checksumBase = serverProject.fileInfo( filePath ).checksum.toLatin1(); - merginFile.diffName = QgsQuickUtils::getRelativePath( diffPath, transaction.projectDir + "/.mergin/" ); + merginFile.diffName = diffName; merginFile.diffChecksum = QString::fromLatin1( checksumDiff.data(), checksumDiff.size() ); merginFile.diffSize = QFileInfo( diffPath ).size(); merginFile.chunks = generateChunkIdsForSize( merginFile.diffSize ); @@ -1906,12 +1913,12 @@ void MerginApi::uploadInfoReplyFinished() diffFiles.append( merginFile ); - InputUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 successful: total size %2 bytes" ).arg( filePath ).arg( merginFile.diffSize ) ); + CoreUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 successful: total size %2 bytes" ).arg( filePath ).arg( merginFile.diffSize ) ); } else { // TODO: remove the diff file (if exists) - InputUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 FAILED with error %2 (will do full upload)" ).arg( filePath ).arg( geodiffRes ) ); + CoreUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 FAILED with error %2 (will do full upload)" ).arg( filePath ).arg( geodiffRes ) ); } } @@ -1957,8 +1964,8 @@ void MerginApi::uploadInfoReplyFinished() totalSize += file.size; } - InputUtils::log( "push " + projectFullName, QStringLiteral( "%1 items to upload (total size %2 bytes)" ) - .arg( filesToUpload.count() ).arg( totalSize ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "%1 items to upload (total size %2 bytes)" ) + .arg( filesToUpload.count() ).arg( totalSize ) ); transaction.totalSize = totalSize; transaction.uploadQueue = filesToUpload; @@ -1975,7 +1982,7 @@ void MerginApi::uploadInfoReplyFinished() else { QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() ); - InputUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); transaction.replyUploadProjectInfo->deleteLater(); transaction.replyUploadProjectInfo = nullptr; @@ -1999,7 +2006,7 @@ void MerginApi::uploadFinishReplyFinished() { Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); QByteArray data = r->readAll(); - InputUtils::log( "push " + projectFullName, QStringLiteral( "Transaction finish accepted" ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction finish accepted" ) ); transaction.replyUploadFinish->deleteLater(); transaction.replyUploadFinish = nullptr; @@ -2018,13 +2025,14 @@ void MerginApi::uploadFinishReplyFinished() QString sourcePath = transaction.projectDir + "/" + filePath; if ( !QFile::copy( sourcePath, basefile ) ) { - InputUtils::log( "push " + projectFullName, "failed to copy new basefile for: " + filePath ); + CoreUtils::log( "push " + projectFullName, "failed to copy new basefile for: " + filePath ); } } } // clean up diff-related files - for ( const MerginFile &merginFile : qgis::as_const( transaction.uploadDiffFiles ) ) + const auto diffFiles = transaction.uploadDiffFiles; + for ( const MerginFile &merginFile : diffFiles ) { QString diffPath = transaction.projectDir + "/.mergin/" + merginFile.diffName; @@ -2033,16 +2041,16 @@ void MerginApi::uploadFinishReplyFinished() int res = GEODIFF_applyChangeset( basePath.toUtf8(), diffPath.toUtf8() ); if ( res == GEODIFF_SUCCESS ) { - InputUtils::log( "push " + projectFullName, QString( "Applied %1 to base file of %2" ).arg( merginFile.diffName, merginFile.path ) ); + CoreUtils::log( "push " + projectFullName, QString( "Applied %1 to base file of %2" ).arg( merginFile.diffName, merginFile.path ) ); } else { - InputUtils::log( "push " + projectFullName, QString( "Failed to apply changeset %1 to basefile %2 - error %3" ).arg( diffPath ).arg( basePath ).arg( res ) ); + CoreUtils::log( "push " + projectFullName, QString( "Failed to apply changeset %1 to basefile %2 - error %3" ).arg( diffPath ).arg( basePath ).arg( res ) ); } // remove temporary diff files if ( !QFile::remove( diffPath ) ) - InputUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath ); + CoreUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath ); } finishProjectSync( projectFullName, true ); @@ -2051,7 +2059,7 @@ void MerginApi::uploadFinishReplyFinished() { QString serverMsg = extractServerErrorMsg( r->readAll() ); QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "uploadFinish" ), r->errorString(), serverMsg ); - InputUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); transaction.replyUploadFinish->deleteLater(); transaction.replyUploadFinish = nullptr; @@ -2069,13 +2077,13 @@ void MerginApi::uploadCancelReplyFinished() if ( r->error() == QNetworkReply::NoError ) { - InputUtils::log( "push " + projectFullName, QStringLiteral( "Transaction canceled" ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction canceled" ) ); } else { QString serverMsg = extractServerErrorMsg( r->readAll() ); QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "uploadCancel" ), r->errorString(), serverMsg ); - InputUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); } emit uploadCanceled( projectFullName, r->error() == QNetworkReply::NoError ); @@ -2090,7 +2098,7 @@ void MerginApi::getUserInfoFinished() if ( r->error() == QNetworkReply::NoError ) { - InputUtils::log( "user info", QStringLiteral( "Success" ) ); + CoreUtils::log( "user info", QStringLiteral( "Success" ) ); QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); if ( doc.isObject() ) { @@ -2102,7 +2110,7 @@ void MerginApi::getUserInfoFinished() { QString serverMsg = extractServerErrorMsg( r->readAll() ); QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getUserInfo" ), r->errorString(), serverMsg ); - InputUtils::log( "user info", QStringLiteral( "FAILED - %1" ).arg( message ) ); + CoreUtils::log( "user info", QStringLiteral( "FAILED - %1" ).arg( message ) ); mUserInfo->clear(); emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getUserInfo" ) ); } @@ -2352,7 +2360,7 @@ QStringList MerginApi::generateChunkIdsForSize( qint64 fileSize ) QStringList chunks; for ( int i = 0; i < noOfChunks; i++ ) { - QString chunkID = InputUtils::uuidWithoutBraces( QUuid::createUuid() ); + QString chunkID = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); chunks.append( chunkID ); } return chunks; @@ -2412,11 +2420,11 @@ void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSucc // update info of local projects mLocalProjects.updateLocalVersion( transaction.projectDir, transaction.version ); - InputUtils::log( "sync " + projectFullName, QStringLiteral( "### Finished ### New project version: %1\n" ).arg( transaction.version ) ); + CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### Finished ### New project version: %1\n" ).arg( transaction.version ) ); } else { - InputUtils::log( "sync " + projectFullName, QStringLiteral( "### FAILED ###\n" ) ); + CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### FAILED ###\n" ) ); } bool updateBeforeUpload = transaction.updateBeforeUpload; @@ -2427,7 +2435,7 @@ void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSucc if ( updateBeforeUpload ) { - InputUtils::log( "sync " + projectFullName, QStringLiteral( "Continue with push after pull" ) ); + CoreUtils::log( "sync " + projectFullName, QStringLiteral( "Continue with push after pull" ) ); // we're done only with the download part before the actual upload - so let's continue with upload QString projectNamespace, projectName; extractProjectName( projectFullName, projectNamespace, projectName ); @@ -2478,7 +2486,7 @@ void MerginApi::createPathIfNotExists( const QString &filePath ) { if ( !dir.mkpath( newFile.absolutePath() ) ) { - InputUtils::log( "create path", QString( "Creating a folder failed for path: %1" ).arg( filePath ) ); + CoreUtils::log( "create path", QString( "Creating a folder failed for path: %1" ).arg( filePath ) ); } } } @@ -2525,5 +2533,5 @@ QSet MerginApi::listFiles( const QString &path ) DownloadQueueItem::DownloadQueueItem( const QString &fp, int s, int v, int rf, int rt, bool diff ) : filePath( fp ), size( s ), version( v ), rangeFrom( rf ), rangeTo( rt ), downloadDiff( diff ) { - tempFileName = InputUtils::uuidWithoutBraces( QUuid::createUuid() ); + tempFileName = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); } diff --git a/app/merginapi.h b/core/merginapi.h similarity index 99% rename from app/merginapi.h rename to core/merginapi.h index 9d53d9e0a..12ab1c5cf 100644 --- a/app/merginapi.h +++ b/core/merginapi.h @@ -378,6 +378,15 @@ class MerginApi: public QObject bool apiSupportsSubscriptions() const; void setApiSupportsSubscriptions( bool apiSupportsSubscriptions ); + /** + * Sets projectNamespace and projectName from sourceString - url or any string from which takes last (name) + * and the previous of last (namespace) substring after splitting sourceString with slash. + * \param sourceString QString either url or fullname of a project + * \param projectNamespace QString to be set as namespace, might not change original value + * \param projectName QString to be set to name of a project + */ + static bool extractProjectName( const QString &sourceString, QString &projectNamespace, QString &projectName ); + signals: void apiSupportsSubscriptionsChanged(); @@ -480,14 +489,6 @@ class MerginApi: public QObject bool validateAuthAndContinute(); void checkMerginVersion( QString apiVersion, bool serverSupportsSubscriptions, QString msg = QStringLiteral() ); - /** - * Sets projectNamespace and projectName from sourceString - url or any string from which takes last (name) - * and the previous of last (namespace) substring after splitting sourceString with slash. - * \param sourceString QString either url or fullname of a project - * \param projectNamespace QString to be set as namespace, might not change original value - * \param projectName QString to be set to name of a project - */ - bool extractProjectName( const QString &sourceString, QString &projectNamespace, QString &projectName ); /** * Extracts detail (message) of an error json. If its not json or detail cannot be parsed, the whole data are return; * \param data Data received from mergin server on a request failed. diff --git a/app/merginapistatus.cpp b/core/merginapistatus.cpp similarity index 100% rename from app/merginapistatus.cpp rename to core/merginapistatus.cpp diff --git a/app/merginapistatus.h b/core/merginapistatus.h similarity index 100% rename from app/merginapistatus.h rename to core/merginapistatus.h diff --git a/app/merginprojectmetadata.cpp b/core/merginprojectmetadata.cpp similarity index 100% rename from app/merginprojectmetadata.cpp rename to core/merginprojectmetadata.cpp diff --git a/app/merginprojectmetadata.h b/core/merginprojectmetadata.h similarity index 100% rename from app/merginprojectmetadata.h rename to core/merginprojectmetadata.h diff --git a/app/merginprojectstatusmodel.cpp b/core/merginprojectstatusmodel.cpp similarity index 96% rename from app/merginprojectstatusmodel.cpp rename to core/merginprojectstatusmodel.cpp index ff7097835..a8df3d926 100644 --- a/app/merginprojectstatusmodel.cpp +++ b/core/merginprojectstatusmodel.cpp @@ -9,7 +9,7 @@ #include "merginprojectstatusmodel.h" #include "geodiffutils.h" -#include "inpututils.h" +#include "coreutils.h" MerginProjectStatusModel::MerginProjectStatusModel( LocalProjectsManager &localProjects, QObject *parent ) : QAbstractListModel( parent ) @@ -89,7 +89,7 @@ void MerginProjectStatusModel::infoProjectUpdated( const ProjectDiff &projectDif QString summaryJson = GeodiffUtils::diffableFilePendingChanges( projectDir, file, true ); if ( summaryJson.startsWith( "ERROR" ) ) { - InputUtils::log( "MerginProjectStatusModel", QString( "Diff summary JSON for %1 in %2 has an error." ).arg( projectDir ).arg( file ) ); + CoreUtils::log( "MerginProjectStatusModel", QString( "Diff summary JSON for %1 in %2 has an error." ).arg( projectDir ).arg( file ) ); ProjectStatusItem item; item.status = ProjectChangelogStatus::Message; diff --git a/app/merginprojectstatusmodel.h b/core/merginprojectstatusmodel.h similarity index 100% rename from app/merginprojectstatusmodel.h rename to core/merginprojectstatusmodel.h diff --git a/app/merginsecrets.cpp.enc b/core/merginsecrets.cpp.enc similarity index 100% rename from app/merginsecrets.cpp.enc rename to core/merginsecrets.cpp.enc diff --git a/app/merginsubscriptionstatus.cpp b/core/merginsubscriptionstatus.cpp similarity index 100% rename from app/merginsubscriptionstatus.cpp rename to core/merginsubscriptionstatus.cpp diff --git a/app/merginsubscriptionstatus.h b/core/merginsubscriptionstatus.h similarity index 100% rename from app/merginsubscriptionstatus.h rename to core/merginsubscriptionstatus.h diff --git a/app/merginsubscriptiontype.cpp b/core/merginsubscriptiontype.cpp similarity index 100% rename from app/merginsubscriptiontype.cpp rename to core/merginsubscriptiontype.cpp diff --git a/app/merginsubscriptiontype.h b/core/merginsubscriptiontype.h similarity index 100% rename from app/merginsubscriptiontype.h rename to core/merginsubscriptiontype.h diff --git a/app/merginuserauth.cpp b/core/merginuserauth.cpp similarity index 100% rename from app/merginuserauth.cpp rename to core/merginuserauth.cpp diff --git a/app/merginuserauth.h b/core/merginuserauth.h similarity index 100% rename from app/merginuserauth.h rename to core/merginuserauth.h diff --git a/app/merginuserinfo.cpp b/core/merginuserinfo.cpp similarity index 96% rename from app/merginuserinfo.cpp rename to core/merginuserinfo.cpp index 14a2cbdd4..4acc1a410 100644 --- a/app/merginuserinfo.cpp +++ b/core/merginuserinfo.cpp @@ -8,7 +8,7 @@ ***************************************************************************/ #include "merginuserinfo.h" -#include "inpututils.h" +#include "coreutils.h" MerginUserInfo::MerginUserInfo( QObject *parent ) : QObject( parent ) @@ -67,11 +67,11 @@ void MerginUserInfo::setFromJson( QJsonObject docObj ) QString validUntil = subscriptionObj.value( QStringLiteral( "valid_until" ) ).toString(); if ( nextPaymentDate.isEmpty() ) { - mSubscriptionTimestamp = InputUtils::localizedDateFromUTFString( validUntil ); + mSubscriptionTimestamp = CoreUtils::localizedDateFromUTFString( validUntil ); } else { - mSubscriptionTimestamp = InputUtils::localizedDateFromUTFString( nextPaymentDate ); + mSubscriptionTimestamp = CoreUtils::localizedDateFromUTFString( nextPaymentDate ); } mSubscriptionId = subscriptionObj.value( QStringLiteral( "id" ) ).toInt(); diff --git a/app/merginuserinfo.h b/core/merginuserinfo.h similarity index 100% rename from app/merginuserinfo.h rename to core/merginuserinfo.h diff --git a/app/project.cpp b/core/project.cpp similarity index 91% rename from app/project.cpp rename to core/project.cpp index f95332d22..d59ab23b9 100644 --- a/app/project.cpp +++ b/core/project.cpp @@ -9,7 +9,7 @@ #include "project.h" #include "merginapi.h" -#include "inpututils.h" +#include "coreutils.h" QString LocalProject::id() const { @@ -42,10 +42,10 @@ ProjectStatus::Status ProjectStatus::projectStatus( const std::shared_ptrlocal->projectDir + "/" + MerginApi::sMetadataFile; - QDateTime lastModified = InputUtils::getLastModifiedFileDateTime( project->local->projectDir ); + QDateTime lastModified = CoreUtils::getLastModifiedFileDateTime( project->local->projectDir ); QDateTime lastSync = QFileInfo( metadataFilePath ).lastModified(); MerginProjectMetadata meta = MerginProjectMetadata::fromCachedJson( metadataFilePath ); - int filesCount = InputUtils::getProjectFilesCount( project->local->projectDir ); + int filesCount = CoreUtils::getProjectFilesCount( project->local->projectDir ); if ( lastSync < lastModified || meta.files.count() != filesCount ) { return ProjectStatus::Modified; diff --git a/app/project.h b/core/project.h similarity index 100% rename from app/project.h rename to core/project.h diff --git a/docs/developers/index.md b/docs/developers/index.md index 8122e0f97..5b39c2a3a 100644 --- a/docs/developers/index.md +++ b/docs/developers/index.md @@ -7,6 +7,9 @@ Build and run Input on various platforms: - [Android](./android.md) - [Windows](./win.md) +Build and run Mergin Client +- [Mergin CPP Client](./client.md) + Other topics: - [Translations](./translations.md) - [Publishing](./publishing.md) diff --git a/scripts/check_all.bash b/scripts/check_all.bash index 737be4cdf..f28c6ce39 100755 --- a/scripts/check_all.bash +++ b/scripts/check_all.bash @@ -5,5 +5,5 @@ set -e DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" PWD=`pwd` cd $DIR -./astyle.bash `find ../app ../qgsquick/from_qgis -name \*.mm* -print -o -name \*.h* -print -o -name \*.c* -print` +./astyle.bash `find ../client ../core ../app ../qgsquick/from_qgis -name \*.mm* -print -o -name \*.h* -print -o -name \*.c* -print` cd $PWD From 39175a55c41ac685f6bbf18f55e0d6bcb6d2caf8 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Fri, 9 Apr 2021 10:02:07 +0200 Subject: [PATCH 31/53] refresh project list on panel opened --- app/qml/ProjectPanel.qml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/qml/ProjectPanel.qml b/app/qml/ProjectPanel.qml index e0774f153..73b886ab3 100644 --- a/app/qml/ProjectPanel.qml +++ b/app/qml/ProjectPanel.qml @@ -234,7 +234,19 @@ Item { onStateChanged: { refreshProjectList() - console.log("New state: ", pageContent.state) + pageFooter.setActiveButton( pageContent.state ) + } + + Connections { + target: root + onVisibleChanged: { + if ( root.visible ) { // projectsPanel opened + pageContent.state = "local" + } + else { + pageContent.state = "" + } + } } StackLayout { @@ -298,6 +310,15 @@ Item { property int itemSize: pageFooter.height * 0.8 + function setActiveButton( state ) { + switch( state ) { + case "local": pageFooter.setCurrentIndex( 0 ); break + case "created": pageFooter.setCurrentIndex( 1 ); break + case "shared": pageFooter.setCurrentIndex( 2 ); break + case "public": pageFooter.setCurrentIndex( 3 ); break + } + } + spacing: 0 contentHeight: InputStyle.rowHeightHeader From 8302341852c5a11e34e446645a379d782ed1cc05 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Fri, 9 Apr 2021 10:34:05 +0200 Subject: [PATCH 32/53] bump versions to 0.9.3 --- app/ios/Info.plist | 2 +- app/version.pri | 2 +- scripts/version.cmd | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/ios/Info.plist b/app/ios/Info.plist index a7781474a..406841124 100644 --- a/app/ios/Info.plist +++ b/app/ios/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.9.2 + 0.9.3 CFBundleSignature ${QMAKE_PKGINFO_TYPEINFO} CFBundleVersion diff --git a/app/version.pri b/app/version.pri index 9aaf9596f..3dd75ae81 100644 --- a/app/version.pri +++ b/app/version.pri @@ -1,6 +1,6 @@ VERSION_MAJOR = 0 VERSION_MINOR = 9 -VERSION_FIX = 2 +VERSION_FIX = 3 INPUT_VERSION = '$${VERSION_MAJOR}.$${VERSION_MINOR}.$${VERSION_FIX}' diff --git a/scripts/version.cmd b/scripts/version.cmd index feecb7703..805e5efaf 100644 --- a/scripts/version.cmd +++ b/scripts/version.cmd @@ -1,3 +1,3 @@ set VERSIONMAJOR=0 set VERSIONMINOR=9 -set VERSIONBUILD=2 +set VERSIONBUILD=3 From e0c4c1b8adaf1495f10ce08cb43b7754bee54d90 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Fri, 9 Apr 2021 11:06:50 +0200 Subject: [PATCH 33/53] clear created/shared models when user logs out --- app/projectsmodel.cpp | 14 ++++++++++++++ app/projectsmodel.h | 1 + 2 files changed, 15 insertions(+) diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index 72ff4ec75..7995976f9 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -28,6 +28,7 @@ void ProjectsModel::initializeProjectsModel() QObject::connect( mBackend, &MerginApi::syncProjectFinished, this, &ProjectsModel::onProjectSyncFinished ); QObject::connect( mBackend, &MerginApi::projectDetached, this, &ProjectsModel::onProjectDetachedFromMergin ); QObject::connect( mBackend, &MerginApi::projectAttachedToMergin, this, &ProjectsModel::onProjectAttachedToMergin ); + QObject::connect( mBackend, &MerginApi::authChanged, this, &ProjectsModel::onAuthChanged ); if ( mModelType == ProjectModelTypes::LocalProjectsModel ) { @@ -553,6 +554,19 @@ void ProjectsModel::onProjectAttachedToMergin( const QString &projectFullName ) qDebug() << "PMR: Project attached to mergin " << projectFullName; } +void ProjectsModel::onAuthChanged() +{ + if ( !mBackend->userAuth() || !mBackend->userAuth()->hasAuthData() ) // user logged out, clear created and shared lists + { + if ( mModelType == CreatedProjectsModel || mModelType == SharedProjectsModel ) + { + beginResetModel(); + mProjects.clear(); + endResetModel(); + } + } +} + void ProjectsModel::setMerginApi( MerginApi *merginApi ) { if ( !merginApi || mBackend == merginApi ) diff --git a/app/projectsmodel.h b/app/projectsmodel.h index 3f6288f09..78ea4291c 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -121,6 +121,7 @@ class ProjectsModel : public QAbstractListModel void onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ); void onProjectDetachedFromMergin( const QString &projectFullName ); void onProjectAttachedToMergin( const QString &projectFullName ); + void onAuthChanged(); // when user logs out // LocalProjectsManager signals void onProjectAdded( const LocalProject &project ); From 96e2d5d65baeba95fa660e08619b6b6ca8b01468 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Fri, 9 Apr 2021 11:08:24 +0200 Subject: [PATCH 34/53] remove last __merginapi qml call to list projects --- app/qml/ProjectListPage.qml | 8 +++++--- app/qml/ProjectPanel.qml | 13 +++++++------ app/qml/components/ProjectList.qml | 5 ++--- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/qml/ProjectListPage.qml b/app/qml/ProjectListPage.qml index 947e235c5..21110923e 100644 --- a/app/qml/ProjectListPage.qml +++ b/app/qml/ProjectListPage.qml @@ -23,9 +23,11 @@ Item { signal openProjectRequested( string projectId, string projectFilePath ) signal showLocalChangesRequested( string projectId ) - function refreshProjectsList() { - searchBar.deactivate() - projectlist.refreshProjectList() + function refreshProjectsList( keepSearchFilter = false ) { + if ( !keepSearchFilter ) + searchBar.deactivate() + + projectlist.refreshProjectList( searchBar.text ) } SearchBar { diff --git a/app/qml/ProjectPanel.qml b/app/qml/ProjectPanel.qml index 73b886ab3..43319c314 100644 --- a/app/qml/ProjectPanel.qml +++ b/app/qml/ProjectPanel.qml @@ -121,21 +121,21 @@ Item { else __inputUtils.showNotification( qsTr( "No Changes" ) ) } - function refreshProjectList() { + function refreshProjectList( keepSearchFilter = false ) { stackView.pending = true switch( pageContent.state ) { case "local": - localProjectsPage.refreshProjectsList() + localProjectsPage.refreshProjectsList( keepSearchFilter ) break case "created": - createdProjectsPage.refreshProjectsList() + createdProjectsPage.refreshProjectsList( keepSearchFilter ) break case "shared": - sharedProjectsPage.refreshProjectsList() + sharedProjectsPage.refreshProjectsList( keepSearchFilter ) break case "public": - publicProjectsPage.refreshProjectsList() + publicProjectsPage.refreshProjectsList( keepSearchFilter ) break } } @@ -506,8 +506,9 @@ Item { anchors.horizontalCenter: parent.horizontalCenter onClicked: { stackView.pending = true + // filters suppose to not change - __merginApi.listProjects( searchBar.text ) + projectsPage.refreshProjectList( true ) reloadList.visible = false } background: Rectangle { diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml index 499ddbefa..9e1816c70 100644 --- a/app/qml/components/ProjectList.qml +++ b/app/qml/components/ProjectList.qml @@ -32,8 +32,8 @@ Item { else viewModel.searchExpression = searchText } - function refreshProjectList() { - controllerModel.listProjects() + function refreshProjectList( searchText ) { + controllerModel.listProjects( searchText ) } function modelData( fromRole, fromValue, desiredRole ) { @@ -129,7 +129,6 @@ Item { stackView.push(projectWizardComp) } else if ( __inputUtils.acquireStoragePermission() ) { -// TODO: reload project dir ~> rather connect to permission granted signal and reload project dir restartAppDialog.open() } } From 1bd70b659e861c489135c8ce4d64944501278c93 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Fri, 9 Apr 2021 13:16:12 +0200 Subject: [PATCH 35/53] add ping mergin --- app/qml/ProjectPanel.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/qml/ProjectPanel.qml b/app/qml/ProjectPanel.qml index 43319c314..eefd55ec4 100644 --- a/app/qml/ProjectPanel.qml +++ b/app/qml/ProjectPanel.qml @@ -233,6 +233,7 @@ Item { ] onStateChanged: { + __merginApi.pingMergin() refreshProjectList() pageFooter.setActiveButton( pageContent.state ) } From 3fac84b5db8ddb648dc66f6362b0ec347bd50012 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Fri, 9 Apr 2021 14:17:51 +0200 Subject: [PATCH 36/53] add texts --- app/qml/ProjectListPage.qml | 2 +- app/qml/ProjectPanel.qml | 76 ------------------------------ app/qml/components/ProjectList.qml | 62 ++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 77 deletions(-) diff --git a/app/qml/ProjectListPage.qml b/app/qml/ProjectListPage.qml index 21110923e..5a7dfe50c 100644 --- a/app/qml/ProjectListPage.qml +++ b/app/qml/ProjectListPage.qml @@ -7,7 +7,7 @@ * * ***************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.12 import lc 1.0 import "./components" diff --git a/app/qml/ProjectPanel.qml b/app/qml/ProjectPanel.qml index eefd55ec4..b059d884b 100644 --- a/app/qml/ProjectPanel.qml +++ b/app/qml/ProjectPanel.qml @@ -412,82 +412,6 @@ Item { } } -//------------------------------------------------- -// Text { -// id: noProjectsText -// anchors.fill: parent -// textFormat: Text.RichText -// text: "" + -// qsTr("No downloaded projects found.%1Learn %2how to create projects%3 and %4download them%3 onto your device.") -// .arg("
") -// .arg("") -// .arg("") -// .arg("") - -// onLinkActivated: Qt.openUrlExternally(link) -// visible: grid.count === 0 && !storagePermissionText.visible -// color: InputStyle.fontColor -// font.pixelSize: InputStyle.fontPixelSizeNormal -// font.bold: true -// verticalAlignment: Text.AlignVCenter -// horizontalAlignment: Text.AlignHCenter -// wrapMode: Text.WordWrap -// padding: InputStyle.panelMargin/2 -// } - -// Text { -// id: storagePermissionText -// anchors.fill: parent -// textFormat: Text.RichText -// text: "" + -// qsTr("Input needs a storage permission, %1click to grant it%2 and then restart application.") -// .arg("") -// .arg("") - -// onLinkActivated: { -// if ( __inputUtils.acquireStoragePermission() ) { -// restartAppDialog.open() -// } -// } -// visible: !__inputUtils.hasStoragePermission() -// color: InputStyle.fontColor -// font.pixelSize: InputStyle.fontPixelSizeNormal -// font.bold: true -// verticalAlignment: Text.AlignVCenter -// horizontalAlignment: Text.AlignHCenter -// wrapMode: Text.WordWrap -// padding: InputStyle.panelMargin/2 -// } -// } -//------------------------------------------------- - -//------------------------------------------------- -// TODO: unable to get list of the projects -// Label { -// anchors.fill: parent -// horizontalAlignment: Qt.AlignHCenter -// verticalAlignment: Qt.AlignVCenter -// visible: !merginProjectsList.contentHeight -// text: reloadList.visible ? qsTr("Unable to get the list of projects.") : qsTr("No projects found!") -// color: InputStyle.fontColor -// font.pixelSize: InputStyle.fontPixelSizeNormal -// font.bold: true -// } -//------------------------------------------------- - -// Toolbar -//------------------------------------------------- - -// onHighlightedChanged: { -//// searchBar.deactivate() -// if (toolbar.highlighted === homeBtn.text) { -//// __projectsModel.searchExpression = "" -// } else { -// __merginApi.pingMergin() <------------------!!! -// } -// } -//------------------------------------------------- - // Other components Item { diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml index 9e1816c70..33d120ec5 100644 --- a/app/qml/components/ProjectList.qml +++ b/app/qml/components/ProjectList.qml @@ -54,6 +54,7 @@ Item { anchors.fill: parent clip: true maximumFlickVelocity: __androidUtils.isAndroid ? InputStyle.scrollVelocityAndroid : maximumFlickVelocity + visible: !storagePermissionText.visible // Proxy model with source projects model model: ProjectsProxyModel { @@ -135,6 +136,67 @@ Item { } } + Text { + id: storagePermissionText + + anchors.fill: parent + textFormat: Text.RichText + text: "" + + qsTr("Input needs a storage permission, %1click to grant it%2 and then restart application.") + .arg("") + .arg("") + + onLinkActivated: { + if ( __inputUtils.acquireStoragePermission() ) { + restartAppDialog.open() + } + } + visible: !__inputUtils.hasStoragePermission() + color: InputStyle.fontColor + font.pixelSize: InputStyle.fontPixelSizeNormal + font.bold: true + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + padding: InputStyle.panelMargin/2 + } + + Text { + id: noLocalProjectsText + + anchors.fill: parent + textFormat: Text.RichText + text: "" + + qsTr("No downloaded projects found.%1Learn %2how to create projects%3 and %4download them%3 onto your device.") + .arg("
") + .arg("") + .arg("") + .arg("") + + onLinkActivated: Qt.openUrlExternally(link) + visible: listview.count === 0 && !storagePermissionText.visible && projectModelType === ProjectsModel.LocalProjectsModel + color: InputStyle.fontColor + font.pixelSize: InputStyle.fontPixelSizeNormal + font.bold: true + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + padding: InputStyle.panelMargin/2 + } + + Label { + id: noMerginProjectsTexts + + anchors.fill: parent + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + visible: reloadList.visible || ( projectModelType !== ProjectsModel.LocalProjectsModel && listview.count === 0 ) + text: reloadList.visible ? qsTr("Unable to get the list of projects.") : qsTr("No projects found!") + color: InputStyle.fontColor + font.pixelSize: InputStyle.fontPixelSizeNormal + font.bold: true + } + MessageDialog { id: removeDialog property string relatedProjectId From 889424d51732b7ffb4c7bcc71027b547fb8f3030 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Fri, 9 Apr 2021 14:40:07 +0200 Subject: [PATCH 37/53] redirections with user auth --- app/qml/ProjectPanel.qml | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/app/qml/ProjectPanel.qml b/app/qml/ProjectPanel.qml index b059d884b..8b0c85929 100644 --- a/app/qml/ProjectPanel.qml +++ b/app/qml/ProjectPanel.qml @@ -27,6 +27,7 @@ Item { property real panelMargin: InputStyle.panelMargin signal openProjectRequested( string projectId, string projectPath ) + signal resetView() // resets view to state as when panel is opened function openPanel() { root.visible = true @@ -179,7 +180,6 @@ Item { if (__merginApi.userAuth.hasAuthData() && __merginApi.apiVersionStatus === MerginApiStatus.OK) { __merginApi.getUserInfo() stackView.push( accountPanelComp ) - reloadList.visible = false } else stackView.push( authPanelComp, { state: "login" }) @@ -248,6 +248,11 @@ Item { pageContent.state = "" } } + + onResetView: { + if ( pageContent.state === "created" || pageContent.state === "shared" ) + pageContent.state = "local" + } } StackLayout { @@ -487,18 +492,13 @@ Item { } onAuthChanged: { stackView.pending = false - if (__merginApi.userAuth.hasAuthData()) { - refreshProjectList() + if ( __merginApi.userAuth.hasAuthData() ) { + stackView.popOnePageOrClose() + projectsPage.refreshProjectList() root.forceActiveFocus() } - stackView.popOnePageOrClose() - - } - onAuthFailed: { - homeBtn.activated() - stackView.pending = false - root.forceActiveFocus() } + onAuthFailed: stackView.pending = false onRegistrationFailed: stackView.pending = false onRegistrationSucceeded: stackView.pending = false @@ -519,8 +519,8 @@ Item { toolbarHeight: InputStyle.rowHeightHeader onBack: { stackView.popOnePageOrClose() - if (stackView.currentItem.objectName === "projectsPanel") { - __merginApi.authFailed() // activate homeBtn + if ( !__merginApi.userAuth.hasAuthData() ) { + root.resetView() } } } @@ -556,10 +556,11 @@ Item { } } onSignOutClicked: { - if (__merginApi.userAuth.hasAuthData()) { + if ( __merginApi.userAuth.hasAuthData() ) { __merginApi.clearAuth() - stackView.popOnePageOrClose() } + stackView.popOnePageOrClose() + root.resetView() } onRestorePurchasesClicked: { __purchasing.restore() From 46c01cbf8afb9d8b3254abd0a1323851485ad35b Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Fri, 9 Apr 2021 14:40:53 +0200 Subject: [PATCH 38/53] move reload list to project list --- app/qml/ProjectPanel.qml | 41 --------------------------- app/qml/components/ProjectList.qml | 45 ++++++++++++++++++++++++++++++ core/merginapi.cpp | 2 ++ 3 files changed, 47 insertions(+), 41 deletions(-) diff --git a/app/qml/ProjectPanel.qml b/app/qml/ProjectPanel.qml index 8b0c85929..eb7352c49 100644 --- a/app/qml/ProjectPanel.qml +++ b/app/qml/ProjectPanel.qml @@ -419,43 +419,6 @@ Item { // Other components - Item { - id: reloadList - width: parent.width - height: root.rowHeight - visible: false - Layout.alignment: Qt.AlignVCenter - y: root.height/3 * 2 - - Button { - id: reloadBtn - width: reloadList.width - 2* InputStyle.panelMargin - height: reloadList.height - text: qsTr("Retry") - font.pixelSize: reloadBtn.height/2 - anchors.horizontalCenter: parent.horizontalCenter - onClicked: { - stackView.pending = true - - // filters suppose to not change - projectsPage.refreshProjectList( true ) - reloadList.visible = false - } - background: Rectangle { - color: InputStyle.highlightColor - } - - contentItem: Text { - text: reloadBtn.text - font: reloadBtn.font - color: InputStyle.clrPanelMain - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - } - } - Connections { target: __projectWizard onProjectCreated: { @@ -470,9 +433,6 @@ Item { onListProjectsFinished: { stackView.pending = false } - onListProjectsFailed: { - reloadList.visible = true - } onListProjectsByNameFinished: stackView.pending = false onApiVersionStatusChanged: { stackView.pending = false @@ -597,5 +557,4 @@ Item { } } } - } diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml index 33d120ec5..45f52c27a 100644 --- a/app/qml/components/ProjectList.qml +++ b/app/qml/components/ProjectList.qml @@ -197,6 +197,51 @@ Item { font.bold: true } + Item { + id: reloadList + + width: parent.width + height: InputStyle.rowHeightHeader + visible: false + y: root.height/3 * 2 + + Connections { + target: __merginApi + + onListProjectsFailed: reloadList.visible = root.projectModelType !== ProjectsModel.LocalProjectsModel // show reload list to all models except local + onListProjectsFinished: reloadList.visible = false // hide reload list on other models + } + + Button { + id: reloadBtn + width: reloadList.width - 2* InputStyle.panelMargin + height: reloadList.height + text: qsTr("Retry") + font.pixelSize: reloadBtn.height/2 + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + stackView.pending = true + + // filters suppose to not change + projectsPage.refreshProjectList( true ) + reloadList.visible = false + } + background: Rectangle { + color: InputStyle.highlightColor + radius: InputStyle.cornerRadius + } + + contentItem: Text { + text: reloadBtn.text + font: reloadBtn.font + color: InputStyle.clrPanelMain + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + } + } + MessageDialog { id: removeDialog property string relatedProjectId diff --git a/core/merginapi.cpp b/core/merginapi.cpp index 56bc31e63..c85c1c3e8 100644 --- a/core/merginapi.cpp +++ b/core/merginapi.cpp @@ -1270,6 +1270,8 @@ void MerginApi::listProjectsByNameReplyFinished( QString requestId ) QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjectsByName" ), r->errorString(), serverMsg ); emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjectsByName" ) ); CoreUtils::log( "list projects by name", QStringLiteral( "FAILED - %1" ).arg( message ) ); + + emit listProjectsFailed(); } r->deleteLater(); From 2cd17bf073ea134fa88f44d289540e165c6b7157 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Fri, 9 Apr 2021 14:57:40 +0200 Subject: [PATCH 39/53] add download project dialog --- app/qml/components/ProjectList.qml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml index 45f52c27a..2cf67817a 100644 --- a/app/qml/components/ProjectList.qml +++ b/app/qml/components/ProjectList.qml @@ -92,7 +92,14 @@ Item { viewContentY: ListView.view.contentY viewHeight: ListView.view.height - onOpenRequested: root.openProjectRequested( projectId, model.ProjectFilePath ) + onOpenRequested: { + if ( model.ProjectIsLocal ) + root.openProjectRequested( projectId, model.ProjectFilePath ) + else if ( !model.ProjectIsLocal && model.ProjectIsMergin ) { + downloadProjectDialog.relatedProjectId = model.ProjectId + downloadProjectDialog.open() + } + } onSyncRequested: controllerModel.syncProject( projectId ) onMigrateRequested: controllerModel.migrateProject( projectId ) onRemoveRequested: { @@ -244,6 +251,7 @@ Item { MessageDialog { id: removeDialog + property string relatedProjectId title: qsTr( "Remove project" ) @@ -272,6 +280,18 @@ Item { } } + MessageDialog { + id: downloadProjectDialog + + property string relatedProjectId + + title: qsTr( "Download project" ) + text: qsTr( "Would you like to download the project\n %1 ?" ).arg( relatedProjectId ) + icon: StandardIcon.Question + standardButtons: StandardButton.Yes | StandardButton.No + onYes: controllerModel.syncProject( relatedProjectId ) + } + MessageDialog { id: restartAppDialog From 21a0182821bd0c815bb1490b3f0ab1a0a934c201 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Sat, 10 Apr 2021 19:03:52 +0200 Subject: [PATCH 40/53] fill more menu dynamically --- app/qml/components/ProjectDelegateItem.qml | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/app/qml/components/ProjectDelegateItem.qml b/app/qml/components/ProjectDelegateItem.qml index 081bf3812..d4f62219f 100644 --- a/app/qml/components/ProjectDelegateItem.qml +++ b/app/qml/components/ProjectDelegateItem.qml @@ -122,21 +122,16 @@ Rectangle { let items = getMoreMenuItems() items = items.split(',') - if ( contextMenu.contentData ) - while( contextMenu.contentData.length > 0 ) - contextMenu.takeItem( 0 ); + // clear previous items + while( contextMenu.count > 0 ) + contextMenu.takeItem( 0 ); items.forEach( item => { contextMenu.addItem( - menuItemComponent.createObject( null, itemsMap[item] ) ) + menuItemComponent.createObject( contextMenu, itemsMap[item] ) ) }) contextMenu.height = items.length * root.menuItemHeight } - onProjectStatusChanged: { - console.log("Project " + projectId + " status changed to: " + projectStatus) - fillMoreMenu() - } - color: root.highlight || !projectIsValid ? InputStyle.panelItemHighlight : root.primaryColor MouseArea { @@ -296,7 +291,10 @@ Rectangle { MouseArea { anchors.fill: parent - onClicked: contextMenu.open() + onClicked: { + fillMoreMenu() + contextMenu.open() + } } } } @@ -327,8 +325,6 @@ Rectangle { } } - Component.onCompleted: fillMoreMenu() - //! sets y-offset either above or below related item according relative position to end of the list onAboutToShow: { let itemRelativeY = parent.y - root.viewContentY From 6c4b95c7b0fc5c4968ae60160ffe5e791d0cb1ad Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Sat, 10 Apr 2021 19:07:24 +0200 Subject: [PATCH 41/53] handle storage permission --- app/qml/components/ProjectList.qml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml index 2cf67817a..1e150b576 100644 --- a/app/qml/components/ProjectList.qml +++ b/app/qml/components/ProjectList.qml @@ -100,7 +100,14 @@ Item { downloadProjectDialog.open() } } - onSyncRequested: controllerModel.syncProject( projectId ) + onSyncRequested: { + if ( __inputUtils.hasStoragePermission() ) { + controllerModel.syncProject( projectId ) + } + else if ( __inputUtils.acquireStoragePermission() ) { + restartAppDialog.open() + } + } onMigrateRequested: controllerModel.migrateProject( projectId ) onRemoveRequested: { removeDialog.relatedProjectId = projectId @@ -289,7 +296,14 @@ Item { text: qsTr( "Would you like to download the project\n %1 ?" ).arg( relatedProjectId ) icon: StandardIcon.Question standardButtons: StandardButton.Yes | StandardButton.No - onYes: controllerModel.syncProject( relatedProjectId ) + onYes: { + if ( __inputUtils.hasStoragePermission() ) { + controllerModel.syncProject( relatedProjectId ) + } + else if ( __inputUtils.acquireStoragePermission() ) { + restartAppDialog.open() + } + } } MessageDialog { From 0baaff0d50e36f215e5f9854b12b0bd45b5ab66e Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Sat, 10 Apr 2021 19:08:34 +0200 Subject: [PATCH 42/53] update no local projects screen --- app/qml/components/ProjectList.qml | 73 +++++++++++++++++++++------- app/qml/components/RichTextBlock.qml | 15 ++++++ app/qml/qml.qrc | 1 + 3 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 app/qml/components/RichTextBlock.qml diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml index 1e150b576..b015042a8 100644 --- a/app/qml/components/ProjectList.qml +++ b/app/qml/components/ProjectList.qml @@ -10,6 +10,7 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.12 import lc 1.0 import "../" import "." @@ -138,6 +139,7 @@ Item { width: parent.width height: InputStyle.rowHeight text: qsTr("Create project") + visible: listview.count > 0 onClicked: { if ( __inputUtils.hasStoragePermission() ) { @@ -175,27 +177,62 @@ Item { padding: InputStyle.panelMargin/2 } - Text { - id: noLocalProjectsText + Item { + id: noLocalProjectsMessageContainer + + visible: listview.count === 0 && !storagePermissionText.visible && projectModelType === ProjectsModel.LocalProjectsModel anchors.fill: parent - textFormat: Text.RichText - text: "" + - qsTr("No downloaded projects found.%1Learn %2how to create projects%3 and %4download them%3 onto your device.") - .arg("
") - .arg("
") - .arg("") - .arg("") - onLinkActivated: Qt.openUrlExternally(link) - visible: listview.count === 0 && !storagePermissionText.visible && projectModelType === ProjectsModel.LocalProjectsModel - color: InputStyle.fontColor - font.pixelSize: InputStyle.fontPixelSizeNormal - font.bold: true - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - padding: InputStyle.panelMargin/2 + ColumnLayout { + id: colayout + + anchors.fill: parent + spacing: 0 + + RichTextBlock { + id: noLocalProjectsText + + Layout.fillHeight: true + Layout.fillWidth: true + + text: "" + + qsTr( "No downloaded projects found.%1Learn %2how to create projects%3 and %4download them%3 onto your device." ) + .arg("
") + .arg("
") + .arg("") + .arg("") + + onLinkActivated: Qt.openUrlExternally(link) + } + + + RichTextBlock { + id: createProjectText + + Layout.fillHeight: true + Layout.fillWidth: true + + text: qsTr( "You can also create new project by clicking button below." ) + } + + DelegateButton { + id: createdProjectsWhenNone + + Layout.preferredHeight: InputStyle.rowHeight + Layout.fillWidth: true + + text: qsTr( "Create project" ) + onClicked: { + if ( __inputUtils.hasStoragePermission() ) { + stackView.push(projectWizardComp) + } + else if ( __inputUtils.acquireStoragePermission() ) { + restartAppDialog.open() + } + } + } + } } Label { diff --git a/app/qml/components/RichTextBlock.qml b/app/qml/components/RichTextBlock.qml new file mode 100644 index 000000000..351e41c30 --- /dev/null +++ b/app/qml/components/RichTextBlock.qml @@ -0,0 +1,15 @@ +import QtQuick 2.12 +import "../" + +Text { + id: root + + textFormat: Text.RichText + color: InputStyle.fontColor + font.pixelSize: InputStyle.fontPixelSizeNormal + font.bold: true + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + padding: InputStyle.panelMargin/2 +} diff --git a/app/qml/qml.qrc b/app/qml/qml.qrc index ddada93c7..ac33f5ab0 100644 --- a/app/qml/qml.qrc +++ b/app/qml/qml.qrc @@ -65,5 +65,6 @@ components/Symbol.qml components/ProjectDelegateItem.qml components/ProjectList.qml + components/RichTextBlock.qml From caf4103ca48a3d68b86fd97ebc255ef0c818e91a Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Sat, 10 Apr 2021 21:11:23 +0200 Subject: [PATCH 43/53] show no projects only when model finished loading --- app/projectsmodel.cpp | 21 +++++++++++++++++++++ app/projectsmodel.h | 13 +++++++++++-- app/qml/components/ProjectList.qml | 2 +- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index 7995976f9..0bb684ee7 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -150,6 +150,9 @@ void ProjectsModel::listProjects( const QString &searchExpression, int page ) } mLastRequestId = mBackend->listProjects( searchExpression, modelTypeToFlag(), "", page ); + + if ( !mLastRequestId.isEmpty() && page == 1 ) + emit setModelIsLoading( true ); } void ProjectsModel::listProjectsByName() @@ -160,6 +163,9 @@ void ProjectsModel::listProjectsByName() } mLastRequestId = mBackend->listProjectsByName( projectNames() ); + + if ( !mLastRequestId.isEmpty() ) + emit setModelIsLoading( true ); } bool ProjectsModel::hasMoreProjects() const @@ -238,6 +244,8 @@ void ProjectsModel::onListProjectsFinished( const MerginProjectsList &merginProj mServerProjectsCount = projectsCount; mPaginatedPage = page; emit hasMoreProjectsChanged(); + + emit setModelIsLoading( false ); } void ProjectsModel::onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ) @@ -253,6 +261,8 @@ void ProjectsModel::onListProjectsByNameFinished( const MerginProjectsList &merg mergeProjects( merginProjects, pendingProjects ); printProjects(); endResetModel(); + + emit setModelIsLoading( false ); } void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious ) @@ -673,6 +683,17 @@ std::shared_ptr ProjectsModel::projectFromId( QString projectId ) const return nullptr; } +bool ProjectsModel::isLoading() const +{ + return mModelIsLoading; +} + +void ProjectsModel::setModelIsLoading( bool state ) +{ + mModelIsLoading = state; + emit isLoadingChanged( mModelIsLoading ); +} + ProjectsModel::ProjectModelTypes ProjectsModel::modelType() const { return mModelType; diff --git a/app/projectsmodel.h b/app/projectsmodel.h index 78ea4291c..49fee8872 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -69,6 +69,7 @@ class ProjectsModel : public QAbstractListModel Q_PROPERTY( ProjectModelTypes modelType READ modelType WRITE setModelType ) Q_PROPERTY( bool hasMoreProjects READ hasMoreProjects NOTIFY hasMoreProjectsChanged ) + Q_PROPERTY( bool isLoading READ isLoading NOTIFY isLoadingChanged ) // Needed methods from QAbstractListModel Q_INVOKABLE QVariant data( const QModelIndex &index, int role ) const override; @@ -113,7 +114,11 @@ class ProjectsModel : public QAbstractListModel std::shared_ptr projectFromId( QString projectId ) const; - public slots: + bool isLoading() const; + + void setModelIsLoading( bool state ); + +public slots: // MerginAPI - backend signals void onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ); void onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ); @@ -136,7 +141,9 @@ class ProjectsModel : public QAbstractListModel void modelInitialized(); void hasMoreProjectsChanged(); - private: + void isLoadingChanged( bool isLoading ); + +private: QString modelTypeToFlag() const; void printProjects() const; QStringList projectNames() const; @@ -155,6 +162,8 @@ class ProjectsModel : public QAbstractListModel //! For processing only my requests QString mLastRequestId; + + bool mModelIsLoading; }; #endif // PROJECTSMODEL_H diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml index b015042a8..33d565cda 100644 --- a/app/qml/components/ProjectList.qml +++ b/app/qml/components/ProjectList.qml @@ -241,7 +241,7 @@ Item { anchors.fill: parent horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter - visible: reloadList.visible || ( projectModelType !== ProjectsModel.LocalProjectsModel && listview.count === 0 ) + visible: reloadList.visible || !controllerModel.isLoading && ( projectModelType !== ProjectsModel.LocalProjectsModel && listview.count === 0 ) text: reloadList.visible ? qsTr("Unable to get the list of projects.") : qsTr("No projects found!") color: InputStyle.fontColor font.pixelSize: InputStyle.fontPixelSizeNormal From 9fafe022d2e13f198cb098b7eaa8e7e1e3f0f74f Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Sat, 10 Apr 2021 21:51:01 +0200 Subject: [PATCH 44/53] move reload list to projectList --- app/projectsmodel.cpp | 2 +- app/qml/ProjectListPage.qml | 9 ++-- app/qml/ProjectPanel.qml | 6 +-- app/qml/SearchBar.qml | 2 +- app/qml/components/ProjectList.qml | 66 ++++++++++++++++-------------- 5 files changed, 41 insertions(+), 44 deletions(-) diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index 0bb684ee7..efe45e0aa 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -151,7 +151,7 @@ void ProjectsModel::listProjects( const QString &searchExpression, int page ) mLastRequestId = mBackend->listProjects( searchExpression, modelTypeToFlag(), "", page ); - if ( !mLastRequestId.isEmpty() && page == 1 ) + if ( !mLastRequestId.isEmpty() ) emit setModelIsLoading( true ); } diff --git a/app/qml/ProjectListPage.qml b/app/qml/ProjectListPage.qml index 5a7dfe50c..5e38d00d2 100644 --- a/app/qml/ProjectListPage.qml +++ b/app/qml/ProjectListPage.qml @@ -23,10 +23,8 @@ Item { signal openProjectRequested( string projectId, string projectFilePath ) signal showLocalChangesRequested( string projectId ) - function refreshProjectsList( keepSearchFilter = false ) { - if ( !keepSearchFilter ) - searchBar.deactivate() - + function refreshProjectsList() { + searchBar.deactivate() projectlist.refreshProjectList( searchBar.text ) } @@ -41,8 +39,6 @@ Item { } allowTimer: true - - onSearchTextChanged: projectlist.searchTextChanged( text ) } ProjectList { @@ -50,6 +46,7 @@ Item { projectModelType: root.projectModelType activeProjectId: root.activeProjectId + searchText: searchBar.text anchors { left: parent.left diff --git a/app/qml/ProjectPanel.qml b/app/qml/ProjectPanel.qml index eb7352c49..bf0369998 100644 --- a/app/qml/ProjectPanel.qml +++ b/app/qml/ProjectPanel.qml @@ -430,9 +430,7 @@ Item { Connections { target: __merginApi - onListProjectsFinished: { - stackView.pending = false - } + onListProjectsFinished: stackView.pending = false onListProjectsByNameFinished: stackView.pending = false onApiVersionStatusChanged: { stackView.pending = false @@ -461,8 +459,6 @@ Item { onAuthFailed: stackView.pending = false onRegistrationFailed: stackView.pending = false onRegistrationSucceeded: stackView.pending = false - - //TODO: on sync project failed: push auth panel too } } } diff --git a/app/qml/SearchBar.qml b/app/qml/SearchBar.qml index ad826c679..4fc34d926 100644 --- a/app/qml/SearchBar.qml +++ b/app/qml/SearchBar.qml @@ -20,7 +20,7 @@ Rectangle { signal searchTextChanged( string text ) - property string text: searchField.displayText + property string text: "" property bool allowTimer: false property int emitInterval: 200 diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml index 33d565cda..83c26ef98 100644 --- a/app/qml/components/ProjectList.qml +++ b/app/qml/components/ProjectList.qml @@ -20,20 +20,20 @@ Item { property int projectModelType: ProjectsModel.EmptyProjectsModel property string activeProjectId: "" + property string searchText: "" signal openProjectRequested( string projectId, string projectFilePath ) signal showLocalChangesRequested( string projectId ) signal activeProjectDeleted() - function searchTextChanged( searchText ) { - if ( projectModelType === ProjectsModel.PublicProjectsModel ) - { + onSearchTextChanged: { + if ( projectModelType === ProjectsModel.PublicProjectsModel ) { controllerModel.listProjects( searchText ) } else viewModel.searchExpression = searchText } - function refreshProjectList( searchText ) { + function refreshProjectList() { controllerModel.listProjects( searchText ) } @@ -259,37 +259,41 @@ Item { Connections { target: __merginApi - onListProjectsFailed: reloadList.visible = root.projectModelType !== ProjectsModel.LocalProjectsModel // show reload list to all models except local - onListProjectsFinished: reloadList.visible = false // hide reload list on other models + onListProjectsFailed: { + reloadList.visible = root.projectModelType !== ProjectsModel.LocalProjectsModel // show reload list to all models except local + } + + onListProjectsFinished: { + if ( projectCount > -1 ) + reloadList.visible = false + } } Button { - id: reloadBtn - width: reloadList.width - 2* InputStyle.panelMargin - height: reloadList.height - text: qsTr("Retry") - font.pixelSize: reloadBtn.height/2 - anchors.horizontalCenter: parent.horizontalCenter - onClicked: { - stackView.pending = true - - // filters suppose to not change - projectsPage.refreshProjectList( true ) - reloadList.visible = false - } - background: Rectangle { - color: InputStyle.highlightColor - radius: InputStyle.cornerRadius - } + id: reloadBtn + width: reloadList.width - 2* InputStyle.panelMargin + height: reloadList.height + text: qsTr("Retry") + font.pixelSize: reloadBtn.height/2 + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + // filters suppose to not change + controllerModel.listProjects( root.searchText ) + reloadList.visible = false + } + background: Rectangle { + color: InputStyle.highlightColor + radius: InputStyle.cornerRadius + } - contentItem: Text { - text: reloadBtn.text - font: reloadBtn.font - color: InputStyle.clrPanelMain - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } + contentItem: Text { + text: reloadBtn.text + font: reloadBtn.font + color: InputStyle.clrPanelMain + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } } } From 8440896b7632a1bc52369df1c79ec48f66e9485f Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Sat, 10 Apr 2021 22:20:05 +0200 Subject: [PATCH 45/53] texts visibility order --- app/qml/components/ProjectList.qml | 5 ++--- core/merginapi.cpp | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml index 83c26ef98..3d18ec2b7 100644 --- a/app/qml/components/ProjectList.qml +++ b/app/qml/components/ProjectList.qml @@ -167,7 +167,7 @@ Item { restartAppDialog.open() } } - visible: !__inputUtils.hasStoragePermission() + visible: !__inputUtils.hasStoragePermission() && !reloadList.visible color: InputStyle.fontColor font.pixelSize: InputStyle.fontPixelSizeNormal font.bold: true @@ -180,7 +180,7 @@ Item { Item { id: noLocalProjectsMessageContainer - visible: listview.count === 0 && !storagePermissionText.visible && projectModelType === ProjectsModel.LocalProjectsModel + visible: listview.count === 0 && !storagePermissionText.visible && projectModelType === ProjectsModel.LocalProjectsModel && root.searchText === "" anchors.fill: parent @@ -279,7 +279,6 @@ Item { onClicked: { // filters suppose to not change controllerModel.listProjects( root.searchText ) - reloadList.visible = false } background: Rectangle { color: InputStyle.highlightColor diff --git a/core/merginapi.cpp b/core/merginapi.cpp index c85c1c3e8..97d8e0065 100644 --- a/core/merginapi.cpp +++ b/core/merginapi.cpp @@ -71,6 +71,7 @@ QString MerginApi::listProjects( const QString &searchExpression, const QString bool authorize = !flag.isEmpty(); if ( ( authorize && !validateAuthAndContinute() ) || mApiVersionStatus != MerginApiStatus::OK ) { + emit listProjectsFailed(); return QString(); } From 64a1ce013b87cf035011a583401445b526d63ebe Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Sat, 10 Apr 2021 22:32:41 +0200 Subject: [PATCH 46/53] filtering on mergin projects --- app/projectsproxymodel.cpp | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/app/projectsproxymodel.cpp b/app/projectsproxymodel.cpp index c355423c7..9d25d84a3 100644 --- a/app/projectsproxymodel.cpp +++ b/app/projectsproxymodel.cpp @@ -18,7 +18,11 @@ void ProjectsProxyModel::initialize() setSourceModel( mModel ); mModelType = mModel->modelType(); - setFilterRole( ProjectsModel::ProjectFullName ); + if ( mModelType == ProjectsModel::CreatedProjectsModel ) + setFilterRole( ProjectsModel::ProjectName ); + else + setFilterRole( ProjectsModel::ProjectFullName ); + setFilterCaseSensitivity( Qt::CaseInsensitive ); sort( 0, Qt::AscendingOrder ); @@ -70,7 +74,6 @@ bool ProjectsProxyModel::lessThan( const QModelIndex &left, const QModelIndex &r { QString lProjectFullName = mModel->data( left, ProjectsModel::ProjectFullName ).toString(); QString rProjectFullName = mModel->data( right, ProjectsModel::ProjectFullName ).toString(); - qDebug() << "Comparing " << lProjectFullName << rProjectFullName; return lProjectFullName.compare( rProjectFullName, Qt::CaseInsensitive ) < 0; } @@ -82,17 +85,17 @@ bool ProjectsProxyModel::lessThan( const QModelIndex &left, const QModelIndex &r { return false; } + } - QString lNamespace = mModel->data( left, ProjectsModel::ProjectNamespace ).toString(); - QString lProjectName = mModel->data( left, ProjectsModel::ProjectName ).toString(); - QString rNamespace = mModel->data( right, ProjectsModel::ProjectNamespace ).toString(); - QString rProjectName = mModel->data( right, ProjectsModel::ProjectName ).toString(); + // comparing 2 mergin projects + QString lNamespace = mModel->data( left, ProjectsModel::ProjectNamespace ).toString(); + QString lProjectName = mModel->data( left, ProjectsModel::ProjectName ).toString(); + QString rNamespace = mModel->data( right, ProjectsModel::ProjectNamespace ).toString(); + QString rProjectName = mModel->data( right, ProjectsModel::ProjectName ).toString(); - if ( lNamespace == rNamespace ) - { - return lProjectName.compare( rProjectName, Qt::CaseInsensitive ) < 0; - } - return lNamespace.compare( rNamespace, Qt::CaseInsensitive ) < 0; + if ( lNamespace == rNamespace ) + { + return lProjectName.compare( rProjectName, Qt::CaseInsensitive ) < 0; } - return false; + return lNamespace.compare( rNamespace, Qt::CaseInsensitive ) < 0; } From 32e06ec57ddf7d2c986d9ac3da7e0a80ad073e43 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Sat, 10 Apr 2021 22:46:41 +0200 Subject: [PATCH 47/53] remove debug code --- app/projectsmodel.cpp | 83 ++----------------------------------------- app/projectsmodel.h | 1 - app/qml/main.qml | 1 - core/project.h | 1 - 4 files changed, 2 insertions(+), 84 deletions(-) diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index efe45e0aa..224f20c65 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -14,7 +14,6 @@ ProjectsModel::ProjectsModel( QObject *parent ) : QAbstractListModel( parent ) { - qDebug() << "PMR: Instantiated ProjectsModel! " << this << "MerginAPI: " << mBackend << "LPM:" << mLocalProjectsManager << "Type: " << mModelType; } void ProjectsModel::initializeProjectsModel() @@ -22,8 +21,6 @@ void ProjectsModel::initializeProjectsModel() if ( !mBackend || !mLocalProjectsManager || mModelType == EmptyProjectsModel ) // Model is not set up properly yet return; - qDebug() << "PMR: initializing projects model " << this; - QObject::connect( mBackend, &MerginApi::syncProjectStatusChanged, this, &ProjectsModel::onProjectSyncProgressChanged ); QObject::connect( mBackend, &MerginApi::syncProjectFinished, this, &ProjectsModel::onProjectSyncFinished ); QObject::connect( mBackend, &MerginApi::projectDetached, this, &ProjectsModel::onProjectDetachedFromMergin ); @@ -215,21 +212,16 @@ QVariant ProjectsModel::dataFrom( int fromRole, QVariant fromValue, int desiredR void ProjectsModel::onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ) { - qDebug() << "PMR: onListProjectsFinished(): received response with requestId = " << requestId; if ( mLastRequestId != requestId ) { - qDebug() << "PMR: onListProjectsFinished(): ignoring request with id " << requestId; return; } - qDebug() << "PMR: onListProjectsFinished(): project count = " << projectsCount << " but mergin projects emited: " << merginProjects.size(); - if ( page == 1 ) { // if we are populating first page, reset model and throw away previous projects beginResetModel(); mergeProjects( merginProjects, pendingProjects, false ); - printProjects(); endResetModel(); } else @@ -237,7 +229,6 @@ void ProjectsModel::onListProjectsFinished( const MerginProjectsList &merginProj // paginating next page, keep previous projects and emit model add items beginInsertRows( QModelIndex(), mProjects.size(), mProjects.size() + merginProjects.size() - 1 ); mergeProjects( merginProjects, pendingProjects, true ); - printProjects(); endInsertRows(); } @@ -250,16 +241,13 @@ void ProjectsModel::onListProjectsFinished( const MerginProjectsList &merginProj void ProjectsModel::onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ) { - qDebug() << "PMR: onListProjectsByNameFinished(): received response with requestId = " << requestId; if ( mLastRequestId != requestId ) { - qDebug() << "PMR: onListProjectsByNameFinished(): ignoring request with id " << requestId; return; } beginResetModel(); mergeProjects( merginProjects, pendingProjects ); - printProjects(); endResetModel(); emit setModelIsLoading( false ); @@ -269,8 +257,6 @@ void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Tra { LocalProjectsList localProjects = mLocalProjectsManager->projects(); - qDebug() << "PMR: mergeProjects(): # of local projects = " << localProjects.size() << " # of mergin projects = " << merginProjects.size(); - if ( !keepPrevious ) mProjects.clear(); @@ -347,34 +333,18 @@ void ProjectsModel::syncProject( const QString &projectId ) { std::shared_ptr project = projectFromId( projectId ); - if ( project == nullptr ) - { - qDebug() << "PMR: project" << projectId << "not in projects list"; - return; - } - - if ( !project->isMergin() ) - { - qDebug() << "PMR: project" << projectId << "is not a mergin project"; - return; - } - - if ( project->mergin->pending ) + if ( project == nullptr || !project->isMergin() || project->mergin->pending ) { - qDebug() << "PMR: project" << projectId << "is already syncing"; return; } if ( project->mergin->status == ProjectStatus::NoVersion || project->mergin->status == ProjectStatus::OutOfDate ) { - qDebug() << "PMR: updating project:" << project->mergin->id(); - bool useAuth = !mBackend->userAuth()->hasAuthData() && mModelType == ProjectModelTypes::PublicProjectsModel; mBackend->updateProject( project->mergin->projectNamespace, project->mergin->projectName, useAuth ); } else if ( project->mergin->status == ProjectStatus::Modified ) { - qDebug() << "PMR: uploading project:" << project->mergin->id(); mBackend->uploadProject( project->mergin->projectNamespace, project->mergin->projectName ); } } @@ -383,32 +353,17 @@ void ProjectsModel::stopProjectSync( const QString &projectId ) { std::shared_ptr project = projectFromId( projectId ); - if ( project == nullptr ) + if ( project == nullptr || !project->isMergin() || !project->mergin->pending ) { - qDebug() << "PMR: project" << projectId << "not in projects list"; - return; - } - - if ( !project->isMergin() ) - { - qDebug() << "PMR: project" << projectId << "is not a mergin project"; - return; - } - - if ( !project->mergin->pending ) - { - qDebug() << "PMR: project" << projectId << "is not pending"; return; } if ( project->mergin->status == ProjectStatus::NoVersion || project->mergin->status == ProjectStatus::OutOfDate ) { - qDebug() << "PMR: cancelling update of project:" << project->mergin->id(); mBackend->updateCancel( project->mergin->id() ); } else if ( project->mergin->status == ProjectStatus::Modified ) { - qDebug() << "PMR: cancelling upload of project:" << project->mergin->id(); mBackend->uploadCancel( project->mergin->id() ); } } @@ -442,8 +397,6 @@ void ProjectsModel::onProjectSyncFinished( const QString &projectDir, const QStr QModelIndex ix = index( mProjects.indexOf( project ) ); emit dataChanged( ix, ix ); - - qDebug() << "PMR: Project " << projectFullName << " finished sync"; } void ProjectsModel::onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ) @@ -457,8 +410,6 @@ void ProjectsModel::onProjectSyncProgressChanged( const QString &projectFullName QModelIndex ix = index( mProjects.indexOf( project ) ); emit dataChanged( ix, ix ); - - qDebug() << "PMR: Project " << projectFullName << " changed sync progress to " << progress; } void ProjectsModel::onProjectAdded( const LocalProject &project ) @@ -487,8 +438,6 @@ void ProjectsModel::onProjectAdded( const LocalProject &project ) mProjects << newProject; endInsertRows(); } - - qDebug() << "PMR: Added project" << project.id(); } void ProjectsModel::onAboutToRemoveProject( const LocalProject project ) @@ -504,8 +453,6 @@ void ProjectsModel::onAboutToRemoveProject( const LocalProject project ) beginRemoveRows( QModelIndex(), removeIndex, removeIndex ); mProjects.removeOne( proj ); endRemoveRows(); - - qDebug() << "PMR: Deleted project" << project.id(); } else { @@ -535,7 +482,6 @@ void ProjectsModel::onProjectDataChanged( const LocalProject &project ) emit dataChanged( editIndex, editIndex ); } - qDebug() << "PMR: Data changed in project" << project.id(); } void ProjectsModel::onProjectDetachedFromMergin( const QString &projectFullName ) @@ -560,8 +506,6 @@ void ProjectsModel::onProjectAttachedToMergin( const QString &projectFullName ) // To ensure project will be in sync with server, send listProjectByName request. // In theory we could send that request only for this one project. listProjectsByName(); - - qDebug() << "PMR: Project attached to mergin " << projectFullName; } void ProjectsModel::onAuthChanged() @@ -583,7 +527,6 @@ void ProjectsModel::setMerginApi( MerginApi *merginApi ) return; mBackend = merginApi; - qDebug() << "PMR: New MA: " << mBackend; initializeProjectsModel(); } @@ -593,7 +536,6 @@ void ProjectsModel::setLocalProjectsManager( LocalProjectsManager *localProjects return; mLocalProjectsManager = localProjectsManager; - qDebug() << "PMR: New LPM: " << mLocalProjectsManager; initializeProjectsModel(); } @@ -603,7 +545,6 @@ void ProjectsModel::setModelType( ProjectsModel::ProjectModelTypes modelType ) return; mModelType = modelType; - qDebug() << "PMR: New Type: " << mModelType; initializeProjectsModel(); } @@ -620,25 +561,6 @@ QString ProjectsModel::modelTypeToFlag() const } } -void ProjectsModel::printProjects() const // TODO: Helper function, remove after refactoring is done -{ - qDebug() << "PMR: Model " << this << " with type " << modelTypeToFlag() << " has projects: "; - for ( const auto &proj : mProjects ) - { - QString lcl = proj->isLocal() ? "local" : ""; - QString mrgn = proj->isMergin() ? "mergin" : ""; - - if ( proj->isLocal() ) - { - qDebug() << " - " << proj->local->projectNamespace << proj->local->projectName << lcl << mrgn; - } - else if ( proj->isMergin() ) - { - qDebug() << " - " << proj->mergin->projectNamespace << proj->mergin->projectName << lcl << mrgn; - } - } -} - QStringList ProjectsModel::projectNames() const { QStringList projectNames; @@ -659,7 +581,6 @@ void ProjectsModel::loadLocalProjects() { beginResetModel(); mergeProjects( MerginProjectsList(), Transactions() ); // Fills model with local projects - printProjects(); endResetModel(); } } diff --git a/app/projectsmodel.h b/app/projectsmodel.h index 49fee8872..a95f8454b 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -145,7 +145,6 @@ public slots: private: QString modelTypeToFlag() const; - void printProjects() const; QStringList projectNames() const; void loadLocalProjects(); void initializeProjectsModel(); diff --git a/app/qml/main.qml b/app/qml/main.qml index d2791040a..f69a635bd 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -599,7 +599,6 @@ ApplicationWindow { } onOpenProjectRequested: { - console.log("loading ", projectPath) __appSettings.defaultProject = projectPath __appSettings.activeProject = projectPath __loader.load( projectPath ) diff --git a/core/project.h b/core/project.h index 3a97ec0bc..6a435edab 100644 --- a/core/project.h +++ b/core/project.h @@ -14,7 +14,6 @@ #include #include #include -#include struct Project; From b437bf0bb7ef832a42c6f7069950dd098a2724be Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Mon, 12 Apr 2021 07:40:48 +0200 Subject: [PATCH 48/53] remove unused code --- app/projectsmodel.cpp | 32 ------------------------------ app/qml/components/ProjectList.qml | 4 ---- core/merginapi.cpp | 1 - core/merginapi.h | 4 ++-- core/project.h | 8 +++++--- 5 files changed, 7 insertions(+), 42 deletions(-) diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index 224f20c65..e6e8122e8 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -178,38 +178,6 @@ void ProjectsModel::fetchAnotherPage( const QString &searchExpression ) listProjects( searchExpression, mPaginatedPage + 1 ); } -QVariant ProjectsModel::dataFrom( int fromRole, QVariant fromValue, int desiredRole ) const -{ - switch ( fromRole ) - { - case ProjectId: - { - std::shared_ptr project = projectFromId( fromValue.toString() ); - if ( project ) - { - QModelIndex ix = index( mProjects.indexOf( project ) ); - return data( ix, desiredRole ); - } - return QVariant(); - } - - case ProjectFilePath: - { - for ( int i = 0; i < mProjects.size(); i++ ) - { - if ( mProjects[i]->isLocal() && mProjects[i]->local->qgisProjectFilePath == fromValue.toString() ) - { - QModelIndex ix = index( i ); - return data( ix, desiredRole ); - } - } - } - default: return QVariant(); - } - - return QVariant(); -} - void ProjectsModel::onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ) { if ( mLastRequestId != requestId ) diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml index 3d18ec2b7..194c871aa 100644 --- a/app/qml/components/ProjectList.qml +++ b/app/qml/components/ProjectList.qml @@ -37,10 +37,6 @@ Item { controllerModel.listProjects( searchText ) } - function modelData( fromRole, fromValue, desiredRole ) { - controllerModel.dataFrom( fromRole, fromValue, desiredRole ) - } - ListView { id: listview diff --git a/core/merginapi.cpp b/core/merginapi.cpp index 97d8e0065..59f5ec883 100644 --- a/core/merginapi.cpp +++ b/core/merginapi.cpp @@ -1255,7 +1255,6 @@ void MerginApi::listProjectsByNameReplyFinished( QString requestId ) QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); - /* TODO: Detect orphaned project? Project that was considered Mergin but did not get info back */ MerginProjectsList projectList; if ( r->error() == QNetworkReply::NoError ) diff --git a/core/merginapi.h b/core/merginapi.h index 12ab1c5cf..9e408334f 100644 --- a/core/merginapi.h +++ b/core/merginapi.h @@ -366,7 +366,7 @@ class MerginApi: public QObject QString apiRoot() const; void setApiRoot( const QString &apiRoot ); - QString merginUserName() const; // TODO: remove (use can be replaced with userInfo->username) + QString merginUserName() const; // TODO: replace (can be replaced with userInfo->username) MerginApiStatus::VersionStatus apiVersionStatus() const; void setApiVersionStatus( const MerginApiStatus::VersionStatus &apiVersionStatus ); @@ -479,7 +479,7 @@ class MerginApi: public QObject void sendUploadCancelRequest( const QString &projectFullName, const QString &transactionUUID ); bool writeData( const QByteArray &data, const QString &path ); - void createPathIfNotExists( const QString &filePath ); // TODO: make static and move to InputUtils + void createPathIfNotExists( const QString &filePath ); static QByteArray getChecksum( const QString &filePath ); static QSet listFiles( const QString &projectPath ); diff --git a/core/project.h b/core/project.h index 6a435edab..cb0e0454a 100644 --- a/core/project.h +++ b/core/project.h @@ -17,6 +17,9 @@ struct Project; +/** + * \brief ProjectStatus namespace + */ namespace ProjectStatus { Q_NAMESPACE @@ -35,7 +38,7 @@ namespace ProjectStatus struct LocalProject { - LocalProject() {}; // TODO: define copy constructor + LocalProject() {}; ~LocalProject() {}; QString projectName; @@ -78,10 +81,8 @@ struct MerginProject ProjectStatus::Status status = ProjectStatus::NoVersion; bool pending = false; - qreal progress = 0; - // Maybe better use enum or int for error code QString remoteError; // Error leading to project not being able to sync bool isValid() const { return !projectName.isEmpty() && !projectNamespace.isEmpty(); } @@ -151,4 +152,5 @@ struct Project typedef QList MerginProjectsList; typedef QList LocalProjectsList; Q_DECLARE_METATYPE( MerginProjectsList ) + #endif // PROJECT_H From 2c8c1f55409fc918c4e38d16b6fd5e4935aa6a17 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Mon, 12 Apr 2021 07:56:58 +0200 Subject: [PATCH 49/53] add code documentation --- app/projectsmodel.h | 37 ++++++++++++++++++++++++++++++------- app/projectsproxymodel.h | 5 ++++- core/project.h | 24 +++++++++++++++++++++--- 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/app/projectsmodel.h b/app/projectsmodel.h index a95f8454b..2fb6e03c1 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -19,7 +19,24 @@ class LocalProjectsManager; /** - * \brief The ProjectsModel class + * \brief The ProjectsModel class holds projects (both local and mergin). Model loads local projects from LocalProjectsManager that hold them + during runtime. Remote (Mergin) projects are fetched from MerginAPI calling listProjects or listProjectsByName (based on the type of the model). + * + * The main job of the model is to merge projects coming from MerginAPI and LocalProjectsManager. By merging it means Each time new response is received from MerginAPI, model erases + * old remembered projects and fetches new. Merge logic depends on the model type (described below). + * + * Model can have different types that affect handling of the projects. + * - LocalProjectsModel always keeps all local projects and seek their mergin part when listProjectsByNameFinished + * - Created-, Shared-, and PublicProjectsModel does the opposite, keeps all mergin projects and seeks their local part in projects from LocalProjectsManager + * - EmptyProjectsModel is default state + * + * To avoid overriding of requests, model remembers last sent request ID and upon receiving signal from MerginAPI about listProjectsFinished, it firsts compares + * the remembered ID with returned ID. If they do not match, response is ignored. + * + * Model also support pagination. To fetch another page call fetchAnotherPage. + * + * This is a QML type with 3 required properties (pointer to merginApi, pointer to localProjectsManager and modelType). Without these properties model does nothing. + * After setting all of these properties, model is initialized, starts listening to various signals and offers data. */ class ProjectsModel : public QAbstractListModel { @@ -47,7 +64,10 @@ class ProjectsModel : public QAbstractListModel Q_ENUM( Roles ) /** - * \brief The ProjectModelTypes enum + * \brief The ProjectModelTypes enum: + * - LocalProjectsModel always keeps all local projects and seek their mergin part when listProjectsByNameFinished + * - Created-, Shared-, and PublicProjectsModel does the opposite, keeps all mergin projects and seeks their local part in projects from LocalProjectsManager + * - EmptyProjectsModel is default state */ enum ProjectModelTypes { @@ -63,12 +83,16 @@ class ProjectsModel : public QAbstractListModel ProjectsModel( QObject *parent = nullptr ); ~ProjectsModel() override {}; - // From Qt 5.15 we can use REQUIRED keyword here that will ensure object will be always instantiated from QML with these mandatory properties + // From Qt 5.15 we can use REQUIRED keyword here, that will ensure object will be always instantiated from QML with these mandatory properties Q_PROPERTY( MerginApi *merginApi READ merginApi WRITE setMerginApi ) Q_PROPERTY( LocalProjectsManager *localProjectsManager READ localProjectsManager WRITE setLocalProjectsManager ) Q_PROPERTY( ProjectModelTypes modelType READ modelType WRITE setModelType ) + //! Indicates that model has more projects to fetch, so view can call fetchAnotherPage Q_PROPERTY( bool hasMoreProjects READ hasMoreProjects NOTIFY hasMoreProjectsChanged ) + + //! Indicates that model is currently processing projects, filling its storage. + //! Models loading starts when listProjectsAPI is sent and finishes after endResetModel signal is emitted when projects are merged. Q_PROPERTY( bool isLoading READ isLoading NOTIFY isLoadingChanged ) // Needed methods from QAbstractListModel @@ -77,10 +101,10 @@ class ProjectsModel : public QAbstractListModel QHash roleNames() const override; int rowCount( const QModelIndex &parent = QModelIndex() ) const override; - //! Called to list projects, either fetch more or get first + //! Called to list projects, either fetch more or get first, search expression Q_INVOKABLE void listProjects( const QString &searchExpression = QString(), int page = 1 ); - //! Called to list projects, either fetch more or get first + //! Called to list projects via listProjectsByName API, used in LocalProjectsModel Q_INVOKABLE void listProjectsByName(); //! Syncs specified project - upload or update @@ -95,10 +119,9 @@ class ProjectsModel : public QAbstractListModel //! Migrates local project to mergin Q_INVOKABLE void migrateProject( const QString &projectId ); + //! Calls listProjects with incremented page Q_INVOKABLE void fetchAnotherPage( const QString &searchExpression ); - Q_INVOKABLE QVariant dataFrom( int fromRole, QVariant fromValue, int desiredRole ) const; - //! Method merging local and remote projects based on the model type void mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious = false ); diff --git a/app/projectsproxymodel.h b/app/projectsproxymodel.h index 3c9dd885f..905eb1af0 100644 --- a/app/projectsproxymodel.h +++ b/app/projectsproxymodel.h @@ -16,7 +16,10 @@ #include "projectsmodel.h" /** - * @brief The ProjectsProxyModel class + * \brief The ProjectsProxyModel class used as a proxy filter/sort model for the \see ProjectsModel class. + * + * ProjectsProxyModel is a QML type with required property of projectSourceModel. Without source model, this model does nothing (is not initialized). + * After setting source model, this model starts sorting and allows filtering (search) from view. */ class ProjectsProxyModel : public QSortFilterProxyModel { diff --git a/core/project.h b/core/project.h index cb0e0454a..f40a716bb 100644 --- a/core/project.h +++ b/core/project.h @@ -17,9 +17,6 @@ struct Project; -/** - * \brief ProjectStatus namespace - */ namespace ProjectStatus { Q_NAMESPACE @@ -33,9 +30,17 @@ namespace ProjectStatus }; Q_ENUM_NS( Status ) + //! Returns project state from ProjectStatus::Status enum for the project Status projectStatus( const std::shared_ptr project ); } +/** + * \brief The LocalProject struct is used as a struct for projects that are available on the device. + * The struct is used in the \see Projects struct and also for communication between LocalProjectsManager and ProjectsModel + * + * \note Struct contains member id() which in this time returns projects full name, however, once we + * start using projects IDs, it can be replaced for that ID. + */ struct LocalProject { LocalProject() {}; @@ -66,6 +71,13 @@ struct LocalProject } }; +/** + * \brief The MerginProject struct is used for projects that comes from Mergin. + * This struct is used in the \see Projects struct and also for communication between MerginAPI and ProjectsModel + * + * \note Struct contains member id() which in this time returns projects full name, however, once we + * start using projects IDs, it can be replaced for that ID. + */ struct MerginProject { MerginProject() {}; @@ -98,6 +110,12 @@ struct MerginProject } }; +/** + * \brief The Project struct serves as a struct for any kind of project (local/mergin). + * It consists of two main parts - mergin and local. + * Both parts are pointers to their specific structs and based on the pointer value (nullptr or assigned) this structs + * decides if the project is local, mergin or both. + */ struct Project { Project() {}; From 90ce60608a03722159934fd5fe36758ab2215fd5 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Mon, 12 Apr 2021 08:25:46 +0200 Subject: [PATCH 50/53] nit, check all --- app/projectsmodel.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/projectsmodel.h b/app/projectsmodel.h index 2fb6e03c1..ce93c34e3 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -141,7 +141,7 @@ class ProjectsModel : public QAbstractListModel void setModelIsLoading( bool state ); -public slots: + public slots: // MerginAPI - backend signals void onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ); void onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ); @@ -166,7 +166,7 @@ public slots: void isLoadingChanged( bool isLoading ); -private: + private: QString modelTypeToFlag() const; QStringList projectNames() const; void loadLocalProjects(); From ea24774a94b165b00196058d7e43c1ce20e3d8c4 Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Mon, 12 Apr 2021 08:42:08 +0200 Subject: [PATCH 51/53] add diagrams --- .../code/projects/media/class-structure.png | Bin 0 -> 321892 bytes .../code/projects/media/more-menu.png | Bin 0 -> 157577 bytes .../developers/code/projects/project-models.md | 8 ++++++++ 3 files changed, 8 insertions(+) create mode 100644 docs/developers/code/projects/media/class-structure.png create mode 100644 docs/developers/code/projects/media/more-menu.png create mode 100644 docs/developers/code/projects/project-models.md diff --git a/docs/developers/code/projects/media/class-structure.png b/docs/developers/code/projects/media/class-structure.png new file mode 100644 index 0000000000000000000000000000000000000000..48d9e146920173a19ae3e06006cf7f1b389c5300 GIT binary patch literal 321892 zcmb4rbySsG_qDwhvMKl``LTNTyxE}LsV5{G0|?L-MDcBQ(jI={l*Pc+8Z|@5m03ClP`fK zFK^tSydf_ouHj~|nTlGi`QW-UVg9woV=;|j^~W4$U4urUXwr1`p-MxKyOg?AoIs2H+zg(Xd9XD>*Z8w~!ln;DbdF$TaFgv?Hy`to;wI;AG2^Dh&-`mK4Qp!|6RGL zEbIS%oKlpg;7odm6v*WMvnfc4`O)el210vN1V*Nxjq6*i4rb)IY)xj;{O=(xz4h6S z?YX3*yz$S;3&@CFmXlh;@MZt{UI{38kI$Jf-v-a~H$(4alSMqw64l8E(vB(4ynI7Z>|9{K; zj_GUt;nSCUKbjCNzIR3Ec%B~-vs7)za=lbX!Ts~~jFym5Q5H+g;`PwK4qg_%!&rgP z6wjlt7Sq*^Ikr=k%QY)+@yPxx@6QvuNDzcJOZ;`~-AV=ax@h{)+3TZ9oi;@NGGJdP z_@+C3nJXy%Yd78C@3N#CEEY*F-0OKfV=4G--HcxO4Xxlm$MyR!QG^h!Vm+$@?GJXJ zYe95*bGpA`Su|aH|1S$lC&f{{E+XZrEMWf265xA==DW9DxAP{9t+q=eU%g!^P2`iF zKL1SZvvMY^TGt&rdZk2F)#JmB@nox!oTN|3-=;QeH;R`+Lzr3q&$D2!e9pp$_^k?u zseT+y*(dvB33ictL<%2qpcAlmO@!diFqm&b1fzbwrkEMRy1kUlYd#W@>x+Gh!uOS2 zeaU*JX|C|IoqWMW#E1J%=u(KC^^C41yivt#lsK% zsY1yNZ{1o_ysu6zdJ{R3rkWOBYa4K0pN%*63aq4nh}5K$694o*ILHfo$+5pZUO__4FAMhK&cg~0`}*CNHG4fQeVBaHs<;i$ zt9D8|$*pr#QlHpg+1D(^`|XVv>5j1KmSw4WJ{iq3#u7O(Lj158A=E!wsLj5!UWhd;!h{;|Rhu=uGfyAvucUcfY}SVW zpKw9=9fl~mZoX2JmU&G`0^-Toy`u@!z@CStCeqGLH&Jwq)$3fgHl6Q;D$S&$YqC#^gsEy|1s}PARW; z_sMA2)3xFDT82_;0c}6t_T7*W7`-CzH@8JSriUu=i*MbxOWAbVntT!aK8_naQtihi z;nktS^%HKBQ@R^HsY>;)ajtX6f3Th#eQ=)#TlAvj{XuUM_j``+_WPojX1Dq5Y~tAs z?FiZKT^@|+`Yxu}3Fv}-+!WT&p&s&7&x!c3_$!)8F{E=AtQPGVe+FAO9Kmn}b{#dW z?(%3VIf6{+c3Wqbxn=;K>a~*g%fDPXSgaVHwg#!Q9t*+az;d#^9m*h!WgsnVlO(iwD>nCCuzaXD*{w5g+lUU5}FB*3{Nc!3d4fS#=dswuKE2v4xmaOmu2wvJTrB!e0cOqjF))G{SLjHof9G-(a%2cTjPF?zf>(E7$Y+Kte8;Z)V5x#6lszaTP4d=Zqv;4d1=h=7^8z zk(nB)Bob0ntbll8(6Ha%6zcD3CeU{8(~!5fTY!Q#y8k0!0Zd<@Y8K}4G`&0{Yl zAHV}$0;Yp$m8iBLj&CnE-j6WTu(XI?{E#enn3vcGo|A0R_vY&hSyZjm(iU<{4#Eb? z5MHU06+hoJ6bcWHo{W#L{@Gpu>U0s)HNMJZN}I`w{L~ahK80aRkK-BFK3^1aoz=Jo z%6#kbVq{q)LjIzKtE5+X4?h-cA>w^hIe5WcAWbvu7kPkEWdm;*_qh&h6{aHo#_9`I9I~N%d(Ch@oPsgOm`)%DRUnU#&bJ7souY z{mp%c=f22k7Ctr7B5-)VAo3TF3tA^MT(tEZr8BltC`05~hMi8dzQ!`#=Oq(zQF=bD zWYcsg#oW}#JH?6@^tp!_9Uo-%ym!FV`KPv1%9oQ2y?RxmBx`wo$hytfxDuoU+I zE!01jK0+EKUgbFIcsG*mB(L)=KKGQhk=&^UjWVAZ@2hsXJC)EGy04z< zA?U@3zk+g>ve@M-w?jUi3Ar(IN z7j~%=)kQSd&5&k8xV02B!iUbVQ@g=de*DcnStV7F?t;98jQW7!&clE*wdU_s4^>XtS;N!3^MrAbbgd6e{NhI| z_Dey?$Q!jtI>q=2{Qjb+Dg;nRyT(fwp&McxK8;4r2QHCUvxw!r6UY7!O=-==a~ScST2XoQ$}45 z@v%HLnsU1tK_S#dbsG*QZmVJ1k^1M(kB|wo7EKSIJG3kVh{A_0d=Ql+4U$G6s>+O!uun0C)p3YI$Lg{@NmK2sNgf zZ|<9~2(@R2eZy%-k$&Bmh8&8(M>w9a?K@mUmRW{!X~{p%!=W*W(8ag@BB|)xl8G1x z{^vB?uN~ARU{Z}`z!0?RJ%{BPlH%_?qW{#EKQ(3FXnS4=(8b-lod);jql4G0-p&+K zjU3U5o@>L|761~uAPhD#%ugUfC1yM#oMrVVyy;}uXxx9KZ zo6MRgDAAdS*-eVVjp}1KRXeIyA8=F>6M(SWNXEB!j(z3QPBvoROvf~$qtTIQRF*#Q za*=kLkwuh=yYqAIMo`%*n5(f1Gf9KklFZ+o6`jm zSDJ5nS8aiKY!pbI@7L^3&sojXf!T$Gcs4zO_vytU7l4Tk_~RM{8r|PE7tHbLsmKJI>2ZWe^prf`U#2TUZXsz?g2slD zD495r(=*~Cb#|%<5w7Z#Wc)^2W&8WykUmC4ziu1ULiI7t zw6I@AqZlZEiP0Z)YotYc-Bgi0z+6S2FX}=F&}hyg@#NU%*1J_T5Zrp>(vAMYVV9KO zA^jO2okM;)_Qr5_jWboP%!%Y+=duWT^v5)6ejOBuylF=`D!${0cEf6B*k&d+VekQt zCk?9;I$q*v+j}O~>IAXDz+@{ON)zfCXO27indey=w&?@EkR2B&P+pyYP+8yZCq~dV z^L!=MB|K~w`;06+$@6S2_b6K06~G*?j8Y1ML@shk!*`C09Z2ZZ8Ep&v9nM6B3$;{k z%4sdGw9IRXY>UCBoe>{9bFn21X^G+}v>8NPO@+fTxxH@FFSwkScSW4x$dWn+==ZKj z{It*B>-E*(zwDC|p`DIWeE2%nbg<5K=bKA5yZ`yxGP&$s@v9drtgXgXQ+7fdCatAi zwh=z9%E%S=v)|AnVeT-fV`7Qh=~BX$HESp_M3&NzzYFy~Y$MAcQ~6Wk4u2 z(|RhDKsG_OOl?bqcr*BkLoHPvBAY_ofr!x@7w=6L%_!%e+_K68b*b+;O>3Ei$V!5m5R7HzX z)(1lGXnkNnm%2-e?n|VidN0U7&Ck!}-Q@!Ts=(BskPLvxX1t_MlfFBSXEzG_@wF-F zgnTY<^>^ZPboDwRc4h z-Po2}eeNE!Cn8OL`{lJ;Yq@EavkW~+n{|E1v8WtDyZxlwFz#>)aNV1Hneg zBGS~m;of9}K`of5IJmJd4%%18&m$!1*0|p~b`ZPlMKmz^k)3VZeQYqqI;&Q}_^zj2 z(`Ygr=}s&EoC{Q#!JdC7p1ZVQs2B^hK}WchEDX15TZ` z)f8e~hfY74fXVvK>Mx*>FvyzZ$iPww7Y2Y^qJMrpEilqlw#KXCAF%q{?Ogy!IJi#1 z+3kHRJcWh?!^k2Dex&n$(J$qj&a$93)i+tFGr;ascW*%a5BcB1FkT8}+@wYe5Kv-- zO>;QkLzutiheSAik4AN2qKMpt^KeTMwj9Xu1IWhh()&}X)~>w zaY@FvsY&Fqj{o%Bl_ii!m47A084W*)i(|qkn_Gn^ZXA2^TS(;_9%~DvyIv$tDi9$E z9%r%s#ifzk&ZpzaBRZnHb7x|SBQ?&J{tkhaOBdY;H(?03rh|2&$uQQJbNl^qSjKSd zk!GrJQ-j8INAY$sW3@37Ilf9fI$IE=FUxy}_kE5x;6{iK&HxUx8oS7Tpjr{k)F*h3 zUx_ymsh3g6wujlZ?Vb{p%FLu`bs>zsQX+fq35OxHL!_c^$Ktz9qX2_P;wU%JmY$%_kB&94cxcP(?DaVu?PMxHNG>0Xnj4!m9)xD9h?uUp1l2>*{g%VL=OM~F3}EO&G3T%5;A1cE~jPIm&d*LOZHg*@v`^NkiKc zA&YY1@<{%iDg-k)2d-}cmFA~BDyn!df^210bWHg#6ukEekm5JhZdg@^#%_lt&k=K5 zOdK&-^7A#R1mo2+$}x2m=m(%=>52VldBx0P;$Tm8sL7Eaz&8XHfO%Jws6{BgVAJY! zU?cb6mcK$HtF%1CwBs@M?PJG7hEU(W&LPi$`r#lL<7Gm7loUQlVhfLAK}U;pxz`RO zyF!`~{X+1xP^2;G$zjYm6C{S7hhrIaTthxLkdB%5ipwV_3GbBRnGow``iC8w}?3yS&+g1vrX8l#Le%Tv_S$^GK{%;GC=6#>!-nbuVfi5vzEX zKZyR^5TB&E>GcT_&1=I-%52}R{um*b#pJBCCLvNv9s!0I@E zv_7|DDV8RUJV`Nf=td7wLh+P$Av`WLdiEYAgCKc?n>g~8A?j4MO&L4>j z;O@%MxU^57XP~o7rf6FOq{VJKIZsh%(EeRydmEXRZIX>MULePu7c@GC!X|8@%ayDt zWE49^H`SWu(vARo(C}Sj_othg&$e5kXITfi`m}ZTd$M=ZdZ^(mTMGOYu}c`=1B&Zt z&m1Gt3&m6^e1o&2Wr8LQJvI4h=En4^V>(4|f=YY&j;Efk&(CDSb4yF&jC!MX={s^5 zh0ul97&trR2X`EpOx6rfi{pz^?4}bO!(sm4{}vLQgEjCy>iYJd)$o48;S9h=EAX9Hj8dw8?DaNVL#NU zW2R!SVaH3#>+r3xJAoiwWkdA`{%9nMGEtU@OAS*y1_<7^C#f^czFMrPnfY-5Uy+21 zio%-5&5IDI+oBsDPj!eTZa^~=4-aMG6<>1td-iTjn|iQqOUyS&IkKV{b(md%d_D zAxRQ@yAPW_J&~U#50Q7a|I5@YRxZQ35>ixju0RJ;16{)LxOh7f3uedV zRs=0lpxUi~&%h5KlKI+5Yg!wecqvBf-_<2Ry-U1gqBiB&Nv8eyk@p2a=!bmi_Dg5N z)dDqvnd@nn2{C7uYQ=W8e|M$K?}Lt!X%WnyM6?B!Y}od;>wb?ieWx5j(F;cv?kuB4 ziz(F15)2tV96F*dhHOGR;f*ivS<2L*8Hj!;2vjjFglqE`8iw0?N^T_!!O+kG*b9f& zFhxwP`|YSw?ES=@X(=I2tPFAzwEjA@x+e>`^!G2u6gF4X{W*;n(A}mLn6(sWMr(ew zpuZ;y(p+MbLwXq-iO04p!qQBoCY_%)^^#X2SdubI;>Il=m@21^Z#j0XG@HZJ z?Yobo@ab_zgW_DF;m~3nbyEmp2IhDsUfTTQz*amw10@d}mC;*D?>a1%F6M3pI1|nr z`Wv&UH++t=WUjzi%xh`wEAU|7|l`|Nt}rxeFkj* z)M_b_L-InZ9u%rbETb2Tr4K1qXxnrGlD|74@C5{NvA7-DPP6;e~;z?3LBYl$6`&f1nZm=}_+@x)0UL7PZlC zrP*^_n>@jqqdz#?_%OAVht9+YyXHjgaLJmQn|wM?|JH3LLo&!O&c*AiUTwtgV&@}Y zY;$AjTjRXND*X*4yCma$OW%bds(R_y-|-uO$2!uSZ#G&bWmgMd@0+LooyxJ7D)An zKRu=JJW#8vP5SZvemEtz-J#1~fqjy+m-b}e1tPZ)D zAn!)vb)K$ddR0?U!Kh(M0zph|!q9<>=2){OvoaLkXX~S-OVs=#EZxv=xwa=5zG-!D zJ$H#?)!|+DP2HJ(YfI5So{o<)$y`Ku@kg)>B5Ll0+mSmxfOFqGmGAbo#x&;#c!JGIl?bg)Sd0p0&*Q#CbF~)6(zgILgk2x4EJ<)XS1{+!nTpd9Eudy;Vtgr`zCZE(Mn>sTvX`4tcuQS1#IM@ zLsp>2sAt8&0jrV2JJ}SkbW*l(qU&r|AC&)C0E7oSBYN!1=|+dfLqNMs96LUj&XT?+ z&fvdPAcz{|dlxC=jn^!YR)=(Z&0_$VWwtYh?JogA6fsZ(wB#(wI+m#Y8=Gp17 z&lI*3=5mm)oN=l}JOEZ1F`$F%NubwG9BHnH-br)D5H7%NCp#P1fMfbNICNTvqCY&p z!!PB8l+YmoOB;YbLM$PiH=+?_*?s@^l75%xN1!}63)gq_tM-YLZ%4I@udzC6N9vC7 zuS2s!yuh#KpP!3gkRlD{A#&?{kCXvffJ)tM79;(xXXuya=-i`VH2Xd%4m~(dZ<0wb z3dTxBPX$w$N0!ourNOE<`5BU>iY3*m=kEpJVfsp{3D)D_Q016>R4bc%`S#XBrCP{{ zz{$CQHVHi!?4tuFr5KITt1#Tc*i2-W53o-T+)AogP95c3XpXnG75ZN>jB%z?hHN>I zDSWSNU%{|5QVVwr5)^Wb^2VZn4(oUi8b~>h-g$y{m36Y0zkGV$yjHxk3ZGhALvV@% zEgEvaRBI46d)b?}PEPOfFuEL+Id4cZ-$S-{aB?dA?$>En_G+W{;m0Rm0-;o}Sc5P^ zb~Y!kO5@?MjSeIgOQp%uvT!knHnbQ0^32kt^?D@q3li=e-OB+UcEgyKh3pL{nR9iw z0t?xr6ts32JBNQKJZjZ10OTpnASA7GpZU?Al^A|vD2siTr#Ug z`Huh9^rn+!moh_IIT~RG4mD*PFUH%txtzob^RErxFxfIg@a*_B1S@dERt?R~ZxA;*@Z-+QUaF3!JBU7WRQ>eQ5 zjdfIy-(r~SxpF`)PK7KtSbwaaz>kMPG44yEF5Vf2I{@ouT;%h?8n z-D+0&4CD>tU;LY?8&H(ysOD-x>wS|~Jb^=NuQfKk0h*jreFy*1EKhHChT_La5yD@h zD_=c$Fsq2~mP(7hy1S+<$JgO0mWhN`ECsi{bI^#JICAt`3w)J-lDkil{O1#Oq{=-Urqx97!RPtl~K22R&LbK}Up;oZiO z8|k>j^|Udpcw3<2LGF@v%Utv~?vK)iOPdyxumL60H*eE+ZOzxwd7@dns0lOnWuP|i zKqt0urD952bc-@^?0kx&DWL5{+U4kAxtmK7{%EMNnJI0Ttz#F4f7)D4#N9^^LionT4wq1&XrJU^~lpcnmckD7#@8TJMT+5{V$d)#gW zJ(R8Ps7X>XZo0XPJ+3#c=fx+hFS3U3Z=$&!WtevguZ1 z)!g+Ai3kZs*$y5G#e*KfVsM;S`ai0u)1mf+eI5c_m|jflE%#Z_QJvgCInLAY{`UOpN}UMFfB@F9l@c2D zw$rhEe)Wd)&&tHN7cL+3y2NByH};6!{rEIM>I(lQv=DTlj;`-guPn$2n%2I5_^~YP z5so%D+bB@n1mv1y^wiigu#jA0rFc(#%eH2*)1UTQPkSAge)QYgf7$#^97Z-EZVgpr zLqF$SeFa+pCS1Dv1Sm^%SEf zwC03l4win-ipW1?D*_-}A5=+9gZ=kt;)(aN1nQUvh}{bW(Cin-+qN+0Y!cQSqakP* z{_jq&xgtQXqZj-6jm>T}{Zoi2Sgdl-;%i^llp-y9#*yTn@!C$BWvTL4mQ&v9ROnzD zc~*$Q5^!coY;~JV>Mu``N+$wk5;OAMt(a>_iLAvlZkAZs&I#Xf^%x~{i#TfGij?mh ziX%}m@4YhSCqQAER#m9d1HL35$ueB1g1iTcTI z?r&5DR5rq(7F{+-j;oM5*l|=eDZ@qykP>fv>m`!^{iKzOJN0Ql-JRUuN69D(xClm3 zTe9q&PIC96XPTYejzHq1=F)v`J1YA5oL&RzFFhS463EP>PHQR|2 zV;n3Unnfk)Z_k}3Oo?Aj!LVe%zq1GJ=~COQ&vXa(ik|{CsSR^GX!(AP^Up^}{sFqM zUk|+bQyu#pw&r{S@SoFbRb?~#|B0u-wgkW_DfV+{M~a;p=wYsYnCD$jk#_W#zNf-$ z@rO4z^9K~(*Yp&M6{CyjT?GQgg}B-J=ld6oZZ8$`uXJCEt#kaHG$q0A_hfXas&UEs z#MxxA1M%NR5xO!*b*b#Fy>UbDBtR%9zKalKc4iolHj(u?cmZ#IKkFCV@8vet7S;SC zjP79ptNU#Ix$xWX=SP8Ew>OJ?){wTAlWfPOZSXuh->|XabhCV5?~@>NxOjyA&|!~Rz!TX9I{_P>iINZP|K9(=7T7BfzYc!cuR)F)(kF{M#d($Vb(0r z$YabJcnE~WHS;e?HrHvSPw9VZZcQTuI&yT}e>9^guk@$hiP9|8!iUCbeK4@CoOb%W zhpF)KPohkOsitw#W9C2kt<)C7f`_Oe5{Ayum)v|BD=fZ{W&nW!|rCIZp}=KGo`$Tf74Oe8pbS z9(AhMVm7eCHWE!)4s&tL1Vkq64bX&b`V%!m-3 zz%L&RMaw--_x#??hr*i*`!lYyNngt?r;c%Gs1(%W>=8bEI(j3pS<>1odVOIkZd3Wy zA_ow|%U>2PK%9kLggd4vrQ21kprof-ON|C!u7cTZbvs(b- zi|-y+ynsntFeyvM|H#(C@3hl^L*K2o`f!iRhsX+O_J6W7`Bi?tn0qaO=*nO=WIs!W z*}@7pQN**B?Bw)fe;_R7rREcUd!~N^!-6c}_fJ0NKlw9|Q3Isquk@^=rMDu7Y}yArYu9+Nko+K#8ggobeSp z5-k7iesP4rOXCvaBMb)M<1p~n{~Cf4hlYu)iIiJDFE|FU1b=|77uBZxPG{9Lr>~q#*vU zPz6B+ViStxs#z~?@H+0%ve&|I>qL)G-TQ#)AK%-=QHyp@TiJd#;S8Y)7P~t9;EjJ# zGk!%g&}vY3=}8sB&Wnn6-%kRc!2>naz(ZUqF%daKMb%^R+dh&=$lDQ>2F9*WM^o&K z5(@M>CZfmiH-F0>=X8F$BM_~bd5jxU($e-&oJzZ3Jh;`*kK@>2!cMEAeNpG}H1AI}$1Ng@FR;AG>%A^*r=5?5p$Sf?*M=>1zqz2 z@jrGil|BuNqRgMZ`;qzo%r@*&`>6UhcXRji)3CF=>*U63CIiBU6p$kB$&eZPoJ zz$}vJj2EMpE#^pIt9tKzzDHJnaoqA&XviSZS!Ynb*EV?-M=kw2>3ULl1!rD&;&{xn zYUWtrL?0XUjM)9APIrQYv9GpI8pZF;I(@!l>aL$}Laj=Q093fCYu`TA)y(;!_Yyp^ zF`m(M!?@bLRHJ2>3p4W+x8cfdE7`)WVXn1JVAWOcx}<01P@&_`AyBJConq_pYPJPx zwfKtqB*FVR5#>N%{{ZN-5Hj4TwPg&I3?`>&ox(I4wHp}+MDhniC9-f$OJ6~B^FIjK{v)f8 zdJalo^SAjCjQ=OzMj;^pVGs|{-d1L2rN5I)Y*k5gmP;gwWD!Jo3s0Y-!;IA{#*u)r zIae`j+bCI4@oB4>2{3LC#ouzij$X;Xg5j59MmDc(3tva0;sA&t#(z`v(17`v{(665 zyOT4XP5(&&=*=_UG18fAlG>qD;MSSm`pV8~o5`M~5JxcN`eu);$j?qIM4g;o!CKCG zUZ&lC2Iv@`o`OjpH;94PsW}kGyO)Ex2z+R~|AJXSw?|`2(*z;fS?0WyogFBRRtZ+8 zA)d#La5ei~n~4&+f)S3+I*xf2)w$TsXQQ^KwtL>WcpoFUau%JKp$$d<%I^y>xyhDz7hxUi;6C=RUj z@XI5HTiYd=)D+acRv`mmM8`;UMG#}^rRl-W?5v$X-UDiJ^#a>6rp!>M;yHWwL?51Y zUKO#9BW}xn4BL-SD}DEW%(qHLI?p^C$FCQ4m}{b*lEU|zFKPdXCq1eeV|zeH@Vlno zQ=p8Y3{goHWlVa^!4;kDId~?*&ri%Nmh{+%Qdhx83Nf)c4pLl-_pIn5ta)aFO|NRrM5yj()j?vb)!{1^fEO@MKWL3CP*WE;R~;_r%GY_a7sv*L!VG zA2-m(N~j?V_#@_p3XuE4YEe&*PPQWne4$f1qO zq4OwP0bl6r;74n1p{9)#e?0{*(bU8pjD>59+Sx6mNoMVxIm+6!h0h^L6Y(K?MhZDb zX!H!hPTMqB5bZe4Ni_!6JeDPs+h$g zzZ40*PHs8B;)gb7Z(kc^&RSj@@Up(o-kDy0TNhVE%000n|2=axEGtFwjkCi6+3nQo zNdW<5LzMQfDFd83pI&?f@|8DBct?-6zm0#SL-L7~E4 z_fuNIj%Y zYJ|423ZxypG_nz_)e(;O(8B=2=xa!oJ9%wA>*0Dy*?@j-xAgG2CON#uW@JH#hR7F9 zV5sCl81(#D@TbuV=CYI>#CU9cc&gW#hXUjhVV#Y{Khh8=%wDOLlzpDjpnsu6_pN7iMd`}X(=3y8Esm+D(s(LBpRex? z54Y02({ZVSm3$G`alIceJ_laTE!f46T<-1)9d{QmrNcNFiJs1qjmtC-TpqW+MX^7+ z3>ki^Wt4q8?rEcFvwY%Qmg)qpvV#QU(2ikdb4bbYjJ<8{d+Kl--`}>gg=eh^Z%x59L}V#Xl14HRtY`$QOOP9+`T!SYUj7u15OEY3a7Az?CfJ zs*X-dptVQtv?DGm1n-WzV!X;1!d+SgCKA0kQo0!%jxn1bJl9i~JNGkp)D!F8yR%o{ zpK!ff?ag8Sv}~v5Tm<<%VcW}Rhh{5=@s)iUl8^Ydn&ChqXDeEz+&iLUF)4AuVUD28 z>T&%;77~PDyA#Ew66rOCGUZ!@#Y(o4x(`qrGx@!;Stb_hjTcJhs#OYX6=E_u*K5>K zEBUef?vReixe^^E2M~pq%Inpqa6kO2*DhEHhj8C{edYYJ;Nv<*=r2G?MOfstCv;dW zNg+KKJH@Y2yC+6jy%!k5h1}(VzMeivKM~!tG|&o>Y~FL;SCH+N&csSTzjrc&g{OIn z=iL340T&Z_*42;3P%K_9ATezGy(r^t-lp3g>^4zfKCK`-w;G(M?UKr{E#22SqYx!i z{ZXR`)P4zU`c627JwFQrB*;l0yeuQ@m8s}cc>b$~2W2V;)4&9xr{%9>tnQqTQ~zKf z%+{(wj(nJg>>z?2yTsFjuXlnw6?lp6^RqB>!^=>NDn_5O?FBl2p>0iRSeTKNgbc; zE}|1W&#H30ve}|PmT!FB<;&pxT?YUC;14X4W_ zv2v8{HF@YJbRmq-KEV~*VkYpY0b*{-+H_61esqtKtV4Enovy2YV7C@afM@(;%>V!d zl@1IXl1s+b?9-?F^d>Nqjtr~*XaSPWS$p~_*zSSm>M&i?jhORG)Mb;EOAUd^nUyQk zA2p2&TsBMQnPYG4cyh{P?jI6WBwD&3lf&`Nr#Vx4n`9xE&Bxl*&p^%hltfh)UY`5n z!gLzFK5ZrJxz{}zE5Id{ulqHE4CIM?Rv{%~0D-9{-jEO;)`h3p-ZwmoZYC;CHQ)Iy z2TShm^(S#Dam4mIa`)~|lhLeNkR9DV2qCSw9EjmjEA7XX0YX_zk63hyfW8SNyDq1BlB0Y$^d%yTz zzLr8@_G^X880P8%=2T69(zZ@_7!=C!E15r2Q2w~%)T;I56cJ1JjP3QLl^>#Hw|X7~ zCA9@PFLP3oq0ASn-2JGL+jzFFr2a>U7fA~wvKc?r?in>}iX=&-+I5PiJG^vJ{2IH8 zGXzVkkv&csKV^KV2dEN!$HTRePPp^|KJcCGqQB7uuhv!75gIS{!t!;?b1uo~KgIvL zfq~Ksa`I}0{1#3D_;)(T&Fy^M^fG`369FZz?e?qmFj1NtR%GV^l6=l-lum&ZNwi|x ztPb`6TU37+g*$%|x8)l!{5_b{?gpghdyQX+^FY6MitsVS`9)BOV9TD@IYDm->zhkW z)#fe#wCsnR_|NDxseKh4%r)c2ja@pg+>HlXwX6ihRk+YCuFLb7TKuLQlTHdtF2?iO zGSbJM4ywGl#Cy+${)L^f-^s7ohpFIkn0DuaLXOLUS&x0?)*G^~nKhLfy)JvfJZ|chctsFAbeV-TO@6fQ!xGvwVpx#- zn)ACtrMMYzHT@y#jlxem^iiB01LHrfpHhDBTmz)Ieop~3pt^u1Ell|pB>h{-#u>u> z4Z{XL!;W$vrt8DJPd{lq7x2PGXMe8gW}6${Us6fFq3*-vyhN}`^^^b(v-9lJenBY zb#z4xHvF6~^Of4iXeUY%Ye+}G(r4q|&%>92AY+j}1~+fyh^P_oj$8dEbbe04hTaFb zcMY)V+?FYhFZ-0QzdW+XzqXlt@Uy6=v_$Pq6PYt0DMBt=%02v~g zO7dkF4_Ak6QNh1=PKfEDO5xJ>0X2Dw$6mKf%L-sDBU#YL6ye|3!{ZA&u`Q&|y_`9K zm|O-E#+59#`0zIbYCEs1Q_FIoS<-F30Q^C6Vf!!rn;1kv%!d;?ZSL6q037x$I@Jsb zq>)T%^nD<;M*GFmi-n`YDUIb068ktxrr#_2&Tz8~jZ>s%zh7v@5=r9;9Da2n07IwM z;}!(bOedA!vOk6Omf~+Ryo=Wt4s-9?f14gH)}O+l5Sb=*oAdq3=GXsIni`E-^zdWP z==(?GHlGf^Sb$zj^zq0`_~I!MP9=@NFk}UI^e+5ZrBOACbcQNyz7F35*9Nrw657E` z5vr!0!fHV9qLmAEBq@9X|3!J9bImekJuql_s(bVGL-;n8iNOyPOjdj>CdPQms#8LV z-ah=Bi6~PxI^A2c07Io!co%S&29%v)Wmx(?{0P8p1#kuou+)WcSbYj8J(`_d%wNW` z7jz@@0fP2Dc;Zw;c+H|+nO~W!lFIzgnR3`na5x{1X(xjl6gsQpJCqVRn&DIjxTAyn z%b~^+SlNQ4PXqrh7_aTfRWJBCJ!zIGMk_w7ZRlx^!p}SF;k*yXNAqf^V9HNt5@Unu z)VaL>3Oa*IKvLBQ(qSK%@=Qf-i>0c59>8VrUsmP;)=XQNp45^_@g`P2C<%J~sh5k@w27?~OTph9UAL z9>ev0eCjBB2&{!t>uIyZBK$H7(A8dkq^V)>>o*heg2Bif7?Rl(Ed`^my_71yqj_6w z6KE$qA(N%Q|JG)t>Zi?6YNN91*N$GPj1}oVUDtEW}2d}#>3ZSJWt}*#y_M9 z*MoMhv~K4eTZHiWeb48B*5A4urc2N>A)4|AqvE|_QdWC1cYOB`4b262!USBz|Fh5c zP&JD6zN&14R^tp>?9b2$LP9*Nt_heSzMuTWX?wb+=AqK-pK(Mg9RhJ^Zlwz0MEzyW zV4l0mFAL5*{JU=WohRUFrG&KRg{W}!^7h3#le-{lOqsO)N;mCI0$UElscc2UeNYxo zNqU2^VR*i^*qZ#P6;}a9mM!<<1pknmw~~RC<6buj{Fdf3guc<8%(!H(u04d#}83`Dbh8C zQ?$Ha&0@Kj#a&d+L5@%Q_Q^dNjHgaN-T5~2HjcA&t7`UJgX@SgAO5ZHpuf%wMkGs4 z!4EeVggbh7C1;WUc3!Gsu%+`~^Vp52qr(*nj0KJY>< zzC~!K-V`Ja8fy3tqsk!!>;^V(?pm4r$4 z%bAI@?oGG@7yPo6Hfjc+LI3*r-K#RKbzh3mm7Aesyu3cy5emt07(bHw$+Z9dI2QG)OP-gaX z1c7|QnK|3#q0Yxr_$t9|K>!TsWNGKAHlxLWu8iS!1}a!Q7~*=}1nvWQ;&r-2$nAc# zIhY(#{pHaixDcjG6rrdKkO`l9;Uh92;(fX(^ig2_xhqjIH~iRgAbLy~@37!_`y=e#A+3tDSQf#`()kE-_or~3W> z$Im(TKK9nJGP5^v>@5)ym24RuJ1L!mvSntEWF#XyBpum%6+-r`%p^tm->>TZ`Tl>` z<+{2qj+1lW_v<+xkH_CsK3@tqG?Rda27#dm&p05s>iS47 zG}i2Z`Tv+N%6AT2$6wyM4hF#Hr-(wMPwDVYIg$qdwoUtZE#i{8{)q87Xo? zalT`aG0AZOG|H(?$iWo3u3!2fM!LJJzPKErPB*09JI3jtx(gCl<{wX#B9w>QY-4o= z!RD}m^h266UK!v6tUr|80CVOewm!Z5a^I}Hmq7e}=^3REc?Z=ctPcVIfLvjBhVs0S z_eM0t3Y7L1miVhcR{W$2Ib@`W7wy2;87$YWVyX0A7c>*Cd!?;QmnKY{s5zT zX|OV{`=`GGm2VV60F(iitOep;gaWx~gIH@-f++>Tj>}vGagBUIM+A4< z^1SbO0PS6y3s{yKV~U4%DknTpC^KVDMoh$OsfV#Ms*=61`bo-Y()$H;w3z$8i;ZqXO6ddGD3^o^{+@f zqG4`Sz`(^_K;%z}jY914V+jWI1#%dI)#>a>5IDnP0{J$;JhjJiDo^|o+XKfaNkIG8 z6fkGU*{T2+ptbqa-!B=jFDz&pR5D+?-8K|8D&W6dR2Z!37X~9;cOH}FcGPIZ)zU2q zFgFS=I)_!o;uFc_xt4(CN!2<91))@=%ZXhswIpe`uA}t*-*-jW)ET})9g4SGDeymW zo2q(pb>x9yB-$gihb*l#skkgl&hrYlWTHq}o(`3Zc=m-GME5_%OsxQ;tm@5fWEA(~ z&~tCkPGj>cQ#cbThZH#oE~d?2>#d~kibg7NGiV%XW$|2_20jiR&8h;o)mQx2a{{lp zaD~rB-kZCBde0YBCt0T_>uQH=i?l;0dznFIYQX)(^-=$?e(7PAWbYw~d|Qa=2c{zr z7`M1(D2M@U_5vxi$9#5d|2s~kDyR$KG z$)G+&w9fsVoSgt+2vm)4&8U8#^?eW@c0@7CGU;7e#JBSD!JDz8k9`L_`SE|c3ny(< z4M@9FejG<7mz#W6YlL46{Z$>CMiYY8R^ltzcN_Qlv4+iaoi{f|K=xh5!hU>W4Y$V_ z$p2c+Z?n(_wTF*BwwH}i-^>3_Q+U2X44MvML@g=l2U28A^52vjOQr%;4HMjFx2g4u z`1R~fC?tvONjmsQFL=HFW|_53PCE+p99}QDv?LW~7v6>P(z)$Nf)TPz@Im^O*x$dn zJ|8kv&60*52t^UJjRD^CrZ(Hj^|KYJ6xpeI785m2hSDJ?w_k~eQp|?OD{m`&1z0|F z(;Zrt#oL-)bO;=cf4)(q9+H9E2t)uiQuj>xTV9>I!2g5rQX>oZ3HN(zsKK|i@wE7I zi+V_-QI?yJTkmV9_hM)0WtQuA_d2_x9zPSG^pSw<rl+Lzunkm$$ z7rC}%d+-vOO0+zpNmH*OJrrh6izpde7zpxCy8{f&D9cGdQ4kF2D@z3$Dud2epdrrN zZRtvhZExs3?{`4HY;8M3ntwQd|2`nhsa}!UzXGzLKA$k50s)Rn+oSoCXPyg({@sV2 z9^2C4*F=r9!Y9F0hyC^1>5EXZ*UI-G)nekiK@hq0*o5i%&3`%egtQ8QV<%}^?0n&A z!Al#-4!5M2YV3k>2F(30gWAU7D4|NZoj4ny(x(K5xDerJ7g zp%V{=z0VP4hbH|BsuJrqvY}m|@JTf;dtjro^k{)$Aw#A&Vs=*ur5Vy!j_vIntW( zkqoYlzPx4{k?r5YFb@xJf2r>Fut||~iQk#6X%v4Bu(tF38bSlSs-XAO&|Xj~j4z!{ zoFbDPb4t7F+>|sF?=WsjI`}p_38S*qtQ#ItMKLLU_;e6S$9=6JClYKq`A%if?BkP5 z4&(-){)TW^vP1))4F6`Y!(op0A zANkr3WDv%-L!oi8N}O8rhDNbgcUuJ;2`7R?e5wh}M`)g%#Nw{A!nk&K`AjgdK(yr0 zetqvH@5?y)b}*3Au4LSV**vm+u0nWMw3&aEtx}1!#1=&fgz5LlK2hjaQl!O;TU{qq zHSz0miOfSPUL@`_o6c5LMWAL&p0Oij3e>}h8@>$53T-|}Uun1dg?X=mC3elb%Fza|Yd7yZ!`Q4YK9oWd3#$s9q9%=|@0$(dq+l7;C*hpHQjj zZ@6`%w{sE;ErjZ*x(h|iG3aY_cJEa~`OfbChyU?oDK3DVM?MzV`LZ~pf)zoW*3i$% zy3S+fzewF-g$QLkqJwg>2FvV2B+X(E2UBBR!f#4A#ZrovP~k>94_5%SuENm5yYL=_ z04pIospDHjARmVV>8uOpxQ!~}p1!-$nG7{G$TLRTzJoODPHc*28#=NMXv+|nc~x~+ z!c0k-8aWoHweFM);*dm(v^eyGd-@`vn(m;r^mwokr34*ZAtGDSHVh(+dFkx@Vg!SG zNw!b))n`~Fd17=7u+dA`iDR=KrKZHR@Dxpfig*B05zUMZx-W_Lo_|PT);Rk6pv2Yi zQ=%}^YILnIp*{6^oucnu+hUXR-`;6#Urk|ca8z2D?zy!4D_!k=t2MA}q)mM6E3#xu z`pMOIo%rD~aC&6W}vBABZW##tPp^7N?$Q^bshkp_8gb!(*5I+5Qh&mux zdArdeWWuuOuRuW00Bb=t`=y3lWk@D)?F%Q9id1E8O{^^*HNTj<&B03>oRm%_|IF^y|1L{hb_ zK_$?wy+}nZ=r7>F@y#|cb8s>@Aj;=x0NaO)}?dwdlt)NFHZo626-q6ZgmeF!%X$bU; z=MA(XhIi#-M4yI$!L^`L-!;M^&c47$ONu5}NCPlSeU}zA2zLOyFe3G>z39CrOgmb8 zw%N9d<~ZqL!m&Kld0jVx(<6Z#zoZ}_5R17)bh!q=vf}0z@4TVx2QW_~Q2vlTkYu`h zvTW%5J$-fmvmdnBC zBwKMag&RSdWc5T1@Z+u@@~&D0!y^jV^9~a zrbGFtC#>$8=ty8SQ08U=+%3~k{PjD(?b#2lgL0&vz;q2*dlgn{SgB`95$YFKlhX(C zl~Jvp0;^KbX34SiMV)X3l2Mwslzl9ImzOwzN-VPph+rxN`B)T;c^kb~`RU2GVf|Yp{$O0^y>OR}`3d_zA5@ zOW%&oc&lXl>z$7fB!0p&!*f$(ARezvzv610QIXtdBeK~juW?dQk^BnZ7j7Ft(3cFzYNiuxFs(VI=sn<#Ks z$YDs9#<_cu6_UQWRC4hJ0tZ3~R*$Di~Tkm`-EwQ(dx6t`h_AI<1OjqIYSD}{# z&oAH_kbIJR&`i+(WiSp?jOP!4UD@0A4_HuLjHibu&WVb$9P@n>8d4Ytg0Qm(a!+Xt zl!T=oE{uec5{**x*v-p!DIQgE#_L2zA_c3cG?>N`xY^W4w3MnLE6CimW!`Z6Jn(>w z|C#l+)Cv=VEpuj%D?Z>kP%pm-CLAB$)}wG^r+R0X_~YqidUHIvA#7+$)ER3VuAHNgCK+To%0 zS;dm)!NqF2uyA~_*T?235!agiN-Kh`Ft3+Py1thxcrJ8Ug3)7Z0MdOhr>M`ZbDxlyWwP#5hBS|X-ib*?tHfw77ruFaVs2ug+9~nN%RoC{8*k7^nK)D_8kQ13+GO`IE1hDtSkI~Z z7omnMOY-x0#UWtH-K|tZY(Agrbbjgf_EG~}%mH}(*7?(w**5tM6$Alf$CF$;+&qfo zgHOY*a5}W?j!^(6po{*}8N)ntw-6M+{bqZ9Y=6r|54y%r&&vjYQf?67&^19m@)iu%uI3cA@u`kM_lz_@%6_%J##$61zJNq+oTTXWt)5+G|e{@hByb*R>7 z$i}Owvv`WPa~i0)EHNf#cdA|Ee)OB0DDsOqjp`db!UNF7X@Yl2kcfE-~0UJda|p9IH2yQ#bY0bAfJ@(d6q zFHIIdzvxVc(gPq(a@&*tOWI%8hhI|5cr)_>kL#Y@?xmxBuGrxv$2{Ze3(W)i0NP?+ zm9 zNZXshA{Id9)d|k%S7)QLJB6hJIa2`BzXj-y(z?UJi7traDZTot!I(cX1Gi&!>53B? z;|FwC>_c(hZ^5jC5dhN$fa6ky`wFC2_W~&An9}R`EPg~}{~vdUyFYwt0rKgxS+4Zg zKMd_g3!;9(a=RK980cn9RM@0h2Km|kJ&RI&7%*GGW}5^Cm)dnkN&)G^Lu>b7L$aI0 z;j%$u$<=>Vlp5lXxoqWikhJST(q;uoThB=ns8|O$Om+Vuzxs&0dxseiNW^$Uu#W}@ z+We^y387eD*`J47?vR}wMv$k%PHqX{ak!&D3vk;tz@WdRHdf$253Eul^jUOhvF<`VGXLrfpbVg48(_M6$RU&({dZwa>7ZB$ z73bECm?zvhG<}fd4I*1kNJo0R4=t(n_uOsX>k%&LU>VH;;8^Q60}9~WMvDvI{;s+N z9p5Go5_gFZC?A}f1Ga?K&4f*V{$a58vteS2kdq~$e$ZTeli3M~Mp>KzQ*8;qJph&2 zWG0%bNhpegJoq8^5nJGA>kMX-m>B<_4Hl?c_ypwRH! z{5$HEze3m*$L<2PT4YHE@n*VEWA22gbs3iH<_ygK#V+k^B$Q_y@prsQkOBed`#eUz z#9}S5d8R3N&^Fux{m><7YVOaT4UnCH%95D&6Exx@H|_d#Y9iomI2nX-I79a6kTev+ zKUjhFipOW0q-gFgyC;uhG^rIkRKO4#Bwi7s`Ap~q?jR@y3>c`|dhR!#o7MkF< zeN6CVNtpHzK)B(cO>mKFiFYj**trF1UXAi>_|WwQoXH7)t&8bcUj?4HGhW{z`5UXG zeYo3LVN?)^U&cT*ki^4FWCsxq5Oq@7w8pl`h-=|SALfF(HxocpaxNXwpgfU;27%|> zY`s1EvWP9zK}hPOf;O=g+!O*CXkSs2ntB3#Bq)dApf(0V!=1q!@0Ddk83!qX07aS< zWLr0sGkfqy(+!1+1uOw|RcIklweW6!V6@jj zULOR!rZFfLd`q7R8?qu70+)cQL>90nl7Ms(4x`&h`Ilz6LFV-v)n?9s2-V*sf#BRL ztdIS(5KuJ1W=#TYe7yYn8`W;Z=Su*PN(n@qxx0&F})Y@L~CGUM9K=*I)bcIUOEr5iA+u3EUWM-F!~_c z0dex+ehCT@GcYQa$0XoP-#YY1Rmcy)34R}((yX0iOK0vJY+QUh?tK3{B$EyBs2VbN ze2jBfz9mQEDk?+8`Sfe0FIb1Zf?K$j>eXr1Q<_WyR*q3mdqnm_#4=-`+^E4 z9VocQybLl`=I$@xNCw`V{@T$vJ_QkHB964}E1(!m-zJ@%2ifMnJVZ;j=_ok*1JxKb z^@g&2niod;z-M{m9SCJ+=Prny$(Eai26_$J!7%fb%)LLC4KOP%J1xsY&f6jyqVSh9q|2PRXEWcp46d>7w;rT>$ycr4KuTwDk1zrAz18( zF=T!&1I?5!u9^SIYfVNAki-r0@hJ@_q2bEz*S7co9Vj$mv_8PRMP7jj`PH6F{Z$mY zh3wLcYahh~g?8fMdtOfOO`uoBayu!SXcWqWP<|zYx2=?ngo{wGkVcbb8St~YRWT6^ zMY;=++0>BEt=HckyUn!(J)uur`vR1BlBYk0?|;c21cbWhP5DaB>4$bIMBMZy%8lDc z)xhX{99WHOcBPD&kj3;o{LDIe<`ZO^ng#SUkGUFE)TBHWK&5zmk6kUK#dz5&H*{V- zmX>twPDZ=w-_*B40tF+`v>bi zrr_|97|It3^MprNKY>B^AAnTcf5jvG8YKBIWD>CU9mcXlsE(VOyvzu(z_@+Ag@rJh z25NmezSQoC%Q;Vxc62*{mq|kjk&FH{ga|TP005P|@>X4P+f~xZbPtlRtY8R7=0(eX z^e2Yo9O`lNC&m@DGC8LJ+;t!%^Ec)NX+VTq>bio9 z#3oQV%&U~nokj;i953YPQOv#XiRv%l0e^9I{rE$mhrM=%NxNr5G4P}R$xx0{!0fsrpL-^GSCkNlYLXtrUjygFgz33=O0j;qvd}otLG{>%)21cWFKJ6dyhdS1ph6YcMAI z`cD+2+Xm^Kq@mK}1Gv0t>&#M>U|Z;=uKVp~t-xpw}6rbsk z!OA#_ya^j`wZ3UHP@w(?di&a|N8i-#(@8D?uuQt-UbZQI=eIJw;!b)dr~H zBp{Ih5Cpt1j#{72IZlHl%4~EX7tL+!Sksp8v1FPxYqKDy-H8vv=moyab-trck6MBb7sd1^PsPd!u&*X0W8 zXl}fy8xfDt zJ4K}0b(9BE%A|t<$2Eh*OVm&`(W3|zb_ltipm9y{)h;e4s#5+7XKLM$o z0w#bU$Lg7aRgT!TmZ{%igJ}!Sk@1{4XPH-O#oV=nTX)SBwNYuLRpepwf`J4nhEC?z zP}su@U}b}V=)oAWO2~sW&JZ*WbVm){WMGQ?UrS}gwB&&Sg_GkNRw?>#7hpn%0qnzT zcW}$nsMJt~w168E$=TnuRwLpf6oJJ{mXB2(fzXO-i9rpyqynyac)(P#413?;Y4dp@ z>={Bg8;m0k7SjSU_;YzrI}w6xl34Vv3yz^6A505Y=v^HDAZKu%_AQF1jnxgjk9L51 zou;td+79v_lJvsV+=W{Y@DDq$M&8>nl-P;bjZ~YnX~f(}bTD_(x-dX5=Qh?6eGj?0 zuoC7PQSG=EzJULI+C6ss0(Yp*92AJ}(KadI~?OptTPZoIlL5L;Ep`l!A^B zP9Un!V9I2gB#jNTMp-?DwxW2HS)a%LhlX>aW>ACHUPZ_xxpXSz^Dx2ht{NF1E zT3pUi8vZ`}NSGg60DQAmaSC?n7w#_6Kk&I9OEa(xGlf}ZXSzPCn7s&UiN)4skwrOB zRtxO90Pd>PCcj>OJ2uno;gJtKH0_Ee8ovcB;{25H=~{{?*}&n27Xr> z7S(SJ2Qe9txu<>QWBxq9YWhInGo%G(1~!de*Qu8PeV{*C2($78vnFJr{foiQZ@^tS zZioSxqd(bX`mt8+j^ov7mMu~u4VNE9hAFetJE#WFW)}8L&`2wwQTEFlv>PE_p)|pN{)e) zNXHUK$yYs|6Y_IY_i(|x&J+c{Ic;jUztYUjuT|lal?Zb*?q3BR&w4;{j10-u6 zI(hi+nzS1`hN2{&BcH%Dd>*a<<6GlHbH(>$ zJh3^&tIhhjX63{k(4QbM1iLcqx>%R4c+M;|8`kK;2v&hE~8zB$zvlJTy7@x=qls=TeKw&51}hQ4(7HXd9TFK^Vwo;tUhI0uTMdTv%kA zEi6#!-K0xQz!9~wh4!svT_1c?M$8gjOB=zzZPfKtX8$9R{tuI0>6G^R;ybaPuDWLw zI{?PbkNyP0zYyAFD9_#DR(qd~5MsQr7KE+yO zW-C{yD1%`J-4a^wcYh9^2JVIhp6{ha&#J{(`yKpLkl=_DxnMgtoR5{|zeD(n9F~No z>e3p-r#YX$yv7l3_8Tq`Npt_G-3=!h$W}!?nWGe*WCUZ0sgJlqO^tnW!X_9&Yy*)k5~-+Fk`*JNU;ahdB5$!oWC)a`MZ>_nQhV~?cSn~C+yFl2C?Y( zkD;b(SbgkEAK=t-j%=xS`@%tLSgN@QNrY&VwkJ{Qs+2EE$k&l0f$jU}ma$9o3U@{y zShgOc_~r=|=P4B}ntppM=eFI`A7VL3)jWm!BGN4$mK~gFNdb1J{#3_vP2E3QS5=)F zMRu5Bq-ON3$=P*%lR@tTXu1R7d{hz7tB1kHZO9S{6Xgh zv$kkppRsKyv*r5Gf|A%5;kUw_-M&C`@jgJ*1#XwvyoKTOJ7sySoSvt-qg*gf2^~D< zED2s!ZNzTa_X_!+{O(glzpIpG$p!7U^6TTr$C?La!R3$qSsCK2)|b&&U%>h;%P3o9 zEGm@ezU9PILs9Cou${I5iIK>mQLgr%sD&gy$VJeZ(Xqk#zRfkoarx`Oog`uUfC{|g z62KBn28C5{`il_x%yvX*!l{w#g6BH@!($Ao7a|pZP@;tBExI=t5(xYDVh>7w7*~oY z`cOXSKGk2M0g31)G7Yr(n}&>A(UD);ZI4m z2r&#HW*KMcducr|bVKMB3)Q>t`#W%pR%XTTgukn;RCV8fMk~sDX@ZK;w*LqJBy!h4 zj8cVYO{g2N{S$TJVsW5MJXz7u7GH&d;gJ-!GWwqDxq$C*Qv8xn;)w%MPgZ7*CSiWrEyzjT1M$tF2W6*{cp&t-0`FCzsMsQGabBL#j z5u7^p^vO9**6e>HjVC~Degy&58XD2s%yyO+-3dF?HmtO^ebXoo-{SeR#H2*cnz%l6 zk`K$9pYLDLZN@2aMeSm)*+=mwQO%pSx9f-IyWw|BKGguh z(WGjk`eQ}aZSUYLkaL8>O0*~y4|(+Z9HUUzj1k`^GKB-oM0RC()+v| zG2SFdEQrT6=`+XfvQk1n0XZ&O^iCkBNJ)2)zAO7ZoNOx%ibWz;(UUr<;FCWr7V*T| zuCm;!7RG4JAdttuLs%yzz8XwA;3gVK%$6+QZyFZ@E4B>;?n~(A?Unw6xG4B!(UPZpQ2rrL(36Qz%rXi}rtB81n^Y?RFAxT9s15P8r zRk@Y9lVDbMm^PFj?T75=+J@Qk`t-Km;vu*>x3@y=8>|@Fo7}8=)rL}7RByBr7!jTS z7WM-t<=Yxo=~8r-By{ij0e{24reBm}C@k>sn*7lwA-H$V-SllUMy7qQ zp5Nwj4W41+deJZ?m+8-T+M8_!DO5agO=2xgszMcJl-9k<%kFt-sW41fS63SEO0#+v zGaUICM{xdKq`zm-ro=PTtSMAiUKrWxH0A9Uh1*XA+pmXx$iwBkkLLF+l%{?*BTm|1 zsC!>cN&M|2>chf-EH-652~<$Aj}g~m%QrB_@t1-+M?bl9$b&+w*#=H8oSRYJSWz6! z5=1ohfS{j!t4p_)-@5|}s}@8EJ>Z|j{_9f;BDc-*wa5(Ze_1e#TyLO%5Jp$?H8SYC z6lOf@{F%EiYNRP|!(|YH2#MQX2-lYGD`;a)uZw7wCRT|wPaKmSO#&4OpQ0qP>*bqY zTPc$XE{m7=hkXIya+aikqwf|xM}Y-*GrR2hOvAVXGSILye2zp{y6jbKMA z=mn|#V5E3=0?TatHUL>_b>R!nAu2sXSz4!G3aH+DKUG7E z8k1Lh_(|PXgL}wwh*2`886kbfzNvJrVM8Q=mD$%hH|}=qXhQh`RLYk@@S-z#kEQ4_ zd*6IZ@G3-)(C^{~zF{O4WMBh+zORmT-XGunpn=?T&%a^1g1fc5iK7g43*ex+M?mMP zX&;==7TZ8hK$NV()k^FFI9aEHM7ND6why14HOWr5tmodMQX(cNSg_R@#6;Nu*Y&G6K|XSp@2L-(=upgknBg3Vh6 znV_!le6F9MN_c#!=^XJ?Qf^Pc;vMgyjgKs>dKzDjy+bJ1>ewmQe%hbvyR1v@48ukh zg=gBS{Wzi3F%endav9lq&`^0en6vHsMI1WV=5Uc_BL(ZWu5BfIM@ET@?yFD9u8N4n z`Nq}r{>cU#g+S_`2M`{N-lxK6)h}|*@U{_6d}I~>3$~{Cf~&q?f3KMF&h8BI2Jc(} zx3!t|p8dK=JIL6RAES6bi0bQ46c4vJ!AZ~1t`9w$UQCUthu|Dqz{v%Lq=#9-lofcr zPkJdfSEC2Ely!YHeiNEw6ITqLd)W+aQu=u{QCr8J>n2qAB)-DC61HL;ewXO#1R&B0 zyJU#An1Wm5^jg*|M^k4aSfJ!Y2au@Kuz(zbM{l1e0Gt-R^wXS>FUOc z%SsOH@`0af%3^;Lv-0u*p%Gm^OAupB$7&1qhQ$j4YV9VKD|6Q!9P0f+w<@%w?Ta$K zzj(3&AN4)+pBCUG%{J)=T|(U5Ozjhm45hDED&>Fh{30am?Tvz_G^#e{T4I9XsNSIJ zk?UR;For&yJ=cS7nFVRjG%6}y`0%8+VeJ>r!>-B4Dt+K%U*780@#j2SwvoQq2B#}( ztMB`$t!0+&i{gA*VCvINXW2A!2fz27$x1n zZ$Bx!&!Ec&MvtdMeVB-mJvRu5w+L4AUwp+A+2Xl-uq5arJ`U4ow9)$?0Qbs^lS8C9 z<*&@bit1iVmUCGFPQm>8fj&z|SfZ=ft}EI^1fAAaiBpI%NObRHOJ_OB)LTwkk%?9>5vQ=9>ELM!?i^ZYBLOLq1>Fn6T5rmv)#@maB=lI3;mI{2-kfxN&TOl9USn zp_7e+AUj*Uuo<3&D?77J5^hPo^&|q$fKYik*^g!B5i{krFssbjG4@d262jBH$InwZ z)$W|smFOKPfHnN|99A{i5!cJ(G{d7~317BMURae#bk!KADLcS*)#cojROr)kf|?~z ztTKH~w+bANf>yqiQQ{Fw{ic^N&!3Gj%~xiG^N3+-_u}L5 zNRM)GEz#UQD%&%&Y5f@76B%PY2*-w=*i>re5{4th5V<|oM{j_80}oDT5RpG_5~Lc$ zqnHnPclnAHp0w1q%799OEtwZ1XBfKt^}Q}}>5Crs99l0fu(vsLD%oY}b zR;=Xit|Co$xYSe-;U4*TKj6?Q)@9x&)5moXTd8yY+s6wz`$qd%SUH$jeqCXhk_g|@ zxp}R$qN}qDAP-P^<|zUWA!4J7NPEF|X49_)$oSkOCP}%UxVN3dGmA+h9(>`R7z#`E zpMS{im2Sr_Yv>y!*-M9yQ4ece{5lsETT>Jq$QAxAL7)PP@3nNlOA-afT01de+~KIjNK35&WwXuz)$_dJybs z*EZPw$@fHip~|~pD0ul@7uWPHG1}8g z$$R_C`I%}U*eVGwytXQWU~Q>^fM1Z z%jHf4i!VPS(2FkPV@SV(Yq1-$js4#EUmV}Iumgmc2}KxA)Q2YMiT5@wx-Il(yzGY$ zCBebUC+CJy(Le4^>-bs1V|JUSQpyr#lo2nvhLmk%`Y;owS9nW!1k5`>ycwUwIk?C_ zwN5Fc^lMlqdo5#Z7cI_47&Sy>c0XJxu@JDeGf(VHeWg z7>&v|WjR5otT%hvs@=4VDfnbccu=&Y54Y@6n_sC?U~kYrry)J*LYo;5;e5X^XPk3$ zvCyWjzl=lU`4TI{6ydlUAFQvBPxp_PSDtdrhHNZbK}v_87TYwtJ%ly$1cmsk6$Xo} zXhc`0b@<~)0v|d(^-mQ|Q`=D^F!>qt&}ZSl)AL1MUc7FVa>es46|tY=YQ-4FQW~RD zBaUWukwCJ4?&l!>$no5@k6VPs3#W~iNk}Bjc5k+Be2n(86S|>>eUAGi7Px=RbWBc^ zDP-qVvC{@-!KG69Zwdaz?Q$4)TjU#jhY{p~O4#m0_wuRj#@cbu($Bxa*t%_bO`otE z2a)~yCBhbx`k&)0a|JL{m9(Bk(io;1hvGdihYueY6`bcv_~2NzSC6UJtL^8X;Om~F zVfy2rG9HCozKDOT>g&wZ;@uqg2f+lb_9Fxs0&?{^bD8SxBbB$n0*v@0dRurdctBnL z@d(?2N$^paqKh}UOP3MwZx?93`}w_En^xUv`I?l0^^UqrSw($`q{n+OG)YI~;b$c~ zReY(wnEB8ck7!KI3jSb*Lx1`0@6j<-yz&XTmFVVNcE1MoDeW@ppE%-|2ccZOn_#k@ z2d6kH3P0QD)-FxZhS9z_3>hsGd6CRP49_yn_d#-PpVJ74Uv{1BKOXI zND5mm?lo(x??w;U@lD!Vyp3s3YJJ8^`3ydf#ZV4VUc*YneY45><^(RC*5hAoc_-sL z0UvU$B@-%}P?Yob5uQBnH7kzN$#FD|PjW&WJ$+myMeLY*_DP@G zY=$K@^+j9S@a9>LI=thMO>K|N-Bz8^5sGtlrp?sEnGA$e*1a3IVb!)}jkFDHdhJK| z1V@C3X6HY$-z#ZvDl>oH6#`;|odSadwR8i4p>Nn1KSz8EJC@IZCSS>J&M}X%78XF( zw@A5saoH^4Wwna~tVP`cnr~(Mey5V9Jp)PnHLUR*QX*v8=tHAk{>_=VarjzS<=Nj6 zY%ty-L%TZ;?<_0JyNbGSKSzZdFQ=zV9Zs5vD5@W0!G*oQO=r0BXo&6@!*?R*w%&4= zP7Zk-l+cr})SO|`cOuy~`67S#U-y%X0KE8p;i6&{i{IXln0zSKCR4B0Vs2N#^=fE# zXkWM5b#c7XM)iX-h>W7-oLqdm9Ao5XyJ?jj-jD7UcHvEV|4+=|W0;~fV2joA zyt6v)6I1~}JqdMXO|k^4$Vfwz0AVRN0y&{}lV_Wt8JBC5;X*+(-E1e$3ta}c^cmtW z+VRBe8dT_8S&>oSy*NP;SrE(O!mm$toPxSI-B$PsXn3-+^Xs}@(9*;``COaJ zFwhE^m8$mA=aI0FifS5uFpmDppeuw!yfnD>;gv993v;RegtfrEZNSuRe3zLaR&Y|@ zHKFcC*t?0O3H(eOE(=feVZ%4un#Ef@Gvl!w4|~PewqYvvs%=xRAep~XD~b6H4soLG zB659WTr}m4Z|+ywZChXSnw#y4>yHb?X`kTrL>81T9bSxT$9!RP`?h==A_n3Sb{Gdn zWRaROCyaTS-6Rp_bhwoN1!(pw$0I%k-tS7xzll^x&pg*G`ADiER3uR{g7Hq=4Gz+~ zL)3W-tP8KJAMTmlN1d?WYDw(M%%wn|iBH3C*AOI1O8u&>bzI(y3#|Ki#Ngp6T4dby zChkVk+-X|>GV9X}6*-+%;l7LXuAL8RSO+vI*kogHeg~|W#_D<(J8BAY{H4J|_q+P+ zN~|vo=~zg{;+0FhWq(P}-pVsLx}qp_adh4uNBg>G$?gd{j&YZorddV+2^}(XVSG~t z{gY1f+~nV|aZg%L$)1g7PNO z*roHT3Ed0xj>WjhHbFbR`{lwIm9jp55k5~otqEY1e2HgBZu-UJ>=}VuEk3 zPv8HHLopb9X!^eY*sw~$1^e{(66iKeJbgW}RbK=?>qbyIr+$ZXjjM`Zn4TzCl|mWY zQ&}~DlXFBq5z&C0p+>@F}|>?kA;BHU+TCtp54(!3Yo04YMq#wO%; z>m&=Sqr4xH1v;Q_%y$*zgHK&->{5okPYFMhA{(lwfp|1?HY+wcTg^e}l;jdZo>kfV zGvO|5SG;BSZEHhlyYYdIn-RtG>o3=oMiieF@UldiCrjpiwTHt8W6#v~7q?#dMOim? zA;Bu3Z@?2K<|cxT>3@td$-d=aa770?Nd5pEdR}Sap|~Yv$JHY}@wL-e@?V!=x5BXN z_4tYD*U>ponBH1&1n&y7=u(x{+gDe_bp^0|KdlCqIX}GJ+<&`vZ9?p)Ts^{@v%}(F za7B{Gwn915Ns{j_o{m+8_1D_vTk2v@&sL*oc}5-m6$81VXkZjo4`0P6U0vl%j>-Yu zK*<(f6$pqg`)CU*?;ibv@TC634`9}E` zc{f)tz!>xO?2YU-_v4;Ex+g5}%-_%vAL5ply1trr<{nY9{vqszkCpYd6B!oe&2ury zFhaioL!6Y}3$IJO?Q_oR+n{9c??z3yo6$pfc3o#f{xgruQ}Y>1UWJ@kzM?r|aeH-dMs4s9Gs!#1ocU(q*|JeG3GK<6YN*8%B1NJh3i($T(cAajbTn zEPQ*ZymrXI!EBcbIh)?6&}PLT<8>l%cW!E&dgAvjv8-Q>d7#dNkG2Ne!p+we?SepJ9*9FelHRB z^)L2yZ|9p9W|J=J^s^~=-OyG`F<(9Zxo>IxHpv_Il?kyt0F-Ib751Mc(Gd zyd8tYg_WSsO(6gvqj6?Ff67Ynx@Dg2K4pW9$8Zb-CnacXRf$@@1_jpWweisT&iF@B z@|B+0(fW7b$*nMA7wqH5L(?MN%yEs>#e-kDUQ_?$Xwsl_xj-FwhA-q=h3$*VgLyWQ zI-Cq0PaHNfbuff4@F5+RgRIQIv z3#=j{*^|$?S&QFkQI{>WNfRH1h9xk5p}PbwCJ8S3DD1h&f%Tmx+j?Q9^k5nOSxY>X z&U-;goHxma?po62+Ce!R#ABn|wLYl|g{k359cg*xdCca&o<5M`STP<5-Q{QcFx&)ehRo6KJSa^N(cpZ?-r7p#bvBepDaYO z_<$Avvek`doihdjTf}lNlfXBK1>jMc5B5l1bdHl^rHT6z+kC{(T?$w>HdwWy7>hFQ zg_V0mmKB%VoE=n`Z74`#R&x~D znx=H$N1r>aJh?Wx_ro-QhH>}z_G?*ep_f|n=ge;2 zQcJ$LQ1Zp>oz=~!vpT&p{HA=bjP?P2__`9?QR?`zqjtyOzk~xb9b~0r7$YSPEEq%s zRztB+VXJQ3%zCCxhd4Fbkw+WaKko^gSv0QhZLpw2->Z!Wl7R3;zNBNH_B7TZ~Nb z6lMv`Ig7q&#bTQ>%x^wwtYs40(Cgt(Hurb$Fd92#ndVP2F5G^6W8~V7`++!v>t*<` z7n$$yvar|q*7h5*nP9cm+>H!3kBPqOD(m1uxVMzdZ$V}3WYMiAPFvA9PyAfp?4Mk; z&RB&@Myyp;wIOU`G=E`2zuhZ0cSJ>Hl__rjS8D&`(M zTm&exgd}eQjVxz>R9cRA4G?wLxvpb3PC>leW!Bp zq56nd{4Tsh`nIu5z`=r0$omq5K@B`7d@IX1Gt8Aw7$;}YozX(fl@a;!0^GpobXZ?` z^)6gJDX@>whP#}@s7XeQM2I?>F3k4Z^Kku;gydnLdp8+_=4a*4g$uqq=bhryn%Z;> z3Z{=F&L1ulXLvky^s(SOHxhU)0o8SWUi+BnB^tGhHUSn2#{F zeA`ocNWLRRo!$_Ma&KYm{Yg^&#C4Qe*VSW*d>F@%4;gIC3`v7uBt@508R6yBYO}Nr^gf$RV(n2L2}WOCC@OT*d3* z4t@t%I?u*L<)?Kt{tH)T6lVubZ{R3p(8Im}-C&fm8P~OqnR*3FrpQ&nbWgusa3mweM<($Sg0V~%7+gKNNp62|Wm|IXA5NM0R(YwgfX`S^i@ z%TEhp@Xoi*dM4Ui8f){fjttvHJ#3c%G`S&b-K!8}5 z!_=vWtUaOC`2C=`p9UGRcO99*^~fIbX69fVP}dJ>7_@*(upri;-%4F`JbRM}3oufy zeFfFeho^gQV!myy7VsXm?kXL3XWk7LWP9`CBKo(VdRE{ZnfNTauwfyk^wV2g#!>Cfg;d z1mhogj0v+pn*4N6_&OyAH#9OSf8gab#UIEI=GXG`@O1i7gJBmiNC;1LP9BbPJ{bGr z7f}-owgE>!36a5Sr^7^o$Ug$V9&D`~5CJE`svS#NceGx7r%y8@p2&czppoE;Nr-6i zO01FAwgnIas<5im`Th6$NP#sA{t*X!ZfMe8RS&d&C(~zUjm= zUMVPrF_=a@r?kGo+E&Npw4*gmf^6Gb1-pk!Rn;1sn|kvtnu$=AnIwZPI3TET8OT?j z*`*EFD!17JO#kt+LC7Gi0KXvnqTz7AKdZ|G;n8opPk61e)~W8_aHTjyzLiA)K|X4C z(zP$QFA(EczZ5?Y!fVt{@Sk|gHbZjBtThyuYm@^OIU7R`Ru#s|I*HWPW;MrM|SYXtiMtS^M50X58 zyrJa`C0=^sNZxgl$wdqJsM~?jy3E^#Hwrx%xWUG0(1Ro_!*`X55~RiGz-b3wLoWho z)IF21m)J@#5~-RRK5%cvqZxX)_O(pYbcjkbPvKcI>#2V8UYynw4PmI2d^-?D#KFJj z{p3X>Iaw1mnasYXGFGt->u{YMG?TfV$?rWaaHz27%U5oVZ#TdfoaOn4QQbCE8M7@&C^YWf8Ux$>t zw3i%Zg6_w~l#a{tC3Nq*B-n?)7Mm7_e}V)l12VS@FGfV6*FmPLu!BDcRH6YA?SEPzX3l}CH03*qZy;un!LdE&Td3pm#PddQ+rd)(n>cmsfIaQLGfBx7Qd*UC$8n5WLQQmmqv?ghE zeBhs5l6hD@$6+XIbn@6^QgJ)-^BByxI~xk1yy1!!SLW!?CMp3I*shatjen=$UpB=Q zOkBhLSw!V#%+i#Lrg_4>CVpWtqynhM*w^cSezT33b~Au;t4Yk4R(8;HUSs@sN5QT} zUJ>IfeYT_5a};p;Bi$BZ!l}l6#@Z80cHcA*5A&dfX(?81wC2i~Alfd{PZ=;&aIs24FqrZA&UPtI|El&|o^aiy_+)kS6cBsZ#deQAkHIBcR99#P~=~N@&NQZ0Y9OX@w!VYN&>tL9MV`=$~UHVUrLAhNNADQwox7jHK9x4`AG(qyN zMNgJYalWq5!oeJ(-rIqkHnIC1fM!13YF&PE!F`g!oeZw3M#dLD*omRn^o;Mm4tvf0 zIuSaM*+<;N5aQ8Iz@4$?i9Ku}&=-*F`1SC3d?Xau79xCN{LT`@JaE4gWdxF@&GtO^@)~Solu@rT)L|b;K$A@pzPwbt4j#(dW6;C=X&bO@5(PL7c`{$_2XKY^PGUR-rU`51-`?2`VNJ>pYBP;ExDR_ z0;8d>B<=n*56g$G3Fo?>=+aXFuX4>@Cv!6GMh#5HyedE6>|r_V1*TQ*sblCMrG~hH zXYnqHrslWGt~TRT8`xL2lmE%pHT^xF(bwny^X)uXKWnvZ6+hOczW{M2Vdza0kF>Yn z%5c>0`>)f_awfrkr_jvEHH5pNYhemYOgmQ$H5}ELrNdbE$Y@Gm1yVUhW-)s4cOytj z^rxR)dGnZQ8*iHq%jcT5{YEdH^-IKZbJkFz-Gh6V`(TI60rA;ff7Yj;e2qI!_ML$q z`FuBbdCI!CX4A^qOgz}#$j=bUPb3>Eyy*MhFMR7iwG*^&PoZXNtlg+-5Xk^@o;s@k z;ElKu?K!?b1=0nXu=J>{hjKCcw(TvL#OQ7uF2Rio3eOI8MRUJn4 z4*@TFN(OB|i2&g!Nj%&LW{##ZentJj?W?ZY7wZVQ(_iyg95U6Y*W`w1`UU>3X$lQ# zL5W^hIj&BLl@y&#(C4w`Iex+aG*cXSb;3Xzqa^F+TLcmy=H-JKFqdm|K6vzkiu{jx z%162lP%o)CQ0mpb<*iUk^gstyX4ac*V<4rua@odxb0==75iNF z1RCpiAaR54%2Amx@kp_A)lM|Vw2UHCjak6y79Wmuzq=HDi)Si48u_#=5;()WbZP9w z54*{hzsnqyE&oA8p|4yH(qYC?N?DkkxPGJ$d$q`T`O^Ay^v55sKBo9SXbxDOsej@e zcE|%)Zz!)nbN$L4IMIU-tNMJKMbA$o&i6aCzx1_ zmgrZ>F1nW!#oD$@xLDVX}u(nWBf1=~Bhf#NEfW9g+U~*|C3g=I<&xJu7>D zciEq^**BHUWGugIV33Oaam-Ge9aV zHJk}o1O_d;O5kv2%}}h8Bv|1ryWH*XEr+^JX8QJp3If z;m#3gafv!SXJ3R4{3(q8sIbLYYV)BquuBw9k#8KW{W1eaGF5X^BUn|jA=M`Xs^OckqbK3uZm#W%yO%ZDl6v1UMdobk;@rmx)A3R}ZT*Cu(~xSHl^1{14wB zV-e{%R`7h5#nuZy=NR5!g_)g}*umhp9w7OcF-#F!AL$s{QkdkvJey`lGHVxCJ^MJ; zY%*QDQ?hdj8Ck4$_lyWeQwvOL)zQZhzKmkhhv6MgGqdD7!BWXP(Z2k%Wc@_n_WgPq zk^^Rym&aOJ2}{<;7Ot*0FODr7F-dwaL9JZK&6~?D^st}Q_8epqf6!tWaF=L z^RLgKjhRbb5|;`PZwLCM>bba)TAF;CJtsl&x>MENwB)qS_K9fbrEvk0Wl?yN^w*$BJnbA4ha=~AN=5%I3zfFVm7ABYa zas&$foW(~X!|660gYKxu@n#FM{mh9V{=!|uwKH6D(&4S_6Bm=~EtR1`*Q_}%@1jF) zbkjg_6DZ-3FI{x#})-Rno4V2 zzt7&x%}LF9@A~O$kP10Xp|Nb~B+bgrj}K>gBCdT%XF!VAII(9%=d|3Ff|2mxU44_n zN6^VpX4sKiMsrKVlLj z?1JrX@6FVU?Pz@)k*y}kVX=Ao>00pqEi=8|9j7OEXc{=*wuuZST%{dZo9$kb&3+!6 z4kn}WG9v2YQoAJmK94>GG9;yKG8pZS%jFYpn2~$=;ZxyG+ii5RqtKgXWAe$XQ@)e@ zlgTe;ewVDgx~G*U^b;;UY$8YhGJCy!DiJSLKaw^hZD&f2OrtAn$ zQedVjlxQI)Qw}fR{$WY5bd&i$mDPc_q;HwG)uQ!_fzizBe*Db@jb1NtujU26&qoE$njAY%7@RXLS^hP-mjionNfYOK z^kT7!Qv%EhtqVHAQaM(Z!Ng94pK@AeQ7nO<*9h8fuo$I7{=D|Zfk(*eaAiZA%oj%S z6<^SU5F{|ZxyjxMJm~2Rq2j`Xrm^Wjs_lwBbqRwtdadBQ!xF0|%uk&>{KtV7EoS1S zg?j*NXt5)KNzCfK`{Rozn5$31#%+9X9G0~qxt&=rJ zur-ab6n0#e=pr5fw^e3W@+3RJ&Gu)H-hxC#cFhdEssJp-Odtu1bfDs7hVjNUcb%#`SDiaN>JMxe zru5-mk$o)2u72N0?0t!ca(VQ0H7)|TiNc?Ka!_gc~QGE9vZM7w8i8V zm%4m-2=-pezVwxD_~S(*+}kec3vI+D&P=x9t=RI6`=*7~udN9UV(wq5c$*6QjJRF= zEc16-7gd+vS3g5SiT?cKn}>2yc?Cpo8xDQqVKJ+!Qts*a`JWG$Q5XE!OvvX+b zA*+k!?R!VmDfpHLqjL6r9@5xGBUm|zE;CMsJAAo08 z0(RspSPJ|JOD-eRr2(EI$4)(rioA#T{2%zudEv#nNqr-{RgOD`)Hj`{C{sdh_z1U? zc!CF>fGkRF=XNT6)puy*{NyeYj^^oCkpTG=>x-v1Y`3J*;b=qHb;1BmZwf)Ieh6jr;&m+>AbTS50qpPMw1>w=(fEOv5>{J9nNTJt*EE)`0bRZ5jU3<+c9r z5<99_iv%UGyL*F0_C|4j%MPDU&}7&aiT!k;*d6(oQ3!e)h2gm|lx)XI&_}xb3KP=9 z8xb-LgOZn(gW+a%CGeftW!%oX78fSxI}9lju*1hl z)5Nt4lYMv*xy(RuK(cRHGW#1|6$a=gL-?V^%;2A?o#p1c6X)2RRvlt~O;#`Z!;~J! zD*Q*;1KIGVx7y>%@^oQa?HzXkA4-7JWwK#514Xxrqf7kDT^HjPmZ*;19iN<^A-4k? zI4&WzAN1a>&W<4BC~cRxP(k2drs5=mC~>OSBDdqXMp(o`Kawmzn(N#`p330P5-QXm zlNuDe*#*-&P1zIL@!Rp$nWyS!wKXCY2q$k{!iao1%6bjq*Q2=K+DemF*XBs}qB!Dl z`<9*~C1jt-Arrv>0)bmr2t`)I65V)*x&atDi&N|M-*tMm3+s!M9ApG$kJ({&Ku6Ew zF-24L6%IV|n*mWO`*DYaNpq?LpS?LFBOS_jzRc1~_J2+ho_!@1$z+N7tTou`ogYc= zvA^I^4Jz$?D<^@j5t32ljO%aal`AD3?bQq!qWxs-+_|wXW)E8qW;{z>DAy7 zIPp85aIDP-#6~yxtJ9bXT zk6#WT%X_$+sF}e>ln}Fz^AdC~&+iPIQs}IBOvNv4vb}DB$H)#@iJ)T1mB`@QqU;lw z{xI8r1sQgc3L8x2r7k)2u2h*Rxz1kC#7ul5wZ~~WqG1jEFN^UM2m>V3kFK!{9T2F-s?7Wf=>M#( z!W-A|vHL!1OkL@^-1w)&>u(+uyKKR5VJ1jMrDfoN=k!w@lbA)5-+BEA9(cCm*uGT6 zaMTq-usFzM-%)uX7S|2umh+}K7+^!r(cvCsb#$$ZBpt`!T zDYx9|>6a(|Lahdur%KVspFlKGym3RU-{ulvxifWlNwm7?Q(1eL-WVxGrh>+%_LCvEYY?* z^a#y?j2rdC9}LVI+b-Kv=xJ~o_{@ zscgKU6D;fX`9GIMd+M9p{8L{ZXC9hFW-CCfOeqm8T9p|Q1FDQtBhMQeHObJHZY-l04TPl@Ox4U+VBWM#xOF9d%9+|M9+AjbL2eWh{Zx<_9QOW#W(vw^?sp z%+l{wOb3slJzDqrRpwdo`s(*~q!)GZ(Mj&AQdg!5_dJAaW?a_ZSsiQQ=Z=Y<&S=z2 zYq2*Iq)GIOrP`XE3A-@Dv9iCrG(6@7)DGwVNb&a;?`hUc-#2H9yFDQ5r2D&b<%K9$ zZG1r8y?bu82sDeMb8}2tm@-6!)u zqy}VgbNp|McK_u$_4@eU`6nQdd!a()!OeT*Sq$WlQBRKqGi9`Um1YY72y-7j zqL+eXH`BVvE|!o}7{=$vANlf^dDzH+KMNno4)OcOUiEUr3=s09x_qPwZ@ z>$4G9^btyf(H{p z-ylu|-F{`}Twk7%O8$V~J$_64KduryW30b4@iNRPZk5dcOZrM9?p@(!yz66P2H%W> zeSc~;4+6p~Bg0Cw%%cn5unqEzn%t&H0OydUVNdbDJ@BS-aK0)v-9PXkc?orur}f!< z&S@{KZ~;1}FDr~MiDxbQ=dQ39{O+4OfqdG?*B;2oJrXu6DaOsII2VCy;zO8=lQQuj z^h=nqCc|-;oD)-}c*fO}F!AcKilX3T<6P=f;Icj9uNEwJJNuIbj!f zr=iYLo*g(gXqE8riQh(8Tv;$UN1o?UMDrm8Pj~{FW?`79!T+;9U1Zs=syS5#0T~B$ zSCtVNEZJO-7s7(G_1msh8i-!$sxj^&D*g~muI$H={QXDzS)-{hA0NoK9P$wO3TAf- zu1;~+wvKH>v5XCoV5|>KaY>bOL+uPG&bn>|;-qOapW<%NF1*!x1EY(GYK$q+F`5T` zX>Qa5_u)Bl)j>bU%64i_!{VpyJa0eWH0ykg15`#xYf2~>Q<-+P_53W1ta zN>ahpDm#+>X08l6uM8wJ(As!xX6$N7!aV4n^L8ePgWSFlh5Nw}uoD!p#lF*dpq$|L zKN?JDb5eNrwcD+$gEihDYpoNegyODBdqVvauUi6)4@!Qf0ZnP-BE0q+E|2GF_`9V~ z_LvfML;c2gAd3-O%OTK7%s<%i8Oi8PTZ|iSoW60@Z~yD9t?WP7`Z&7uS##m)%6h`Y ze);eR_icZHFchhJ#ObEBoQjfY@la*=U-486`&kKJN~Xc1?cgzi4BeT9loDQEqVG+ zJK>`N?`2eGQcDr9)HLJxd*L;3eGg*#!wd%l_A(`DMK*4DErZxnb=Ho$PNj?JANc@+ z)UeL=FPsv2!2Lvt`3PnJ`%g^qMFK0g1Bi6YTWxOrYe@1>E8q@O;>**J-o2YSvaX%Tj7MOg2REtAem`!OBk1cZo%aFO5?;esch8o{J+OsOo-SM| zd;)b?!$60hCoVw~zl`ql$4pz$+;Aug#HDdk=DcLN1hqQm@6Q~exUQrI*>P*Cb~Q5q zYo6J&!lW%R9Hrj;JuZ~nb2Xdeh#!Fkn?kgR`N*E;W%lgz4g7K}(3{7hz0ExqjH0?QDtv~Z0V5qW zWBarU5_^3({z}vQ{l&gN&EH-shrkh0Lh#XNYgwUohT+v(E~CF{_Y0}FcL=A^CLQBPoi7}7itPtT)IlXB%qvxbxt2|1Ecv}IMCGySS2BRIAIjDB_qy(&6 z1sW&_v2rylOx^uLMZ^g#1S`^Q=c33JOnBK+ghkwcFD9J@t1x!sIQh{_x`CCepg*=K zj!D&kLRS`NEmF#jajo-;%NmM)wXw@apOO{eQu}mDO>046u7tB=K>u-!a>vPM&}k7b zM%1ww8Nm%+Grk9rpA-Uklt~~r6=+B?r{5fe8{+;9v4$qtp0d;-l`dtE!owGl- zfo&k{G3_)wp@#AkasXh@v}qb&5%Ccph2>9Htrq`P4Ja(b7&I$+x#wSUhY^H0F{@D* zqFACW&dE%$mO!^x6eQj)&O1Yz=ez&Y1eyg)MHLB+9MvwV_&{{WaT$!TnszgLBSn_a zbl3*wTuLz#M72p2K3=3+!X|G1#G#f1X^~;zY?6TAgTk+rw>e~ff|N(u0|zj%G;RJ8 z_Bx;Zm)isWK_sFab$EexE+A%uS>&bQ3CQxnps$hYm@<>zK58#q1M$54&3~5oH4Y?L zCs~lyWH1)lgF+3~?}VQ|X3l}GxYjsX6C_sBMezm`x_vKRQ6r6b3f&_|Jj~RM_b7_n z*@5kC62ybGgDfbW)f-=`X*O8|p|M%{C$6`v%$Pp4A*6>79*J9Fj2n_6KC zrx4QE^dY?sM4=cHMnGq9SAv0zc-j4T40=0#Ne4SsJ4bWcyCm7AS$T>=R9 z!s&$ASh>+sC>IfY_FSM96p$cuR(?!{Nhf~};th>mShP*b7?eN3+Cjkco0NPPbZ{Mi zT+cHXnJNS|PtNC*)3xFsnXeggRv+or^|?IaIvlQK=|G~!H;qd{FQ&b!hcDSUU3HpU zDSzL@oWo&78df1LAR(8L!|%7IVte2X?S$|$1f1n{*{Sos4|h~5V{2oEiK|Bwb%#>7 zfj3H}4NxzGNeJZeMGkm8OnEkUFTI&P667zs(C`LVw%mhtjQU@1xE~0#L1IlfMvYk( z9_8{dr6wP%?1%r02n2Y!`>CUF<*U|<#)=kf4ck_I!CwY|KFbXA%l@Ah0KF#$V33M( zN7DY^em{UuPK;(@PeJhVA#v};niD7j$Qcmklu)jbpvUHD>o%KTFA*vu|Cp;l-a>H= zaJ;G%Jrc28?@y05bbeDYzpkvkix3Z1iJEys5dp@?x)$m6wHAKW3Mqbtu>(y}1^8?c zIS6g`a0c(2mAOD}g|h+eQB6$yjaIn=UMsBwB_lLH#EAeyRU2{7V6XP&K58R*A`~PV zeQj!G5v^D=CeRWO0~jFNQlI`n0?gc_Sj-_AAK)rcUqRwkYcGEZb?F?gmG3VOji^)u zU)xO{t|ZofjV1xQsS>mU0qywK3ut7te;c8By=q71AxVhK;@oGiAuWXiqr)t@Or!!8 zjK|yLSqzVXfYs0Jr@Oa_d39-5TrV!&rvK_kWY9m?)P*3AsQCv(|L4iqa)9T~VvA+$ zDq1ix_8#VC=N+emY5=SgX9f6blu+kKdE901?`a1f_)~x>)C4|5)yCq$;2Jo3L+aL@ zM(C8C1hpTxvFw3MGSQ=*#1{0Jldretv1obRsd}w#m*NY}OWRLOKiRa_iIw~j0L=(L zcejEz+&!)_6e*uZcQ(8@0wj7UKdA6AN)%3Cq8#ShYnD46>&tU-u&iZe{iYi0Dq8h{ z>Tc|fAl3Sd$>fg41>p2<)-}1!v?dImZ2nD`?ruUy4F+-+U9=5(jNY_D?7j|Zvj5^*Jz{FKx{42Gfr;dWNGTeFwXK_c3jNX63ffZQ=`Krnb zu)a>HWiKX)TF28I6H0n(!7ue}e=;PDbXXo97RswFz7(r7;B^dgP0Q6LvaW4$Mg_8H zjsZPs|8%hV#`$4#e$<=;ep5;+8)C&I+q}e3&{+^VF{nJ3IfM7nf1k*JGry!C%ovPg z`!5{?SR%00NOGH;i;ZqYP2KU%SEK$M&C|4q=S)4z$EF!aLd}Ni@=fMg#+O(Hrql1i zVIXVhoj8bf)~T;#sQ*JJ{!#}i5z7zf9VRn_<*Q2Dk!uH-?(&3Xq=_; zHh1ncOgG{L#1>ZA>cH(7^3ZC))4ZxX(H|Z}d6Gg*zw(Khu5SI$k3J^F2H>*=U?&K% z{g7b-fG>P!^EV*6%!$6D$30pHbMJW*HK?hX28EjAZkU%Jh#mbcMY-`H*j8=&@fqDt z*#BesR${NM0TAx1q3-6^qZH-=x$!v!jL&6q?fT#o3wO==|BobF(@1440`%o2;ezg@ zA^T8?U;qMiW3pqsz?uw;Lm6?xH8L0vjU(4FkI(UF&mE909_TVI`c6^sl{Fv0XXN-SyZintH5dT+ zYnKB#(TlOWK+=)plHEj2H~Os;#A&x!H{`QC@s=JDVhoFbwtEF9OtGr}*_|zraV>4% z8Nzrj-2Dq*&yV6%R0c1zZM>nCQp}I;U;FcS74q9(pPXaZeg~ow&_gdQiIh{r{7;7l z8lfgjYX!uCMRr2R<9Ipn26WUBF**)I1W7#+(c$&2qIM;f@(B4AknLS4%V+pTB{?pD zkWxEzaD|1p_~jlyWJDVNGU$;;=U)`7l?+V16q{{RL^kblYk;;;TiUE6|9$v=`k-vT zH#3}(b`Ci193GKRpK)p^5CFGL0d(uY??Kk@XW8^KIQ2(-g-? zAQ4G0T>)X|O}{?SG;8+o(OT1n)#*DY3fBmFc02!iH)5h9*Fn#46>h=6+HMxc8;^A7 z(+BbQ++aF)gPZK%n#?8-3zBy9?5n!i3R!B~CH^h&>|$hEF#Qy&?dy4sw@QjPj=-?6 zHR{TNLrzBG>sQ8n9;3hK^mopT{nr*1H^tf1v!z$Q+cNq>gjFUdY#ctOI1Mwrp2TfB z4^l?olnrzJp+Xs43%o}sFx%V!Dh?Wb6K~6w{I5bGK}@uzbO}@=P(1>AD9|m7AjeT$ zZ*sjNL*IZvn`3#H*TBlKUso01&#rDESkjDpkEhRFDH&S_2DlA~;|qTdtR&5p<{i1$ z8!>)3(sJsKwSw2Q_I9sZTC*_MlDC1;YEKaFwzxa0ag^z<0MJl^nBKrD8e?K{|8gOq zDtmwjlAAy2qQt4)Be%U@WkYMw6S|=+EO()j(%|GHZ(;I0NUKrj&IjXTXU0X|_c|1( z8hp(F=PWG>uTHz*Uis4~Z4n@$8?3m>?{W3~E;FSv)ge=Cn0t9eTdC~Tw$?1Dx1Tyg z$&oKeEfsRXuddX5whi==%B-7>z8slsmw)d@1~eJ~<{l?@D{YX0mjq8T3xqi^Mhkij zWD_ww{oaZfLC>k2IOaRVDTgfOwu|qOLa9gibxf*;UKf=3`;2jg2=loUlByl=H zgR!7;pb~t3wvjp$62I7wa}odfeo`T2Sg$qvI22>yt^@5+AoEt{A5^CM43cZzH&j}- z9&Pcowi4AlYOT}^s}mEGzs)j^Lk8`nLQ5&JDn81N!^p2aJ zf@la3QDo8;2|#=t2G#Y@FrrD1`r*0tAnn^7uv8dE{E-ol>(;xGJUIdi&lQE!?BrjIWds4#K8GY9L6AJm*oIaJE-#a$*VHG1S?q7XNgrez9%CV%r`(+(@~ zDs;5e>p&+hICvHC*i;ju$Yp^rRA={VqpE~ejfd0sqJ1&b5HlV%&94klOscP>tQoB?CD1@Yn_Pq`63v{_nL# z5TT7Cz-KXovX~WQ_o5|5os%2~0YmoyT+E_{ZJ~f8Vbs=+!ofnKN#hu>{FBUpO(QYS zJ+v1(LC%N_8qM2nVM@-qAku&v#XZq0Ae~OD)2%4oI#OF4PK?g zkzB>VNb}Eki@1~aftFGeHIJeBKIsuE#4CA^{{WUlZRscMUiTEC=^?ORE|~#`gsP=u zoL>(#syi2>24=RHarqTD@{0LxZyf#V^=x(xy#w`(x`x-D1RUQ7vriQ$6&T%hk3g1z zM*6bngFEMxNC4i`NV&uMJ}T%w?RNgVD*w;rJ(Y#LkHpY;vG$c*U$5CC0s|Fvl42`v z7dd_{aR6D)GsmTZ4{K%b`a-)xjM4zy7h&K5iIp~(J=$sh4>~bYiG!id6j8Wo#z4-y zKH6q8c&0+c#rXr=q3kj&*I3)ypj>EiPv0U$HP=Hc9Dhi@~`#|zM3KoaQ9O9}yEDpX~7{C8+SbL0yY~o~w3pyDpuI6>p zk93^@`$xre)E^uQd8NA6JY&>EIr-`rF^i$q=fc8h3UVv~<UEH|weE@!Pt;6=1*#;!yT`u6o{dWSUgG#;q;4)D) z3Vnk#_?_1w)F&$M zBSN6@D2vxwVx4sdfdx;~WPm~bg;sXY2jb^TI@bufdABIHm}6RZ_UsB!*?yW}L01n!+dKBxoBhV=$# zmI~{{DJTPeZqaAtP8w#PTQ$g(4tg8!`|e@vDvz($U5F*zoIkb5yq>XfW#*SWqSD1l;%HsJW&H37@s+js zpp8lw56I-@41x&QX*h3jpLv3Ga+IT;J=D$v9SUaK;?f!}l)> zuqWgYUKH4$m^t;h+x^R63OEq$sO%$y>DjoT0^CU5LbT@f10h;$Td3yDDex++#=w|O z+hZLJgvj(0q^qhv=MW`tL_xuC;L z0N&&5ZgReWPy?+pJ=@-pVsbF%v&Jwz7J1+Gi%1xlfy&3bT}V%j=mpry)|Gh}=*Gsx zYQ9*m+Q2;KFXl3&ya;F5gdcS;@mBZWCak%sVWWQeY5Nv!$w8TC6ZZ*|V)r`?&i5|M zdz@2qDfdf??%30ScW}aOdM#q-7wD!vGzM4iHGK$%XCS<#SL&x}XU$mcUpV(@)2QF-sK#&kgYgNV8k@B-dL&_SCb-so}RJQ((uR3xKUQ#euS^L;Oz@4QA6 zV1ZtvxV4wo0Y-+L`zt{0+qyNXgl{unDiKo9unDeHe;JS07vBO}?RL)m6b#Rsz+0f> zsWvdl{qY=!QzGhiwQh#lq}}NNESj_*f)P<>pnt#(btyhbZEzKrV2t|-h|+PU zfAO#v?q$_plyU&vQ`9M{9Q` zXCR2&<;@8#*7Z^(K8A;Zfos!nCry5%K`pQa_znCd#7uefTRI95gQH|#?dj;`KqOrs zUxuT(R~g**Zt+Ph{0%BYl?vNliJi-eyK|j+ls2cufg#LwLGw^&$zVxzu)ihQHsM$# z)?vRV(n@1}d=%|KrhXOlQ?l17CWV5zyg6y21 za)GCpr-xV4Sj)v=Z$hJhc9fy%r~_?jm0D4`aYnE=WJ*BYCG*rJ+wKkuYuslo`y+9_ z;v*?{8L+%wHWU|Ml&`JFx%3*(ZaL@kSwbYvA84pq*NazIv;rPEGp)FXXu`+m@NxIO zPk*lro+(A7f*Z*8n&~iD96tbwsq$-Y7Y9EJ{VasQ&<6$8k$W*jZ-cFX>q~c* zB!tJ$IRcGXIrIx(s66y;CFurpf5m?|EzMrAgkw%ta?FQCJ3XMgWRHlW{6a=7H#qD4 zZ|;%M2HxP}ChQjIBWuHb?WN(cjH`q9jPuDt{nv{xIX-fi!f5)U)Ze#XY$wbkQXS-6 z&Cx-t!;UuW4b4Y-1d<8z25emEp7SplXn0NpO8%a!yxf zStHf`_0ED`yvwl?^Lt^0h>F2H;5HO0oc_+u_J#6n#;3yqUtxj&DJKEixe%Nl_=Ql;gswlawi z_ZQV|<`KEm-N^iAdr+daOHJ=aaQ=LB44R=L4kJ9`oN$``iACbDd&L#qkIXtP4C_T@ zQg7+M?(9Kg{20LmKU)!=(O1;tdvWDYbHz7G-VQK-!43T-ceZlIlDgV4#R~01{Cu=8 z?{^na!D+;#v03~@3I*ASNfuV$*u|w~T0wEVk|7}pXx1$al&VNyOTY3TVMTz~DC9y& z8k{4U^4jLk6s0D)1C(BfBs$!THV_x5r&pX0d=;+M2H`_{zy;Yhnw=`yD!2wymjZz4 zYCCNU3Y4|rIo_emvwip4p9FW%>!ociEK0;xk?RTg=SsC{ z1%8v6ulfJ(bW@{qDi7EXxFNhJ{B^f7(yt`DfzIZc5mFpdW3*N!95|WN_A919%#;IT zy1R&q4)+CBO=y0m-$8s+22KfDz+`+ln_z5l_|9e?$*)!c6d9dCBBfsNRC70XG+GwL z?Y%4Z$n4pjfIo5q1%|N`@d0EO1Jo21Kw_g{gol5%TaK;`P7HHZmpj?V-GPOv>Y}|> z8^z12RM-qluD%DAzxFXq?b!p7W2-J{2hmpZfCvdqCuxMh(i`TWE>+v)#>_;o@2vze zJw%NA!xFe&@NhI)gpzs8Xa5x6blY&i;MGD5PRwaM$3j5ekCqTz+cECj5Y;<`lgX2T)6YG<6Vb-%7xg#U1Qo1fwDzZ z|AqO;w#o%ssP2D=2gS`%-wnI~)c~f-c%Ofub$M9k7-S}i2{v+CPK|^w++OGZ+{Kw$ z1!8x<75@IOy*Q2nRx;xSe_*$+J^AhU3wa|-IO>kJ)FoA+l_bZ0mhry^!*k{}6Uh&8 zV7UF2mDREd|_Bf>Ios7X4pbXpI`Qe|B2RWlBh1x0TD8O|vz6$RW-^HMXv)e%ps zFC(*?YA0P(Tm&D7yG;Dqh8j*iZApHCE(|!Ips~yB0^R<%o%{uJLnCt$PS1-W~~qj@D?Rldr#-mOv} zwYKxa_8p@8>J*4XdQlZ^jFAN@!(dhLQZ5kvDdQa;OD$V$5xksBbyv1ujUpZ&YY?qOl}51&JM+A zguk{w&eMIU=YB^i!f6lwy98W%4vf=}pNPC-Wx(EsXgP!@RsxpjLObhv8?mX_&9Q-R z2Es+Mk3<)?abQLOwGN&Y&NzMux>wU@eSZ;<6s2Uh5#OK^M(5r8NzoQv6sdG>+awr8 z7+YO&)SA-`q!H8AGvpi@>>^9Y#H?#6awkvl;eJ1oXLz7yef5ADRKmPR1O-yA-jah8 z?YRhmf}I9T4An&BpfzOA-p`zrfus@~ZOnJzYF8)a&S3THyF^#Z$)~fwu)UZd{17NB ziO|$NTlc69a|(4Z3qdsuG^4maS+AAEO)(ZmP}ljQI-FaPHK_i^Z&^e4|F}*@Dgi#B zZ?NDiLhh326_}fOyGLhgsw2>P_WcU;%@xerWfJ&A8xfOhjX*M`xC1yxdId)MT(5hC zHsOk$6fxP$EDb)Y65}2)3so-mLAy2B_NKIyqoBPD%KW-4FVL}h`DmxI2Qu6WJY1AA zb9}y0?R`#PK;64(U9f4vrC`AIfbM_p+j9aCv=74&4saz%L4g2__S*rQIvkF+%Zi58 z9vkIiH6%G^OQVN?IgH(m+SfeV`u>qDz357J2i(R3@Lr2N7P*Om{sHROFlH$}r-uP& zwdF%sRy&GZX4Oc$7+D3nwH%Yr-WV?oxB=porqu~_iz{&#A>be~yJm=%iK#02~uHu^1#&K!vKeN0pR=hW14NRK4^DI${ zk$KS)f;eNN;-4sEefV^3P~&&W*aJ%R?hq z7py{hfr(!D)<(xtM=kKj+qbgKybVfbwJA)zViVU)`;k6)AeAj+H1jzKzu^)X27x_O z1MbE4Sr`9l0XT7#l(|!7n% zb&6sks(bbBT^r75!9^)(bY43UZQuU`>H|K1pzg|VyKJ*|gXy;-;)#)=sY+;}>*q=Z zM0znr@MR@-?k2{_jmS6nU6-+H5#UT^x`g~K7IKyQ8v#-8UApGE$=IpHBtO2LE7*^$ zg$zg`)+CoMHUR@t=9r&~x=R=r3Fn1B23^~u7|o;PiR^uf?;*rPpy7~AZm5U&o)l@i3z=|j(*g*mW`gh5-a;#p~bl; z9826Vsqqgdlu_ff5tf@om#XAUW6lOW0^);2dwVj2ua1i~i^>Yi{y3eLF$4cgHC=>n zxp|ycxGG&?0LHbgoY{= z2_87h^#P>8LiW5Zx?c-4GhPzGu1~!MiOMh8*^`9ViC~Tr_go7AXZ$t{U^<_n2}+Xl z7CmSrUB$Z@wfO(Bb=~n)ztJDL#g!7*C?kn*jc8CxT`POL$Vx`ITq83pSKZPuA|e#o z*Q{(AX-LY--Wp~`Sw-|apWFKWUccWTU$56U<9S9A2NYPv|=7XRes+_ug1KK zIlvPk%xH_MxV+81@nZyj)HwM&ue%x@+3FzTLPQYn zbI+_3R^^$6)>JBpBO;8L+iPtg_TUsA%8gWdC5(KTv`0H zo8ZStZxXjJ({d8wR$-^S7eTi*qg&iH-kYM9yU%<$S#*R;?2-Zp zm#LTa{ZN@C&0L&SUlCM2vru%Q0a>2=ZzdkZH8nO4J7w?``7-Fq*DO2Cd*G>*7U5uVQBDXKcZ{rp82Ntc#5I;hNkI zi9ho>(?bmUhH5!r*8Y1+ZUJR6p+^K9)L*(1VKXlwu7-v;=G$x0t+G)aP9B1hbrMIP z?rlroZLDXGV&O@-Bw=mB-ea3mggbpr?g0C7u zGoPmFiMR{ff>`mS0_^Hb8JJ7?ASAse;N709JkcqwP@%CkoAh5i2(MbHMS8aFn7G1G zjZ#GPqvR@Im*a8VCTm}y11MkU3*FgRLj++GY<+oC&o+MgRq2|8SDZGF(hOB}^p;u} zhgU^5I?Ug?oaVv3MQCj^o#;a+gT50$K)#?K=6Z*9cE0~I>~){V{Kqb*fhphm`}qU& zHw!~PC*3@fLAOxy_Q*~cpGxo=JLJ0glV?sb7Y$dsd|xu}^}uG&mvl6?rg{buh>yx~ zN%XMjojeeEr)1Ma!R{21K3B+fVA#|`Qx8vy&f~4ibKqZ{c(y-!P&^kC?d{cf2E-Q(miGA8^!BJiNi;4&SE|YGj+M}?Z&>O$luYdaw zV;?~S^yrb9z%%-b2i4M>lS|wCt%K6SW$sew0W8C)eQ7TaS|A;CwwdVZm!OhW3t>-f`ryZ_^*_eQ^}U zlKx{C*Q<}5@lHT~=&ECLTS#X@gio@P2hxu0Zwn6&-tR=O_h1kT-*PX`mNcO^&O24T z^l8%;FreGUBSf3|;}s%YNV?T`RCvab18%vf&I?K30BmihwkW30nD?Xh^WYXQOy}Cm zFD5(a^~HN)@1rtKM@feEL}ePG6pW0Nl@*L|EGjt~TP7|24JUci84NP^8a%&laN7#i z^z(T4&k@#5jaQfxlGlekk1#gQey+Zs;39LZ<;ng{wDc_VX(3koC%r>LM@DNluDQwg zMNky5DOQzUPAg`rok6Qh)8q9xbcM*be1aVM#bv{4!x>k0Ym*oJL{*Xtxns#p=#W=h zR5-<%IKc)m&mQPUz9Ufkf@I6d_n&OukrQ6!RsSs*#8k<43cV-URU|u|KqHsXfTN+k z$RyH{6>^{Qc=B?oj{8CQG}@Es?3s8vb;1@7og*R-o%I>a@qtFwsn2Jo<3X^$aXZ zR4`p53C*74-SqWSlkPEZqVMb5q6I3+oCb1(yd2`DBYY;DSiue~4!>elZLlZzTj)&xY)r9^vXqS`&g7j#WAuyUZ(pzI#EYh6=FWK2X%mlmeN`9k@CsFn zy`@2;vv?QG4+}_3gDvxZZWMhZqR);~Co*is}zMc4+aLd2Uwf zGKr{r4%X*@~J3wk+HeMSVu*>TO2$ zsn0JbdvW7?jpEf}WvWvf+K-!;5I*(2VCFmiEuE0;FOhR&w-LKaXGTXHj!5qG*TLV! z)EIQ`O%B6n>~kAh9n9zoAV}nmL}S;5&P^;$ReDcb9M0Oh^MK%(EdKQMv5S`~!9}9Krr#(w*uPy3TohoTQ{rFtC^F!*! zMGK^iXXzx%B{L`s+jD1CwS?{1JHTCoX6~fmr*Fy>cDaBk%d>O518q1e$czeq9VanO z*_A_QUlaJZ?^oGq1;+lM^pXxO4t7Xs~L7Q~Y_j=srQ0-bXK%GP~LPZjLZcp#3c`6vi0J zji$5VhV-VHER!!zw5Eu4m(~;sh_2f(y{3*u5pZWo0n4rb>fn zbZg00gIMO|Lk1xplkFJ=l)Lv1wrA2}&d3 z<1+nF;DUCyjB$%J)_iUyb=$6kQynOk?TmV!t1 zXy88l_Qgt_RKb!SytkaZ?l6quE?{zkL^|Bk|X}+W9pC39|*lws;wo+3V zLI6k1`lLcKSdT%$(-@~lDmee?bp=fqj47P0H%&NTf1Y+f`SeoNDYw??`@ui6QHk{i zOq`Nl&Rx?c`MS_cVp;j40ZBp6vnMEusR|uYcZJ1tP{EyC@SjoRpUiV#;Md=mQdfYL znxZd}6dVQX?R$z`9HQG(I!9dYV@^i^M-Vmp6wQX4;xWflp1kn@p`1>b<209neeA#!eEm!;6 zh+IqjKwZI+`4X_tT8M)xjD8Z~b%!*qkg3I-yg4+MAUpc%F{( zIg3(E;mJ;s;oNqrIZk2^*hes%UVu|cb=Qj{9_HuQhy`!9z+3;0=OjJk<o0)fg+?ZTw z&(N%ri3H2Qjkqv$4RCgXcF6V=?h=&I0HN9 zl8~PB=d7IvjNFQIU*D?#0>2+-NsEd;70E{=NHoK#j6s10?PmpcCt;mJuz7KV^C|=K z;QT3cXu-~W^X~PTT&6=@f1n=h`RgTyaKJX_+a}HCY@HtF;^Jpl_S~NqCzntGpOACzHDSL$LNr+w!8cblLXm zaIB35$m3^&hmsUe>VEF#khGYf9J^4a`+5ESkPbfGO8|FfW#hua(a!G0Ab>PU6(KqX zVo~WV8NoxLhZcEqIS5cnxG@<%o)Q6kb$5q<>F&|N8ko4zW{U>U5IsaQw>8|9p0JyDwg~13LjC6?ft1ofP6g zxrYzT4eWty@9o*KM()Cq_`~YLE!B~!_ZwCY6HVMwo>vd&Z0xSAhJ(@vS6_|s%|)Kv zII<(+>xq+t-HNqYva|T`z0zeT2TMDLbetKh!%rY9a&ztHJ5)Rdr^g<0IaE7~2}jcdN`fArauuf%xSMV4Pl3{^`dr?;dip z`Sd#W26zKq1YsV&=ij#$H-wTHaNeBPAU1pvi0$UTVuI^oP|;ZEwO z3~l`qD)c4jc;^bMbOvrnIX2@-NOUMBm3CiHf$;_4UMq5O89y6*6U$dtLUlddrh|gM zyni7XW#Cp!ZPet&IehxAC$*7f5x zg)dfu-bx#0YPoU`ot*Q@TDxoFA+jiwx^Q$b$a^cnFSg_TnrW_aJ{x;a^T(?e3cRT3 zMJ@%Vve^A+hEO>C^)snS{E=sZzrTQ6J8W%V-t=fP>Xe^pNmGq9i^bwCaB;u34%hzb z*4`QDUX8G%H;$+ir9W`)jU$BONAOQWQ^0Zr@oALk)G16jx2%uJL2A-QAG$Y1?=sy!$iEJjG_*3FT|cwC+WO|uH@)l+ zAgwG0pIjxYxTyQV>;Bc?S^%;ID!K#whJgy<-Yx9>)6y3#&Gl%_^!2%yh#X@ z2wGi~ynS1bMYv|(Y+_)kBH}Rl_LH~}#+evBTepiZt0O7-B1Tz-Vp`+mi>z|N)pHH< zFPAQ&+!i0Fw6Kq~$tbza1>K9Ek6JmH*-v=RtT_2I^=$UHn!*aeL@!a1Px>jl66mgc z85~->JDb(-%sfOaVJ1!+Nw6#RAH;sl)NmTAS)VBz)-st6__;D>RpvU#M&$Lrh_5~R z4jI2})?Hw!`{Q%+NK4IC0?cE@~Y56Pc-QLkq{F;pFpXhsNzVBrJuxjsm# zEaRi8JEvD09K*f9Oq+d@__L`Tp7e z13*A{reglkhJ4tirxk0Q$0gxxcaOTE*I4xG6rZx9WB@CD>I($ng!wYGFvf?9??UgH z#4gcp+FTy|mwtAp66 z$u6%r9;0_e#6@bB(g24FU>W>q)%0)3ki8)|&M2zN4*8|!nx+~P4--0yj$iniG@{RyqjYuKtTUF|vdH?WX@>0} zfNMIs{k1NuOUtSkAdFWun9c_( zzGr^)*$lun50k;=>FyTPZJ^^f`=Vx{TjE0>&I$4Cs2FJdJ?C2m8PXOtat41$$QcwY zh<(u7vNv(#S4@A3X}wmqN9jsWDX~Aa0%uIF+sTk^U$o&907Xq`Gv|A(5sOWJ+Ep%w z9Q=^`C^_#bJ6Hv8{9CZ%>G7NcvdCNicI`T>3=ojJxHRubmOy@9dA{}6nv5_^ymcu# zYiM4~$hOb)=f*nm$G0!S67$g29? zd5{Lq%2#<$cYSva)}jKNUyn*MWCY5popDU%2FK}SId2PfM}#B+{7S!#@3-ec6SRo2@+Y!)-}}0t;1w7Myf@D11MV^$)}L=AL{% zv`DzDt<05yv)tkpE6((Lal!t>kR-zD;<~qaxqE7k%1gk#+{)X#F2kxk&kGsqD2HC- z!9?zZ=iU^-kpevUj&-r8=vY^UlBLGo~ zo?hC*rgr_1c)t>?QJ^4>zftwrrJQdXI0@%>5F4>7Q=w1azqnD=Mf_vvB2kMl8>S!p z)eIz8+r|V=vm=X$>`@4g9|inXCjeF8)T{}&zChh~3+rVSx%JqSX#)R&%aDw`%+Ie#%tH%|pT-^@yO(XfVCo%(f zBkRJifx6Bvb`H{QWI(orhDlxV4mCbneAt_)J(mJ_)0sB!_8XyvkZJbyIG4E!vioWt zB!sX-a7UHT%p-_inco8AsSo}8%Qbc|0M?)lGkWG+46>Li8u8(9Iz=dZfU2&AMI6*NSe$nKZz_% zh%Rc^VS*j#LT=6UJbadQ(9Vn@{bq>OPrEnqZQFLA_$Vh^( zrZ8<@I+3i{2Vq)cx#0utzeC3ob{oK`V65hSS+{!uk3YaHxtkq4J=>@sww)fMjC9}) zb{Z2}K#b;AN%6bIRT-TN3DvB24B*zDo`<%3AD%7+OOO^%!O~EkXR>+x*D&O$!G>#-_IiD4^JoYwm{WyN? zA6y8jQ%7uf@!SX;ON*luPzPnyp!!Q1#tCfh=ibNtkbMy}@5(n<^;&b;N=t8}Ix?AczZ4u`yw0m%pc1IP75>$gIX4KQTgT|Y2Gub=wHIsc zU*B%*@Gv_J%FK1QD^d{-(ySs;-&jEIOEMZ>yEvWL%_P@uBhQx+E zD~uN-Cdvm`iAbye(E^l0r>P+l7wf_hQ(@t!McqQG!@l(n-@ks6^s5AB%kK0jwE3Nd z+`1G7ulG!K7tRG|S)6YaYW!;If}y1k71e`S#eFjP3GZ<~sdy$kJavap@n5?{L;8mD zA~k?u!H!UsU)v83&Hk`+*JY{)pk`!;-G&B&H@-Wp!MWR1sS!v1Yql)P2p)rjE*-li z2xgt+0~4vRXswML#W(2e&z{XmN0Crj>c2!VVRi$=-8hz7>JEsn1`_%Rk2M@}9q3tIo|(q_0jWB2hPI@)`R?J|oYzSHN_o!b!R4#$6l z4lOYG+AQ`Wz@}t;j=_tr0?Pnnj&JZ0<$CRK>5uZ{i|c z@|sd820@f(?IX%TPKJC}^C}-=w|}C>n8WOA4&=1dq9>BHz!Q9iq3vaGHN(@wcXn3P zwg2_yhbj_wS24PYGH-#Xu>*UQOQL=Y0aPh}3oRD!LJ$}~i$F^0m+F8OAG|e$+4;f{NcT!l zI6AO_mderl&zyKS?2ZIoMBrw0V4MbtpLF6QsOVCc-uHc_|2K2PGRXnIAZAjKR1F^G zU*^vMrvc3&?)PAKP}>6)r~7dXvaYZkqI>6iY$JQ&h!eqmiXNopW2f$PFA63H@lN+X z7AB?J-5H~Opv}MNRtB)^8F(dSp#{hOYj1v$YB)RB9HF0iZu80@kP_~K>W7A94s zRCrs`vX5iG)tiOewZ3eR5wy~*hMPO)HPhF*{JGcjRR3r*1HHC29EQwpMQ&NwVwix` z5B#*Q&_c?LUDqXKu^6-x4!l6HtaDeM2j`AoMWSCj_=}zZ%Ar{I#P{b2y3kIP3I8uW z5oYwn&tnaOsmt6@wGZ^AzT?}w!*u|dc!N_oiOY8+EeQ~dv=SKqMQ@w|{Of?TA(-zE zv7)ojHjAC9c@hLEo^i!_d(D`A;ZO$S`r|sa?j7%y^PP18XZ8L;Qpl|BKR-{8M^?>h zF&Nyz^2nv_W;Zw_LJ9Si@aUv?j2=slU1M~?=bxlaTRwy2xD25Fi*(ct;H7>tWs~RO zWkgKoepwY(18l08gRyX-u;07vFDFzDHKeDgugU%?}nqM&T3 z761XRSRxYcE9v}8gXtp{H3<#Z#cgS-{ea@!^36)FSH3uKmTDq~Oxd4nu@Ms=MxR}s z0o`K}i|=TDif z9CGe@gH!~t30s^wAMb)cJt9a_kJ(otQwmf^22c%5ynhZK*un#9k;!T?g6C-ab&RMD zI9G)Z@#t$*gd!+jU>01bG8oJ;Y>0c2gR>?(h2nn%ue}&nW!2W+GD#;>8^bpx zFn9yVl3k$UW}Jef^^FzyhZOL6rfqnhHnmu6tn*+dl^(BiquYMsTMDtS_!=&H*k|hk z@|pr7DL8|x+juQ{LILOgM2nw(^n9u-e;R9FOziJMURKC+8w)%Se;pm$KCL1Mn*E_D zCKsp^(prgXB0lDqb6~iO;Fc3Xt38n3iJz$n?5gk_PdQygr6)2`08}0D6r+bF&fRiX z4>`5>KvFfRio~oDS#X6AQElL6!ArxLF9WH6Pe8CW@AN@%+VZ4yh6|4pMi|7+ieF82 z=8j|SCmie=B6y7sY;T^m7lIY3yGZ{8*A7v_$-e0elDGY`Efzci5&)JtL;0u}hze8i zG!F==S{&4UScJX9=bLpxaqWX^yriXE_ni~4Hm%5^t6?jLt9@nw5BdK5LkH;btIxb= zkA~V6UgTd(k-mIrFs}_>cbOfipf9;papPa+$6|^A z(e<^JGT@9%3oI(~px#4LIeQkdByqGz7$P(8hgTf@3U{+B`eqOz9kso6>G0l=kQNlg zxD#uT{>?zE;lg*V1U{+(8Xzv{7zO-?kT=`Fb`lO92-#Oxx;k4TmG7o%{zwxi+%o&72N?Qql_0B3p0K7mUZ2CoGN*&%x#%qC({UPK(O8H8~%+_l=_ z->H_MtcD}q00Zm?ce}S(MdlJtir!0c73Rkjfl8qQJ%)rrh^L!vJ@V+Nws8{BAhai_ z%6N`xS#f#<{&iLi%CU%37FR&F#ncovfH-O zzh}?_=wITPDCbQ9n_BMouSM#A2;oFR7`Kt9m$X}c0z_o~f0zztUbzm-3$cftjZFk@ z|N1Q+SSLbPo)Z$K%sb$|s1FpO$v?{kbz&kERSHxXe|fWb*ozWE=XEx>-hlRI^r>aUi0IWV&kef)l~wW|_k`mYRg7}k z=T65atmCC17-WQ6Kqxaf`gCPF90>8mKOb2K)j$H+z+;HYy|97S|0lA9b4k}%dQHB^ z3NrH?ks@$3D*pn*9C|F1pe25op?Rm0`zQ1bJVr5bl>hPL-xtQqE^XP~2iexT%;7Nv zxbd^>ApzjLtEg^77seuz$au33Ms8bv`s(rFWkuw|cnjin=+6cH*wxLA0WQc(p#!cq+k`-wKY@jcTG}Vt((=K{#qCwy=8NL^7v_*cY>Csma=6FG1-u1N zP-7$F(XrrAt?9pxy`9FM1Gpq3te`8&NU}F!T5v( z=A8$g2c7Vnw@#vdIvo~j`jQ+bEK2}8woYJAbHJniI&wG`gu)8~qzO6rF=%z}hy41B zBf=@Lyo0b(4Je@0u}xq+N=TEVJJD)46|M}^pC50YDcEWW%BcSs62xBsC})N=H%;6RRfn!26DiI{%D)w&6GgnGo6iI`531Uih89W3 zc}&6GKYtVwss^s}F`0=|Zga`Fk*MR}y^0mP6R_6@z8Rdji;4qS!lFPW#-Q*hp^ot; z*dzjppiW!X+Onxk@0U4`K$ML>F*P>=vlO=M{O=YiBG6kSv<70P4~$A;Rl9zDut|_= zFiaw02w%7cxz9kd5wotFSGG37^nyb~=5Bo&x@=i-KI6a3v9fz8XvNZGMfad!sFl`JvWrQZPzSEy>3p1&*iW{E)OPFv8iCIu!=z;3L1HiI< zQ5fccJ4X@Vf0iNpKs(ITu?G*~yYltkG`pfbV&$OPSSkgq-S^18>Bm1816~8+Cc5ul zrtZ#GE~&bbzOEOcm414$(MFv~zoP>D*kWKIt{t+U84#B-jVb^5Yh z>C{QbwwfYtz*;hl%9;c7HhTmPuQPb|Qh;rBh9fOtW?*0GE8|Q!vFIcQSwejP7KH#=LQN0et23ZZJ?U{Xv^_!f5j$Atp#&V|1$qZ%$6S}qqzTSe@?HQe#XwXg4CTK2>AeGy69cS9 z!kcIa1+5MQerZRJ;TF%-Jf!OOJN4S z_%W~aF2Lsx(Yh6)H%gaXtq*AIWI*0eEkttv!<{C9p$k?wwt>enkx*Kn`WxTv)1j8u z3k>_f12m_k3+$lJAEyikehvtuVi^2X|DO#6VOeZ7a4(Pq>zl7##?+EQ^fivpN=hXH6Xo)z!AmLisT zJ(vVEcCYTqbvuNQ0q1lY>K`s+ZxY&SrHrXjilwj)a#jnW<9M~cD&N8k?L-dIKV}8N z^#Rps7gYL#gbQ5EivoK@c!l$W@HLs<;&4A`Y}OA36`6y3`;(`UAUQ0LBZ43O?Jfl` zg-}Ra`HuG(wIS0jwXUdI#{^^aj#w3{Zs$Ax>YD7yS^lxxFl#Ifl1Z6vn`5)X9-=a$ zd|Xt^??tjU1Rwz^Pzq&Pp?=$kXaDnr0@S!>Vvbx|Yz&gN>33PWg5yt1sYk=Wlz*1H z7s+Q4rP^N;gRbmc1n2%*I^9QT5Rf$)7g#t!4J;qg_n{6U+zEw@3i=4r39v-h8=U(~ z=}>6;po+Y?pGZ2TISM(N=lk$9IuZ*BGT>gM{zB|8^;US>+SQMvI$L^OYtZ5r9L~ZD z!igN(uvhtu4WbZikh66WAUpvgFZkMzX`v;1fS5*utA*R;M>IB^F%o7SQ0)5n2V}CS z9XRGb5|Ybhn5u}X8oTRk5XgyW!d!KB9o=GGNfGOGh@l5u=DlgPmBC#Qr>zGbR+)D*iuA2g}x9RFra6mL9 zQvj(s@C2Czzg;h`vp)t|LJx3WJWb+xyih2`y2GHSa+YRD%!A~feY|fBGFxP}&4M=W z1I(-`0=QgJ%X#nO#)T&E%3t@=&+mZYX4_4^D4LusgnSdT*sgb2aGlo&Y9#$o`uVs~ zn41Q*b!Yj2<*g~tYc>0zvT&_6S*Kp)azhw1E%}y_koxFxv!xd=PdLP0V?)e$rZrWm z;L|clrOq;Q%cK+R-cOVxrC63&B&d7Lbx)SeHzyxsB&dF}vTOglhc0L&ZXdgk2;Xo9 zE_`BnyAb^vi)@t917*elU?=nazPzVBbF@i`hUAEOm2~v6C5&m7=2bJ8&jLAffnt3Q z_Uc51Wf?;J=9!fc8Y^=<6?NSr0zSaj`HmXT4MWG{-Mq*-qD6l4$6HWVdHmJyRxr!@ z+a>5Et9VPnwt|Mi393<^coIhO`myNBNOna;;=#P@&?~45v3DInm<#OqRA}tu(Y&eM zR(J#^$!$O7!XV<8Q%A6|I$=%J(-EkLl-GR zz*>aU(MLbIgSHo?_o`%zLW?``f;zSoN<)bh zE+KD`a)BG5^${M@Muv#TAxgS7)u*({BEZOHR}mpO`kdf!?;#8VehaQTu0vGN@1*=P zjN>8#@%k9*aD8y&am+}NfcGZe8%RwJNS&pdGNNTL!@(EBMYykaW zDQF)~L#%UYj21duvTk1KmBS7>o^hcS5y*z|o%yoTzO%!%wbh`Znaz|N0)H1DU*(ZV z!yw|4sv&jE=W`KqaWsvX4I_i{sTADAoLeLjl z9@d~d+*oo_)F?Y@IBw4DLJlQ7G#%s+Cue?O-ssOOpA{(a)0~2VablmZaJx#OB6SX9 z0u5dy!A&{MMmyB^@S-f2j-ASX0g|)h)7^#lnS_P95$XI4LO6gotplfY+hY5|{TX8* z5Mh*r1JXNW2}N3m#d63+OOZqtC{osv{PfQshA;jc#4NUL=-;S@%9NmgbKf1bfM|ML z(pr0>9*~MUJmUbBN?`1UWSi%wD{T@fttyBgg5vVE)ze|Kod*KwS@)+5kFN$nq@6>s zro~}TvdZtUjRDFn)~K^90cRk!BKpFN+ zWy4JRUP~nu;;Z?HW3y5vR~F1R1w*YxawOAs9;TtGFHa(90;!Tg8;Eag8rQ?U;Ng+# zI7}68vT+#A!;t9d7YA~uEvee}?XdwJe+1YI9uB!-y zgzTqxh4ysLL+S;fmA?@LT%aVEVUB@NH{UGSxo;;!^a0EX{n+iO<3>UvP<>j4Uic~> zg4HV}c4rJ)!ZIq^ zOK))MDN0@A9M&Lv#l++{6 z+M)>eb0zeQhg%;Ge^l|gx4Squ_koh;7@rYJX*YXwtMmi@ZWz6`cE7?g zh#;F?+g}`U!O8R-z96#^7*GWfAHBJEZC+n{$&c zLd`qhq2->!;j@-tM}UJS1j{K#6Uw=#Ko~Z_c98`w%Ou`7~(-$mB!N7~09rjC#;+s1YTqL#dCI;FV|%FKc+;|x)i752Vb;*EHaiT$#fe!8OEVA? z`B3I{T}NkS^A=NtMdve8Zkx4DJf|og?rR~`W?n=SMUTfGbkHf2m(Rp1I}Ue3?LfNf z`BAY~=_yg6x{v@qxeZK_Nl*h*at<+PW9*hD#HQp_It9XL*6Y*g=83exMctn!Vf(xs zUHb~OFg5LZQEIW|4gu}q8*X4)LAnje5$tXg8%NK7bdOEPwChVr%$_iIZ#rlcuikKj z^TfNxn;>wkcsuD^+qaw2cPm046>wPmXv}F}2Zm2JPZKJJ5S#SVZqZ z&3`e`lmmgAvIF-6W{=XTKMZ_cn%QS#X<4(zyz1Ng}krB z49oh=nZ)61PKi%LpF$`3;m>10!C3cvD?aH9wo=3;9Y?O`YKYv8$T`br9IGgCV-4@@V#x0ARP#>Am7077#umZ zTj^@O+NwOuu%5dbZ8DU?DuYItQK7)ZuH3J3ob5Y934`LqiYdSV%V`O`oPm^DKBB|; zc7M&O1|96QdpeYX85Q4gUAw=1UQtSJ=P~GOF?su%8+}w%IX!5ad9VP~-_5no-37Ds z1w4HI6MGQy%z^=HQeffAYZi&+b-L$&9M4h-dCy>Q`$qIS$S=R#zaSQv9+cjo&m@h#2Rt5dw5mRLga|@HB*T%MmZV^wx}CmP8-m2 zRh+vygA%rjQj;Ecv2PgNGI>OA`grn)Ub$L>P2lLU@1myZwh9doDSVTq_pWwj>fL^s zs&sY0qZ(@VoEGl_F%Q}sy-?biIs_hDvS>RSIqE7%q1tNJm(4kA+AG;l7=BaVBXa69 zlr_`G#d(=lFF?_z?j#)mTKCTBMTS0c`F?!%z_4$=a*Pm}K6iT4miCz)Y!^p*npMH-W%w7Vi zRffg9B8eRz*5+Acc3yNMb^a!uhS>_#Eo=dTdb+6zpA-3(8mCGz@Pv8iRaFxnB`5Im z?Rw7pOD^h5soH}7bCo>MrHWTDV9{j1VN@boV;w>{E@4^OK~y?Lu}L4f6)FUfc%#K= z4JuwBZ1eJbcdgTq8SiA^eoBEo*`;$?u>q>tmSo=Kc>KsK?mjnyZe~h&_pHuz5}Pl99AVLo`J^hjJUs)2PyrIJ@7RtYMew z<0B`zxRBYOqY>S=0~??DS{0qxXnAt~P-`iBUb`MX2WqCmV@2aKv^(@N_LIAeGPF;A z04V%T71t8(Z-q}W#DBd9lc8eFLf`*l{lsbaN2R0HSIq?;vIktss3TB00qU0r`uys^ z0f`pmTK{2xfQzD-K70^ho7g4tw6`ITp2W{MA!q?)$zBe%>&|J**60bZxuHR_I7o6X zl8<6uGvs%CAhtNa%Pf6G(MgNCd4=-%&E3OI_*DHRev~u<56wbzRwF1YBCRp;sWIOv zK52D1^(v`Kc=p9HA3zwEG%CdRM&L2o$PX0z%X17UW~gj4qrli8Nhs}_99AP%upTrz zsu3wP4V9MUuaa_Y6tn{2dj?j{<0^E-5k#fZ07-auCb44-KJn(CN!^cfT-;X^dUOb8 z+@IbBMYum2X1Y>ut&a5Y?#Ho0E zl+KEYv&#NMaZ!xJ+pmOhsu?7zlcixN<|9Ms?okY~TeIuySF7`>l`IbS1q z?ex*16_NjFViqHWJgW>fGxtCoG63DdjsI>R3K?HobLO_95X} zB?$X2(h&wTu8UKAVtAA~&4(`j3|isIzAmfIODRUMs0mSr*kwd+;3jO(#Hyi9v)g0t zU1V?7FFKXVS11r(QBudC^2N~TkEkmVUoEcIfLu;8EKSN7BuP4Er4*@&wYynnrQ~hoP>hbgW@&k9jA1WA zQO!yleNvEksdKo4G7Gb`yf;%qFuPdV{nE$FIJd*7Sp5ODk9DP(5OO{*lYAftuy^SH$@p^Hlv0=V-&8ID#iR(eN+%m z#RtXCOx9g+cEkS`;PPtw*4KTxYS3=PT`78Wwbx9rT|M zT|as&ej4cM&%mRckF^JM6Z=t{;vL&pHz}A(8P1lAtU>CzCNnB{C+Lg+fc+-oo=)JW zXe@wAIcYK8tWf9pRJbEiH@}2+7iDw0k11g$Q%}<|b{|n!;#mh;bI-0q)@Vg-!gPkQ zncUNy);qEt#@eF^trVG|m5$e1?71B}h!#WBkubYvWiCyAbcLDQr*F;gtN=ONC?D@i zp-;u#J#L@Hf}c17)C*Io$jyv8UFbG*`S|Kq!Pnc}cV~^ZQ$oTw#`KR&g9yyM%p_5E z=445vW-PYz;_MX41LM*MZDtp13g>SfXQz*@g8zHwJeXBS{=BqiGSjrEODes5#l)$p zzkzTtYb3HOmBVSM^@$fx4*71x;ko#CM0~2|s37@vab!t;D@~Z9F?y%dMW^9L1*Oc? zLiPwg`KfwWIgVo9bso{cE@=0EC#lrWh2Vgff}V5AH$tuCf`0-b*Zo?1%{=u0!Ye^f z>D@m2sS7ud>JP(&cc|6&?CWX4wIU)s!*hI3s9PDjCk2})C!2;=2YMjrfn+?x%h#t- zhyftjF9cW21guAUY>xjXGdC}&4li4_{6I>DF({)iiY+s8sgzWFgn<*sF?)Y5TNq|%T)i<3bg&qXFHszUMIf}8=wQWbv;odhCa zlo%+k25PzDBK&A!4kfzJPP9BV&YGf}<{lfWy9>_vRCwRj3$!djmYuN}5&ci;fb)|v zYP;mTvUB75pt_rTrglSp;ayV4JcZ*z3kE9$(pMg0Z&u4a{!ZFS2j<1e;?at*L z1kgD!_qAMUO%H$92&J`V3ga(68lg0v9(a33_~;=cd5wn92u5D+U6j_0*T(3ANk0RA z(OJC^W!i@z1Ubf5vhXfYvHJaxD!coWI3>9a`%VzW#$Ziy{G%h}@n$y9uY_Q$yrEBu zHy2%r=%IJhk~7$k;3o`Q2DgJsOP#3c@v9OW2Q_2s8C_$l00G(G=g~+7#R#O#gm!GzN!nV*i`zec~WvJB-EdUa41D9mfl5G**c9q)}wiX(AyHj*1GV zy_N^;T@1AR4+npA)8$f`1=k{n7QXH=P1?P9JB`nJD*I4bisrpSUc>HN*-Q0vrBQdi zYGGmepj*{4{*-g~Pxkli$1!30#}1$zK-=-DCUZC(-8M13YOY4hjf%zS8HAWfG%8*V z#Sv1oi1!F0MwubxAMDBo)*37NOb1afIwGWmuGVW)L^AYl>zvOeCn|cPFNsJQ6xzVRR89+#)N7lz z$E&qFd#y9Spk~{@zVPp&uiqw^WAGAg%YJXB{%e*%=w6^)(qHqUQz)Ixzs^kvwk&&< zIo9CEV}}mx7%~R5=xb|l(nBnds9*X>q$1Kuea;TeqS)pz^TS7M?9h90l1zZy&J{EpY0g_H z#&`ad>3%o*{XO&I2-Df#`5ctA(4uL2EMX6#3 z!0RJ7u-z~jsQ=!V?m(CtGNRK(AsB*GT~|vdzqKP&N7$oNL!13N=DZm5jd(>tKJ5dSl2_cfO~_q}GOLniVPFSB@nkNgC!-EUeF5Ztp3fj< z_aq+b+-5u~h@bqMdgNE*dyUznJ67+ap~I);UrCnQpCMR1kq?N{c~ps<&d;|k50s}i zr}8YCWscNnA&d-svMW2Q)kUK zFDpQZeY_gXwN8vKW}dwaA`=Qe_Z$q+jaoCGfu3!TnSLYYsNWTKQY$Vxbb~;8^S>=4 z8l#4_tSoju^P8WEfW$d^=wmPp(bWD^@H9I833x+9`u3ejcrz}}#B(xup!wv_K*M8F zC63O$k1{|6|N5;Br*x z`!4YstB^&&3`{~X2VtWN5WJ+*yP2qJrz59vv8R^G-Zt#M=t3WTMD9OOT^)jcJG_}w zr2KMxtf=7?oWY?Pd#Y$(P|+F&t#noSx+Na*cu?i-$Q`0QkPIyWFl+;llPH~ivYi2M0yc2OwSN@Kj| zUS8{UT`yuI2>BhtC@xTGh{%c81XB=h_Ng$M3#%`zYXn=zQFuC!#BzfI<>`q&FYJA5 zL5U@Zve;YvgewVhaKWV z=Kwj{wWkxqoO02nD;9P@Z+CqAE}O}4{bHZ^PXuS6Uuc7PY*T8;x4&?CA^iw~PysQg zc#cGHhDz;Ko@A2HGP1CQfeFKYWZ$0vkjNJhJrgOtAsRPNCTte!b@`4J68Dz_f5W97 zr*hYFt+FjEh*kq+p!O30HO01BooAAsP#>RNGu!ssu7W7J#dYX@-Zv~#ea

CcM;YQ+So)>v&LGtX$M5=hL&TIL1Y0c-5x8DI)moyc&fm#1WIwXp9V~ zQ8V+S%TrVeGU_ZOF^!N+k zAAo-!e)`CP_14d#E@D0*dp0A=@&~-i!*GH!VQ%VuETQngJ3D$oQZENp5!@Kj=?$3> zwPi!1+voYpmfBjGXwTygYcuQKUNUOC5PUS8r|LO;j5rWnZlq9}(ECiXmNu`-A|E#I z2%IWE6+X4_gKr;OJ-Ab*!(UcMq50(c4cuT^J65qVa1i?qh9vXJwW=!@lyQHoA>xYy zXBZ%h3Y27h(~sJAT9d4W2(1SkipQ-Cy$=ViJA|p1u#AQGdW7%&na~U;=Y46T+wBby zi()T322^Wi2Tpmm6O~1PgJR}%?@C4mImNDFQg328naF?b9F)MwAacDJ6w>5s+JX2yny|09Eg=7%KKi8IM zd=Xc|y_%j-)v^zJ%`7*sen3$EO}%y&BPr)QEOdmHjnMYH4ft{R_2-4YjmrLFX40HM z=cgsn7;b#K{6-6*UndkvYf9I@U3CGN^QfDmjH1J*IgzFUHMvXKYfU{7j^=PKBNwEu zUK3^}DTLMcwnZ32SYAFH*c6^L?@L`r^(RMF_&=O#!(sUG@w+(POmK(`uQQ=yNBBqc zm`n!Mt88>7HyLn@sS`PsuXK8KC-9u~t);0L3nvpX3zKzYw}#p$-V%bGrSMp}<@)JvF17{k8NY`lIwBE+rRr z7wl5+c&ZY2ncHDbx-lhjXKwa)iV(6|Qz{O9)$vQ2Snnf@G>7>Q2kIa12m_N*hawCU z(XVxWymr=s%}RKX=p*3;qV5u*sVA*z_7pACu!VSDhNLL3-%(`0OXOTiBo;+R!krP! zC7~sHCwkrU4T86KRq>fA5FqJ+bw*UR#3GT?64hH38)~86Vh?mu)2R;eE=I>_NOLfUooRiDKwmxE@d*q6-{y$oP(<|0c zqB~Ek{N|@qW`z^pY^!91B2)Q47cx*7bI;rFQmt#EpdLI_2<{IJ=l0)9xZ@Rm0q3wM z;q6eT@cYn_4+{KPx^Y3_5o&wLDf zT3JW^#V__eX|{Yi6jPl%`_&v@c!hOcN(2A%1z{-%rFo3o7sRVFzEbuSwiaTP5q5lN zDjb!FaZliEGnf9}dY%YZmS^=!uMPCrQ_Yde4Not;>_;sVdq&Q7XSGrK#Zb0)GJK`n zf<2+jw~QxZlv`$6JSc@INrm4M*`Uri*_7rA4@}}N!gdNPLOTjtC)wp@*2`2`4TR~0 zT-?&dq?&pIzm+JN<=)L=F;4`q(RL}#IrSQiysx?ze1US#+uSRrwT18^VU**JX@)&f zuZuL{m?O>5J7$g>q!FJY2$qZ!4gx#mMzBUBLJhHSu`W?a_9E(^ND)ceIqJ+mxPuJ& z?x>#ZuXG8253@wdrE8MR=;V}g)Ycz*M9$FmW;lujw#OawIP<10%Y#@gB3*LGEr2#o z;==20Lkm3@tnv_&7_X;s7JQY3{#YMeyFFWN0LJ^Vgt>=bWPNm@v<-s~)C2TDcQ=_(4=)v%noBwjHEoMU{9}u%UPgp4^7Tf4n-TH zn=Ctng8kchl1D8EPx%}`>*gpN$CJ`#Vr95Z(uAGTMR;RbRWj~EA~hC+F#j74ir(8P zeIWwE2dw1MX)i#E@K(7G8v~ERSlHj;C&A_G!%tak{An>Ls8gjI=l3hS@yf2Za1eGX zC0&;!F#n^av-dco{jFBRxwtYYsUq{G~ov1rU@ z4=nbcItrJHjydV^ECxL}Tdem%B(zCJ1N*Zf7GDN^m`+;ua%rY45hYt(h4e1b)>nIz zgy&HvOXmwYUj!+TNy`=C76mh>K53zC?-$k-<`OZWmMYUJOraVTnwS@B7Y$R$u@cDT z+S!-aV~tISJDno{ScM1Ol1Q_q<4Na}0owHEv$<^_L+6QdGGcpL4nl}wg%QRJNutk2 zti05T!gO!i&)};NqoGKMNh67fl!k?GYv7=xDS1;b{i40{*7w2%zD_K953RIwDMc~o zT0$D$*^pANYMjG0X9pYZdhu-BAfM+2by^)CQ(g$`I`r#m$X%1=UY#wEi1U-GORrP4 z5bkwIZ}nSyZXqp3DJ{I!>S#02z-l6-bs*S=&2AdADjY0H_?cMv*K8ZlGqYAy-Nyup zU%XNYEuqwAm@!UK4%DbR)oeWaz_n=9?~$7?ouazWQBRoY5A2JP|l&!CWVsljZxqwutxB9j|P!KT4SUP|`@%&II!Sz&b3{#FaT9A=tSROdNs^hp)QTj|+|ll%o*(dH4Iy14m%ZMr@}Z|J zW{9`lEFq)mM2eaJw|Rug8#?I#rH6-?w2EU6ww8{(u4YtS6s+zO;G!~pi~l=1R-B8N z`YQ!vX2(VBXRdP{?H{|wh}^gjbDFMnE6~qlEZs0I?&!vTwsyfycBVUPvL8z0m9D!E zzdEARG01{u-q&@E_^e_xr_2QA-+!J_NHQTr6tq*wPc^VVQqZ#7H}*}@xL!_S%`kbp zBm@UNOHUZJHk|ijni=20?K5{@e+Pb3={;v3Y$cPp`>9VKibs-mm$?bI6X_t+0B@g4 zqWSKylFB%v`K3^Hi^Ned#Kq!w36U2O6`iFhxy);wseQY!>&KEudq-JDH~E%y2>nyM zw+%uu*sVPFL59R;tUp}Xhhz@~sRrV$v9eOyGop-Ss^9dp)VEgzVto`)XJekwU@4;O ze~zu%p-`1ck{COL8y}1Zx%#z0+EU%puXdKb5fHC=pXgHF&`n#VB_fS#4j1Dv2{U5y ziS3H3R{p@*y~|$C*+$)qxj}%{twB{Zc}%W*Z%&R!@=)(IWEvo4IsrhOFwGg2hU0jt zSFjjkF11M5c&eaM=Ehp-?@^|f+~w0okHO+?7i>Ah=+5F4Kzw73(P*pXn?mKedRamT z76LQ8j)TjxqJG$)nN55G@KKe%t*L_&h4L}i!8WIX-e^QT;oC(=fl_o?LHx%YYm`#i zxV)mc*h33SmW1(_hge)AYSju51tlTqBRSQ;rcBVCBN?O+n`zcsn-Tr{)3~siE(S~LzeR}fL-Z$Q{ zrpb!%g*%;brkEpSP-c-Xvkl;ve%7ymF@MwKXn_h{WlXiBNTlm<=C@v}NfNy6dOG?=y5Ycfj_Y2%=4Bm)Hz|KdACL;HULrAT3}kqNtx?$i*bW`n>9+7wvk3>K zE7V5N3`a_**YpR*ceT^fAlGMeOE8N@Qu=R%1Bi1NNV~R zem41{k%X5JP; zCDSr4LNsJ<+uN?hp4Z!^5MKOANmFy*PY&~}k-3e?1r|*3HDv#ag2`w03evC(CWZYE zm>;-SN;kHchruuUTR30KV8Dhq3C?&VGi10@y9 znN^Bx6InvEI_rhma${?L8+Spvy?rO`ADh6xL-p$*WUBxE)fWt`t)(GnWA4~yHn3&{ zIFUE|(5CzpJ~-ex?^)_Ozy0CvT@8sMnv2qNFOCh-B^uVFzDedWb!rAOCQxvh)=R$n zjp6;u{9Lcbh1tL|xtP7saWKDFl{v$Ewqp_uByS3@zf$ZAy~_ObtSk=$2ANqg$(;Xv zzt-Wy>Jkogte)zR8lhD6wh3qbLMe}K-^mEQ5fmb0hLuQR^g9HlD?P5-&@Ig=c7=`R zJg*=6L%J7O=Q)GMWHzI#j_nKs<`;af3E4$bA1FuNs0ij+up;O0K|6N@=T$SlMrB^WDp z+jiEY3aC&|LEQETo2Pp9;T_!>TsWhG+7CH&BLa{0QOiy*OZxO~XH$uWFSDy$W3KEv zQ2Hz)*G$UWhPja#v)yTK6c@MCA@RDrF!K9&MAs4sdbScpoH?q^9X2J-EKmnNrk=e5 zBTC`*9`hw)JfAwPVgQc}?|1w+XpB=SJ$g>Aywsib1*0x{FsqU)6|1ThcCaf8#@7iN zN;>JVp-mqaht+Ss>3)21;KkDr`Zbbj4!c)wq%}k78*LX!Q}a!b*i7F*`+-r)nDzxo z>WHaJ87N|XGIIe2JWttfRL~W*=SD3z6}SzjW3n^D)t13DAH6yB1mC1pTHdNuKY*!H zJ`Oj+p2~-hxs9K_o-E>-0vdI(N&fS@QrYMVEM0*t7pX1~J>TcBGSMy}=oE>fY}(Xl z!Oh{!RHpU~oDX(_p#W+$A1p69f0>uZCgn36=4mDju~t~@CQqJg$2_&nSa)b(Nbg5o zUb;>e7dS`lG2?rlsx902p}#i_qZ}I>QC1tT{t#Kh(p zL#1K>|6wAXomLP=;!P}*kS7P>sf^Svy{cUSp?tk4%A(6<%G_-#5w{qB8HNZy<-w2O zTqv{g=$UO3B+?R&|M*psP;h?V=?x!K?|EYEt)8bp7#?XI2=FFUZGa4Uh)##WLz_un zPt0WGIZ-8QwuPwxWiK_xKpeS6Bf&OWA3XoDqT4eJHmBnM5!`G#C@LhJLj>IDR#9wxi^HFNe*vK~2P{nTa2b3qm|yJs1Qmqh1HHcZ@= zgThj|esQ}{&j}BEAAV!z8qxY#R0U6jy4Q0THk*x57#rwtkZ20`Df-OsZ?*pT`m4)y z8^doPhE>%OC)iU|pFm>Q?Qn`&%ujkBDyz;B{T>AU`=GNlKh3{A_c{zV z)}j(shJ8yxu#?P|C{n<_oVrHXR4T7=ArUW&3g1qyS|1cOSw%XZ7KkvGu_b9Q9iAl2 z-jwZ6Z&MWsnx|}z7Q5Ob5OBjS=>E^AgAXiudHFjYwQ#I*D9%6SBa`z{#tAPZkUNtU zbu7h|1>y(hUNb6AsYqsCe-`#2<;rJ;{Bqf)`eknjqlugeCFV^IVsD1v6ocJOi%*5? zzkThmSZX?YckNz}tJD+HojvOtFlZ~iyu??=iXwTD*W8G7y|2>lzL@}0<#Hls%vz_p ziFJRDDR6PSPC1jrx9(EbU#DnCrnY;5J$)^h+~KUCM{gbAltuQ;@i9^*a1F@xrT2Ht zFgS%o`&Y|(d@hSopWj!F4tMeybU)yAwu(|N;*r zReJV-GSBVayD#V~KPN2LO4+CKQ`T$s-umq^O&-E)TYV_3Z1qfm{Q^;-fkfFH=srly zUo)Pf+xYWQDv}_IK@=74GKhoXivO9O0o`|YUs!O@DW7RJ&dZ9kvKUXz{>*TjTW}2o zC(fX^aZd_b!oT(Fd(ef((ueIeh`l5wZ6@hwW(wm0(4{2ji@bi!4QJcUg#X&V9TFz^ zUdyiueGji9?tqAt`GHl~PgTTZFKrnDnch`tpN{KeI-aU-C8N{+f7oJ9Y>eH-yb3`!CT z(@L5rGidJ?ldS*PM1D@zz4LyhV!{J-=k++-?c@IJ*wI&c^ z2H{;=to&?t>GwMr{wkZ@1F;h#ddf2UWl0m#7Ryh2`9I>YzuvcWIuQU&cOS-7;Uhl5 zt{5;4LoH;|@jNJ2Q^%5#3kUH4Jh9h1nq690&*HU4tfAIH)!;RcsAN{B8@)}P)fP1x zk2Y?&0Lxzk(%V{Eul)=o_88p>lNXAU#P@E_f<{pPw~Lq1Kvwq`Q!{~3_C%54q<<0L z4vWFGM}_wVfwM#%GCn~&G3O8$#7UqirYwl$w4y$hfz5Q*#VJG@SvloqjYvI&+_o)* zP%}+x5nG5QI1mDlUohXZyIMA8I0{;;X(v;V>)l(}9};9iNN0wBC7s0$5Jd>swAO$! z$)FrLp&?D+La7g#_6t1pDei(-kz(WeUIz<#sS5xGe5LXDF5%LIcD_o6T%Z5bL;Vs% zUwcPypq*-gK>zFsgnSWj6dRztbH1@wB9-5sn%#W&|0p#7ejnl(IH14FB$mclNoxWL z!19M@mzIz~jwpm442F3z@xpWwJR{5NR8&v@4Q;S-Son{Gm5}E@cOv&X{3UhSqRamV zHO9!(b?Zfe_EzRU+S`6T(7Gqd-(UT=YFX_W1dIHPk%)h;I}QljGl%|7o}F$6F|ZTZ zu{FfYHi@$K+&?1x`?uh34)>J~E1VuQ(l-!~z$2+bY~+iJE5e7U5C`A&KV8sr_7#?Dgg*bzdscfX-X3xJ3nEp5bfyo+*6?c`V_5|M zTZvo>&o1=;O|| zD@dLG%6z-wz|T#>HTwc9ED&|>icD;-=yt!9UGQaRi|r*uWsEqMan^4SZX`U6a6lvi zuK4u*H$h2Y2RcG$P>|bAZLNVS1SNm1+GJ`yfZYpZPj=0In0B8{;~#^H!^*5rUlCxN z^aiXou(#SMs@-se5+r8=N;k%$;nCSoHmAP|_zO^vLXMhL6#Am}fS7~(Qs%2*2)Hz^ zUy1fWzPqI72zkp9mUJ5v8-cLmn#0-i_}i;`-p4x|{Za(BWhY3W5Cnt8G&FzIPY_Sl zAj7kEP=Poj-kuLm5DlE?;kE`QSkRd;8W9ISx`=n*2}AlL0MHi|H*+g|xp@G;-y{RJ?6o!yjnbOA4S2`s;M z*mW_Bc&k4cys+Az;h!wxWG2E=9)VoW*M3d#Y&XGpOi<|YirOd$!b92fgxRP9{w+i+ zA*lv>xf+;!tbTv@kD3X_Dmkx5Aev7aQ8yC(Yt!g)p2lg#V7P05Au<Msj<^5{7j03vjQ0-jWs|UYMl* zGur?Gc1cO>z+?{ubwV~){&<4l(a-gY@|rtBc}b|mvJ*&0Tmh9v?d;}%N{z-#$gr96 z7}Kq7R|zdwcy=C4y@;?Jdfj>XsDT{JSK(ujhrfCUV`p_py6?{he?b^XL`=H}H&Nhx z@CYf=VUjWWG~F`oBoP9qi?lcG^$zj+&ObbJS)OTikR1fxsQIHPKQ$WUOGEvNGMgRl zO!5S2TSKX+;sW_Ky)o|NTM$(@KyJUs&;Q8o8F~!OYmFlC?p3uJuck_BG$KPwY(!?weh>;-7_#Qo!1pN87t(KQZb zOroaQmC7qcg=^mkN-BmdxK1n#LD^&XE5xL)HIG-JrQRjTGdp!)h7NrPW~(}XU)DXW z?!V)@|9y4?x*f}XLx`Na=r{~dDJcau>$V~y^RxHDC>NRt?<{}w({{b&b?fVMDeA25 z?hlc)n{H46S?HH(5ZZ$_ic$6(&taD#B4d7WLhJ5=ZmW9y+xr)1Z0=QM78N#9Jh{6v z(*CYkl)UDtSETb9DH*m>T-ojhbw|-B-p!I=V`P(xVP!lh9$h+`Sn{6Ss9&0N7woNH zI?(RIaY^g@d{i1ap>y++-YLjd45NoIfI5l zDE`0y2r=-TLCG9%e2NY<&8Gi}qb6uKNil(oaS)R(vrmI{){R zx}%6*N>lV?y`JO|iv_D@(WJLs@ww3goQ3bRt#)Fam9`U;JRsXIkK@!Ghl$&{<2$+f z3FASo1t2deYGJHGXWGF1gfDxm^d*Era6z7)tcxAYlAi#*>DgemVOG`ba8g#4wvC$A zYh56qC`kJA1zEMz!75r1#2@4w?1tHnjEvKYy_Zkswh=XEiyutGbT{_MCq}rOc(t7mTXpJ>Q>`M1NJMxIN zFSVn1WdsILwp2tj%GVqd>zH=MRe-LKrC(6NNpCxyr$Nwo{0d0VG;;6L@5xJqpL9vW zp;Ac_hY=u~U;96bbne*_VQZ?4U;gSr^k3&q{ERQf#gHA8%qJEq!{U6>K#EXKOqy4+ zk%GsM9!su5_(8>TI_UZx{D@N7k%KvF+aGi9%`t8^yPJ0>4B*q;q)6DBYdVJkIV=n8z%~3jwrjD6`K>UxDRY&UW$2+tJkLV;6x)6VS+YeGhWDS^R<3u z>ylJacb%tXoGORwGrx$$>CRzN3E8bJ>ae!i4d)Wg%3bg}S7q|ojT5JPk40Ft?@|DI zVmu@*S~7J)>9RikVJ!f@eVdOD`kL~q+tscbib?$RdZE7Vy^IY1>z@3%Hmn*xsP~P! zIYxW=Zpu3yAPb%OKR;odH3EjaVU*hra@Ljq%g>+_*2`1Ni-thwaiPaYyF3XWm&RN4EdbQ?ygS|7rm~!(SeP8`TvT6JXgi5E`IKrs&y$g?MwZllqP1Yp08F40EKiV zeerGNDXOKz0Ko_d;N78QUx(QdX1&mXKD78}fBi75*I2$<1ezpauqGbH4$a{}-Tf(g z1A>7mT&6(w!@JhUZI8)k4l z8Y+wwmX5`zetHN<7UG6i3IjWzVG5#e#l2Jn`Q$utSo~86)2$+-Uvnvm%sPEyusY$$=9X%~yGK8t+yccenY=*<#GRb}FY!%RzR*SlMrn>&DW)FiU% zP~9%h*v>N*upJ++z%0-z%`C|O6Rrn1zC(@j8C3RGknN!fsv#9Rrx$HT@xJw+@g9bH+2akzh$i3vpe7kNeo4O5@e|d{F zRfO=9U!v!0Dd%S5kR3N#z7HeSsC(id)Ma(>b0O5=Q4-92q$$1R&?EitLyBA!ejK=> zNM?Xph+9Z4IXEd?$bD5v{A&3 zK463|kZOxVuc}|I)CmgjahGY&_G~TehaU@}4$3Kehot zRPmx)-F&-%)qObn*%`}@&}8$+NI|^@N`FUE+#j4hJj(Y8tyrInIDQSm4H{4#*j!=g8+VV!szAVM zIJfC&p(CnQedOwJr47`=P-xg3L0IDSKKZq=0o?Ej>ugJL5 z{ek*@*6<%1fI;Yms5E%Ex}03!06YryNst3WCAq1@sI?FR_Xy<2p~eZw{k7cHPkzkV zd8rw*_rOrqy&OTp(;6UgIycNr9zEr(W_hl96R3L~ZC|Jz%-~6D$}@@%YwX;CRCXox z1~T$dvzM<^k#F7nIxBc%<%m{KB;9upVp2I`Py*q6K3|W7-y)V%cO;O-jj$+di%YX8 z1#VFHvG!>C%naZGjuOE|5~~oiDq%M8dpJQmGA4wMrOs{f1@OmfcdB86gcHgxq^a98 zJcgd`(X67ed@N5suX1|&`K_p;uX^!p(seU;XQd%zOj_nf%Qk}9U*DHBQ|4p{OA{`U zK#q+g^QM!J@RB56HC@>lyw}MN>x63Bsmk80T)~u?QfbPRBT+xkSibE#Xny+~C9ere zLJv-r!ffI$3<1@+e}fs1FAj%#CLg00!uLedwj3Fb>%O~TM6+=lw_zcP3h86jFRlWH!^MyfD4Dlc!4r$yFu7h}I}K+hA0qcSTx31nYxa4584wyaatbQo%oq)2D>^lf`g@4>kE*a_u8>#w z463laIxgQ<9d~=a{03=b0y)QZ2|j;1%!4oPTMVaq6egA5Ro^o2jQQ9A5e9>ulLY6< zC%kb@(u?pY4dQ#HmgXzUU}Er1L7OlzN0d1vE*sAt(9{&;WRckA;l-olW71d=jX(-U zS{ht^lQ+9Y&3jtSpMhYz$fb{B$vpGT5BC}tbVHkfC8tvqZ4iF z?UdYqtG3ve=6ySvhK_{?(s_H>mx=hRJfd)l7GWw=Cj7{E4^L3F4sw$xB~O|VEB zaCNVS1jACi7M)qWKbKclovubnJ&dLQyN(fNU+|AEDf$FSk}p?|RRkjaNWYv_7ymZZ zrLkV=_50YxJUc(@XtVKF|LrxfJbyHj)bq;O47SP$9=i|xIQ)1B-YHdbB%b>6>M}SF z%7+JYAAK9VrlyJ+$M&ekd3{BapR-4Ez`R8_KYZ}s#`$;WhmKVVbK~y9Wslr`*aweT z4o&^5fZgO+j}(=tTlvvc`X>3|UWPYwC}&G-Ze3Oi$=|8u`JZSY5)X$e=e%1|Ewb8R zZ^Y`Gfc@0XJ@<-lDm!;2h^E)$BNA8%EIN1!^2DLQnQ8BB7KdeHtI-f)4 zo=6dKS_9zaTJAvWpvS?_&%pZWKhMY_l#cLnTb{D2eERN@7|!?YDV?daddCJ!c~(Rf z-v=AY&g+9cZy{=55Xt3tYm zG{RjrLE}Iff*-?za`YjRXF0j_ewaPFYL>BbUY*I}n?zMh0;G?cTLl$ku*~oxS=Ll7 zq;tF74hT>RTPl9>qYVn)IrnAged5?WNbuk5Bg?GPNwso)SW<}9Vig>Vne9-WA+&0}lc{nb0(vX7#IXQeW&Ve7E9SnAP7b>j8HmuW9+3%mBx* zeE5R`pVQnmylPFEr)=8gYCV~hB8MEv##&5U9G!Fc)D}27e&+if(tEnG_i)@V>RK`8 zhz=X{Ui$o|(gEigN%<2#+m`A`YJYKl+$WsBw(X@Po=PgJd*=C}fKLidy*?|)Kds%1 z_g8^Mcqb64Msg*kNzc3axc^v!Kgrh z_7+b$LMgZdL*05f{A3;Vs3b_(-M0r;i7o2f`9@u#HkETarGUX0qU!()UB) zIgn(rQA~1i66#2!m$H)Q^B6VY!TCx~;ym3qTX?emnS?s;Q(OzSy6^t1a-kN_Q2k&& z^mVwbbJy&9BWRXan(a_@uhNnB4obnX1E6XJ@%HFb+tA;hB0JzdU29 zj36D}yf1NPX3J>rH1DR1uqTlsvxZf{>(G~BU%0Ai>Khar?6&`gTN>?IKHYGOJOPQi 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 Date: Mon, 12 Apr 2021 14:44:28 +0200 Subject: [PATCH 52/53] resolve comments --- app/projectsmodel.cpp | 71 +++++++++++++++++++------------------- app/projectsmodel.h | 8 ++--- app/projectsproxymodel.cpp | 1 - app/projectsproxymodel.h | 2 +- core/merginapi.h | 2 -- core/project.cpp | 26 ++++++++++++++ core/project.h | 11 +++++- docs/developers/index.md | 3 -- 8 files changed, 76 insertions(+), 48 deletions(-) diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index e6e8122e8..bc010c6bd 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -11,6 +11,7 @@ #include "localprojectsmanager.h" #include "inpututils.h" #include "merginuserauth.h" +#include "coreutils.h" ProjectsModel::ProjectsModel( QObject *parent ) : QAbstractListModel( parent ) { @@ -63,7 +64,7 @@ QVariant ProjectsModel::data( const QModelIndex &index, int role ) const { case ProjectName: return QVariant( project->projectName() ); case ProjectNamespace: return QVariant( project->projectNamespace() ); - case ProjectFullName: return QVariant( project->projectId() ); + case ProjectFullName: return QVariant( project->projectFullName() ); case ProjectId: return QVariant( project->projectId() ); case ProjectIsLocal: return QVariant( project->isLocal() ); case ProjectIsMergin: return QVariant( project->isMergin() ); @@ -91,7 +92,10 @@ QVariant ProjectsModel::data( const QModelIndex &index, int role ) const { return QVariant( project->mergin->serverUpdated ); } - return QVariant(); // This should not happen + + // This should not happen + CoreUtils::log( "Project error", "Found project that is not downloaded nor remote" ); + return QVariant(); } default: { @@ -167,10 +171,7 @@ void ProjectsModel::listProjectsByName() bool ProjectsModel::hasMoreProjects() const { - if ( mProjects.size() < mServerProjectsCount ) - return true; - - return false; + return ( mProjects.size() < mServerProjectsCount ); } void ProjectsModel::fetchAnotherPage( const QString &searchExpression ) @@ -223,7 +224,7 @@ void ProjectsModel::onListProjectsByNameFinished( const MerginProjectsList &merg void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious ) { - LocalProjectsList localProjects = mLocalProjectsManager->projects(); + const LocalProjectsList localProjects = mLocalProjectsManager->projects(); if ( !keepPrevious ) mProjects.clear(); @@ -234,21 +235,21 @@ void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Tra for ( const auto &localProject : localProjects ) { std::shared_ptr project = std::shared_ptr( new Project() ); - project->local = std::unique_ptr( new LocalProject( localProject ) ); + project->local = std::unique_ptr( localProject.clone() ); - MerginProject remoteEntry; - remoteEntry.projectName = project->local->projectName; - remoteEntry.projectNamespace = project->local->projectNamespace; + const auto res = std::find_if( merginProjects.begin(), merginProjects.end(), [&project]( const MerginProject & me ) + { + return ( project->local->projectName == me.projectName && project->local->projectNamespace == me.projectNamespace ); + } ); - if ( merginProjects.contains( remoteEntry ) ) + if ( res != merginProjects.end() ) { - int i = merginProjects.indexOf( remoteEntry ); - project->mergin = std::unique_ptr( new MerginProject( merginProjects[i] ) ); + project->mergin = std::unique_ptr( res->clone() ); if ( pendingProjects.contains( project->mergin->id() ) ) { - TransactionStatus projectTransaction = pendingProjects.value( project->mergin->id() ); - project->mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; + TransactionStatus transaction = pendingProjects.value( project->mergin->id() ); + project->mergin->progress = transaction.totalSize != 0 ? transaction.transferedSize / transaction.totalSize : 0; project->mergin->pending = true; } project->mergin->status = ProjectStatus::projectStatus( project ); @@ -271,7 +272,7 @@ void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Tra for ( const auto &remoteEntry : merginProjects ) { std::shared_ptr project = std::shared_ptr( new Project() ); - project->mergin = std::unique_ptr( new MerginProject( remoteEntry ) ); + project->mergin = std::unique_ptr( remoteEntry.clone() ); if ( pendingProjects.contains( project->mergin->id() ) ) { @@ -280,15 +281,14 @@ void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Tra project->mergin->pending = true; } - // find downloaded projects - LocalProject localProject; - localProject.projectName = project->mergin->projectName; - localProject.projectNamespace = project->mergin->projectNamespace; + const auto res = std::find_if( localProjects.begin(), localProjects.end(), [&project]( const LocalProject & le ) + { + return ( project->mergin->projectName == le.projectName && project->mergin->projectNamespace == le.projectNamespace ); + } ); - if ( localProjects.contains( localProject ) ) + if ( res != localProjects.end() ) { - int ix = localProjects.indexOf( localProject ); - project->local = std::unique_ptr( new LocalProject( localProjects[ix] ) ); + project->local = std::unique_ptr( res->clone() ); } project->mergin->status = ProjectStatus::projectStatus( project ); @@ -387,7 +387,7 @@ void ProjectsModel::onProjectAdded( const LocalProject &project ) if ( proj ) { // add local information ~ project downloaded - proj->local = std::unique_ptr( new LocalProject( project ) ); + proj->local = std::unique_ptr( project.clone() ); if ( proj->isMergin() ) proj->mergin->status = ProjectStatus::projectStatus( proj ); @@ -397,8 +397,8 @@ void ProjectsModel::onProjectAdded( const LocalProject &project ) else if ( mModelType == LocalProjectsModel ) { // add project to project list ~ project created - std::shared_ptr newProject = std::shared_ptr( new Project() ); - newProject->local = std::unique_ptr( new LocalProject( project ) ); + std::shared_ptr newProject = std::make_shared(); + newProject->local = std::unique_ptr( project.clone() ); int insertIndex = mProjects.size(); @@ -442,7 +442,7 @@ void ProjectsModel::onProjectDataChanged( const LocalProject &project ) if ( proj ) { - proj->local = std::unique_ptr( new LocalProject( project ) ); + proj->local = std::unique_ptr( project.clone() ); if ( proj->isMergin() ) proj->mergin->status = ProjectStatus::projectStatus( proj ); @@ -469,7 +469,7 @@ void ProjectsModel::onProjectDetachedFromMergin( const QString &projectFullName } } -void ProjectsModel::onProjectAttachedToMergin( const QString &projectFullName ) +void ProjectsModel::onProjectAttachedToMergin( const QString & ) { // To ensure project will be in sync with server, send listProjectByName request. // In theory we could send that request only for this one project. @@ -532,7 +532,7 @@ QString ProjectsModel::modelTypeToFlag() const QStringList ProjectsModel::projectNames() const { QStringList projectNames; - LocalProjectsList projects = mLocalProjectsManager->projects(); + const LocalProjectsList projects = mLocalProjectsManager->projects(); for ( const auto &proj : projects ) { @@ -561,14 +561,13 @@ bool ProjectsModel::containsProject( QString projectId ) const std::shared_ptr ProjectsModel::projectFromId( QString projectId ) const { - for ( int ix = 0; ix < mProjects.size(); ++ix ) + for ( const std::shared_ptr &it : mProjects ) { - if ( mProjects[ix]->isMergin() && mProjects[ix]->mergin->id() == projectId ) - return mProjects[ix]; - else if ( mProjects[ix]->isLocal() && mProjects[ix]->local->id() == projectId ) - return mProjects[ix]; + if ( it->isMergin() && it->mergin->id() == projectId ) + return it; + else if ( it->isLocal() && it->local->id() == projectId ) + return it; } - return nullptr; } diff --git a/app/projectsmodel.h b/app/projectsmodel.h index ce93c34e3..7444344f3 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -49,7 +49,7 @@ class ProjectsModel : public QAbstractListModel ProjectName = Qt::UserRole + 1, ProjectNamespace, ProjectFullName, - ProjectId, // Filled with ProjectFullName for time being + ProjectId, ProjectDirectory, ProjectDescription, ProjectPending, @@ -101,10 +101,10 @@ class ProjectsModel : public QAbstractListModel QHash roleNames() const override; int rowCount( const QModelIndex &parent = QModelIndex() ) const override; - //! Called to list projects, either fetch more or get first, search expression + //! lists projects, either fetch more or get first, search expression Q_INVOKABLE void listProjects( const QString &searchExpression = QString(), int page = 1 ); - //! Called to list projects via listProjectsByName API, used in LocalProjectsModel + //! lists projects via listProjectsByName API, used in LocalProjectsModel Q_INVOKABLE void listProjectsByName(); //! Syncs specified project - upload or update @@ -122,7 +122,7 @@ class ProjectsModel : public QAbstractListModel //! Calls listProjects with incremented page Q_INVOKABLE void fetchAnotherPage( const QString &searchExpression ); - //! Method merging local and remote projects based on the model type + //! Merges local and remote projects based on the model type void mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious = false ); ProjectsModel::ProjectModelTypes modelType() const; diff --git a/app/projectsproxymodel.cpp b/app/projectsproxymodel.cpp index 9d25d84a3..a7d59dba8 100644 --- a/app/projectsproxymodel.cpp +++ b/app/projectsproxymodel.cpp @@ -57,7 +57,6 @@ void ProjectsProxyModel::setProjectSourceModel( ProjectsModel *sourceModel ) QObject::connect( mModel, &ProjectsModel::modelInitialized, this, &ProjectsProxyModel::initialize ); } - bool ProjectsProxyModel::lessThan( const QModelIndex &left, const QModelIndex &right ) const { if ( mModelType == ProjectsModel::LocalProjectsModel ) diff --git a/app/projectsproxymodel.h b/app/projectsproxymodel.h index 905eb1af0..28befb1c2 100644 --- a/app/projectsproxymodel.h +++ b/app/projectsproxymodel.h @@ -48,7 +48,7 @@ class ProjectsProxyModel : public QSortFilterProxyModel private: void initialize(); - ProjectsModel *mModel; + ProjectsModel *mModel = nullptr; // not owned by this, needs to be set in order to proxy model to work ProjectsModel::ProjectModelTypes mModelType = ProjectsModel::EmptyProjectsModel; QString mSearchExpression; }; diff --git a/core/merginapi.h b/core/merginapi.h index 9e408334f..5c7ba6105 100644 --- a/core/merginapi.h +++ b/core/merginapi.h @@ -214,8 +214,6 @@ class MerginApi: public QObject * Sends non-blocking GET request to the server to listProjectsByName API. Response is handled in listProjectsByNameFinished * method. Projects are parsed from response JSON. * - * TODO: add info when error codes will be parsed - * * \param projectNames QStringList of project full names (namespace/name) * \returns unique id of a sent request */ diff --git a/core/project.cpp b/core/project.cpp index d59ab23b9..5dac5c524 100644 --- a/core/project.cpp +++ b/core/project.cpp @@ -20,11 +20,37 @@ QString LocalProject::id() const return dir.dirName(); } +LocalProject *LocalProject::clone() const +{ + LocalProject *me = new LocalProject(); + me->projectName = projectName; + me->projectNamespace = projectNamespace; + me->projectDir = projectDir; + me->projectError = projectError; + me->qgisProjectFilePath = qgisProjectFilePath; + me->localVersion = localVersion; + return me; +} + QString MerginProject::id() const { return MerginApi::getFullProjectName( projectNamespace, projectName ); } +MerginProject *MerginProject::clone() const +{ + MerginProject *me = new MerginProject(); + me->projectName = projectName; + me->projectNamespace = projectNamespace; + me->serverUpdated = serverUpdated; + me->serverVersion = serverVersion; + me->pending = pending; + me->progress = progress; + me->status = status; + me->remoteError = remoteError; + return me; +} + ProjectStatus::Status ProjectStatus::projectStatus( const std::shared_ptr project ) { if ( !project || !project->isMergin() || !project->isLocal() ) // This is not a Mergin project or not downloaded project diff --git a/core/project.h b/core/project.h index f40a716bb..ddaf13ea6 100644 --- a/core/project.h +++ b/core/project.h @@ -60,6 +60,8 @@ struct LocalProject bool isValid() { return !projectDir.isEmpty(); } + LocalProject *clone() const; + bool operator ==( const LocalProject &other ) { return ( this->id() == other.id() ); @@ -95,10 +97,12 @@ struct MerginProject bool pending = false; qreal progress = 0; - QString remoteError; // Error leading to project not being able to sync + QString remoteError; // Error leading to project not being able to sync (received error code from server) bool isValid() const { return !projectName.isEmpty() && !projectNamespace.isEmpty(); } + MerginProject *clone() const; + bool operator ==( const MerginProject &other ) { return ( this->id() == other.id() ); @@ -148,6 +152,11 @@ struct Project return QString(); } + QString projectFullName() + { + return projectId(); + } + bool operator ==( const Project &other ) { if ( this->isLocal() && other.isLocal() ) diff --git a/docs/developers/index.md b/docs/developers/index.md index 5b39c2a3a..8122e0f97 100644 --- a/docs/developers/index.md +++ b/docs/developers/index.md @@ -7,9 +7,6 @@ Build and run Input on various platforms: - [Android](./android.md) - [Windows](./win.md) -Build and run Mergin Client -- [Mergin CPP Client](./client.md) - Other topics: - [Translations](./translations.md) - [Publishing](./publishing.md) From f3fddc11d7bcd7f9965868da6475ce5f381dcced Mon Sep 17 00:00:00 2001 From: tomasMizera Date: Mon, 12 Apr 2021 14:56:03 +0200 Subject: [PATCH 53/53] resolve QML comments --- app/qml/InputStyle.qml | 1 + app/qml/ProjectPanel.qml | 5 +---- app/qml/components/ProjectList.qml | 13 +++---------- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/app/qml/InputStyle.qml b/app/qml/InputStyle.qml index e1d03d0d5..89e3465f4 100644 --- a/app/qml/InputStyle.qml +++ b/app/qml/InputStyle.qml @@ -48,6 +48,7 @@ QtObject { property real fieldHeight: scale(54) property real delegateBtnHeight: rowHeight * 0.8 property real scaleBarHeight: fontPixelSizeSmall * 3 //according scaleBar text + property real projectItemHeight: rowHeightHeader * 1.2 property real panelSpacing: QgsQuick.Utils.dp * 5 property real shadowVerticalOffset: -2 * QgsQuick.Utils.dp diff --git a/app/qml/ProjectPanel.qml b/app/qml/ProjectPanel.qml index bf0369998..4b8f7bf07 100644 --- a/app/qml/ProjectPanel.qml +++ b/app/qml/ProjectPanel.qml @@ -23,9 +23,6 @@ Item { property string activeProjectId: "" property string activeProjectPath: "" - property real rowHeight: InputStyle.rowHeightHeader * 1.2 - property real panelMargin: InputStyle.panelMargin - signal openProjectRequested( string projectId, string projectPath ) signal resetView() // resets view to state as when panel is opened @@ -162,7 +159,7 @@ Item { width: InputStyle.rowHeightHeader * 0.8 height: InputStyle.rowHeightHeader anchors.right: parent.right - anchors.rightMargin: root.panelMargin + anchors.rightMargin: InputStyle.panelMargin Rectangle { id: avatarImage diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml index 194c871aa..c49798a20 100644 --- a/app/qml/components/ProjectList.qml +++ b/app/qml/components/ProjectList.qml @@ -71,7 +71,7 @@ Item { id: projectDelegate width: parent.width - height: InputStyle.rowHeightHeader * 1.2 + height: InputStyle.projectItemHeight projectDisplayName: root.projectModelType === ProjectsModel.CreatedProjectsModel ? model.ProjectName : model.ProjectFullName projectId: model.ProjectId @@ -148,11 +148,11 @@ Item { } } - Text { + RichTextBlock { id: storagePermissionText anchors.fill: parent - textFormat: Text.RichText + text: "" + qsTr("Input needs a storage permission, %1click to grant it%2 and then restart application.") .arg("") @@ -164,13 +164,6 @@ Item { } } visible: !__inputUtils.hasStoragePermission() && !reloadList.visible - color: InputStyle.fontColor - font.pixelSize: InputStyle.fontPixelSizeNormal - font.bold: true - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - padding: InputStyle.panelMargin/2 } Item {

Q1OLC`XfC!X;C{FcUfTQu!9 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�Jc za)wj*=u76R#MIm&-sVv=r_(Wa9eP{b>{3jyVET}nN9_M2#Q1a#nVi|`>9srDw?MnQEp7vJBQrCz;7^~F zoz5&{jP7QTORais98#FG2$e5HYEf;AvDuclxUIC8C+?CB?m~08yQM{OD~L$?B;wyM zIS;4so$LgcfTEHZI@P_=>(~=82B!t{kLSAFEi@%(i8ZGd3bZuiHo3wtx|nbeOKO~T z>RcX%ZzT7%A7&RgH8+KT^&S+Xa|3ihrl5mqZ6tQvSq%Uot{rs`B#hKCT3gM*urg}| z_=i32qRwAEe-eJAbjSj^HNgCwW{(II7+A3}p;ai-ysmuMQu?gxqNg-ndAS`#@&&5& zuXEE#W~y8JD90V?7lsLz4yIAb_c9}t0<*9)aN!my(GQhD8f2udDH%#&)Fh$a8zb6i zieSV{q34YdhT`=OV*m1)l`tabTuWP_;;%e@l`y%sQ6uzelO3i;rrAnu`|OQR76R8D zrPt@Ka(a3Y1nZe!RuI8<6OP)$?K0iCw5Mmi&=#HPSPsfmsrFa-8G-!a-n4>Gyr*-f z8wG^1F|O?GsIG#tdG_zdh(;MQ z`#YC5ub*xd8xD84Z-NF53ebb2LMwVz6@I^pr+UEHXUA)F?h-(Iv0H&G+MpJGbjY(^ zX-Mc`+zO=r6CZur?ym=a{&HPn6S6ln%(2VJXX}ewY52iuw&%Uxju3jb<W3h41j3m5UJZda!JJ7HvUrc|J>p2{goKC+O>c#({ zd3w?A#$I~A6z}@{&U<6-pm1^2&# zrR~>JNbqmpRkb>0gwf}sKh-~F*ojW*UFefF)1Of{+Q}>|=zL~H{kkFr%n_nch(I}d z6Eck^{QcOuXNaVcm#~#2IZromP15OlE553lOg~#RB>U(}0TTbhNmSDN?r1oh za)vN|eOxA#1%W{jmit92ukA||;lwmQ^d`5@;hE#XT-j4$xlh-?Jk{T+Csqw2l}73< zpF*Za6iJa>fuBXjkq=ocwA_vnhK*Rr`)CZ{Y`tp)=B-l>AdR4TmhlxuS*9Gjr5TY# z^05TOtz#^Fh-2Xil0Tk_;pVURzx&Y>YORwYYIZatm}M8crTk}08nsWfcc;4A@`v+4ww8D4s>pZM5GuB9c;8Thy$ps{ z_0BoY@(l+%+54e|Hp%Bb5+ao65sc*OaZ?Oi8XO&SFc{DfkUM;-Z!O|LIsM3m8250d zQbhTNX~#jwU?wfknYUuv`{P0SeF1`WpK`CRS{D>&2ass_?Fvd)P7XamE}&hoW!YPJ zG?^hK1*-L!Ts&6TJtkk9`tYkL##(b&tDR$!f2Bc_pJJwotvk5|ah&AjKJnmzH0nnN zVNl_0!cej`-}So&I~vfk%~qna=)mA#av!Hj>)^C88A?%1H~HH`*Qp8zrV`q@n7^LE z+aLS@4SWT73_5c+V2Z5(e6VZC^2mOz%mb4d&6-IiOL1CQ)bJ&b6Sd^1g z&RD+3?HE{kfmg?gygYX$LAKXxK{ozn=Mc^1fTZaS8~5UzS>Q!R&w;=XImW^>xy6Tf z?%rjS%QH=0NlrrSc>YPm!REVXy$J%jUP-yb*Y3&(^G7yIwC-?waiJkjFV4-@tAv0 zfNjEp@chHp4d&@f*KgZy$5CE<${5}~{!$BhfQEby5ToJ)M*zs1IN^i38r0ir-oN$; z^Kq7=O~1!1e(@3-6zYlz$gCMZdmGf8XprV;uMqB2^XplHk}nZ1UI<-p(zr?-06{Zy zkdGu&vS@_EjwAiD=o-n7yFc5Jt`0f3^1Au6I_+?rVaFRm9!KPS{@d#F`7QS`n};vq zV<$DY{BQEuZ8RuALPrJ%6j|))g;cV+#YDm$^$_Jwj7K4r6gHgaFY@lIhydHvH$NF| zP9?Iv9E16IAmVO@1piY~2P}ZwQCUy-!fb__+Ph~9Em6%kbXci#SMsgg*=X(B>{RR= zZ9CeZpRw7BVhZwGPz*hPPDQne3MEocOe4YSkoPn9b@vfnAtHY7Z}BzEbC#C=);;0y z^XIQ8p>ek!Wuft5;KCG2Q;6eOPQ$&vc{5A$Ic`CNK{oGacquCVs8B zVmFqT)>aLJR=bk59T9eo>ujN`iH!l`BdxBv`=S9>!ijay?(YJIm5!>$31`jQfwhHlQd`a%Ly^|V2`#F>+uJo1Oe z$29bvX8^He#7u3Eq3~qOk)Uc4m*k@=v;WWWxSI79k#nJ}l?^Twy6%pq2#4n{i*4JVVFo?7YfSip`Vqju1t z7jT-W<9x&}vcVYMuSN-8k#`VMJ&9Fpq-cBB9$=R83G~9>p$F^7`90jUJt)7VMXjJ4 z+2nZCEw|7M@^n>QvIo}hnID#01A0NK!ljF5u`BzY6|ofxYl70vIKFqeTmjGf*yV`# zO)__hR*X6-Jpw0Ok2vmBa2_Xi8hu6ez3@2oNg>`vMF2r zLJ-$H1hMOgyUN#;*j{J7l7#C)K2r0F^{INsSk8b+m{~ zW}=05r9hkO)km|8nP*aU${G#h$1&&oe?k=oY@YPW>mA@7Ir$S~Th86&EeIp0k~>{H9vQCf~X8O~AN@ zBvjihW5>n?ki{jZdCQ_ifmZsumj3E)^TC~mKI+;yhoaDT-H^DV%NvR#kPwMQC;|uLfR~8+dIFonH4@^@q@ZqOgsC}ED@EjG)s?@kpGUq2xZ9zVJ>1XSH z66kV%vEfo}z}P%-^ZaYAb`qLdgoFcS)|U~`#(~EuXant=GDb--vtklP4%Lb<$&Y?s zJTW>fi?X&t9uT~QINC1PJ|zGO7}xw2a6^lIy#;t%D#H_`Sa0%KQ3Dcwh0o{tVb8za zb`Y;44mH^F5+|SvH6p|#0#kIPJoDYL=XI}A+D)Kiwlw*;!-+V03I;F-E`XaGgW8Q^ zwj@1)32J4+RCvdby|9eU!{ZXm+9`Dz@7{aSMmA9Sx$zmxXIe~w!4OL1p-e`2aDNwM z1q*?$cIuJAPU>p7>JWn%j+!BDjqq|xL$hNy`yn_6YF5nQ_zMZKiB2a*J9J4Jy%TQL zBl#~E7u@2`U9(SI4T@$hG_u`(-ZL?q48Cwv&09T}PZKP(F0z`Hw~Z?-2vyi`E?0T- z>&51`PnwQVYC(Hwz(|w;pkpXOFG8xQyy)0_rpI3*PIz z(PTb3jdRHpCdm`xtRCcuRbGnbN~6Ev{>J$#sa#oeDM3L6mkdANG3V7S2jp)X7e5MS ze|z?s)O(!64Ks>iXDHkPNrRD+w-@J34TKKY`66H#geb*YMdh1{hhy|cZ@sU|<>An~ zVqJ?9Fj0=KTHrq?#fA@0e~^;bAs5uvii%G4vlS9BFui$Vq^rj$Bf2{J)w}d<;o?x+ zP4jFG9dDy(rjiosR=xOVoMn3Rv8B}16a}u4r>gLGSlFVg(X1Xq3#$h^aJ0M>JR(oc zW@jPA;-gBLE`>+G|(b zQM;&N-~^UG)?n+c0jU%Ayj}rY?iJu^#@3qx90ih)bCy~@7zh7)+d+z!I_?Y6Yv^#1 z4NB48TtHtgJoI>yF%#@d-zY@%NJ0&$VM{`?=;NeVTUVX76hwolry|XEcvpPh%cRox z>o)+scMx!W7+z~I#6&xC{j>*I75V<}{_8zK+d|t|P)ILEFZrPjxYn^ZDFgA2O2SD` zf6|bPXDk1=1;@N!MDk!nERRTDr;1cl1BQ#{rqyhvMBz_n$DQRZkEziK?ywMp`eyb4 zUi&9Q#8NT?@2a$&438z}Z8Wd#)SJ>tW&@dTs*Ke6k&YHPjLh=MSS^?inV2msy=>ob zJa4b$9MqT>el;)DlAD&-gMl+T8CrujE{N>A05v7&cgRBygaK zRENW>kkAz<$E{Zi+)XS|soe45@Wa}H)>C!v5%Xe1!;XCnhw>|%7!I;n>ODWvpR;t$ zwGQZ8DHBEJgkFSh0_PIiou`8Wwk#AxwE54H(!VIHLI}N#gPvd@S9k;s}?8F*dOe?stdw?~8clflyva@UoH-oEm6qV-FWj>lxC^U)Qre5OTTG zs(F0t8_U2vAHtJfLm$2qqx!Ix?agh_y%jWC>xQpdR?)^PB?}?WTNHxvY)c3Tfp45& zY{3{Wv6?9ti@gAYSP%rd!n&bdp|K=I(+SW9qQL+jYE3a8H$Yddzk`m3Mj}@+`*koS z<+lVr`{(4YKd}YD$ixhLEHLS*qoV~t7YPE+EaWaLuw3dQ3CY_lK=+T?$j2@jMwqEo z*WKIuRcveS!gjrf-Uqvz%Rzb!X)zrNa04=wa;{(YD_jXh*{_oDKEV5sA&8bA!D_Kl zm5-j?ic{Ip?rH5rqNgG-ELV~ra0p<)?5K7Z?AaUs3`@ST8Z1rpmu1zihs(`QsT;t$K4$n+OH39O5n|Tpj2fX-)<~o15u>Ycq+7(3=WznN7)|%$_r<9~ z;VPiOP_m{Mu9ZHc5W=l6+h(Wy(o3zexLQ7^V;L+{D`*3 z+wqfl3XVo3#{3$o zY(7QfV|L!l*i>uTnp%Ti46g8-41v#2lAeVO*oi6tUJ0a6G840nW0Ba8erhX2e02o8 zy%8|Gz|h2mF+Mv_E}4>@T~VkvozGs<=hfp@!!KPT8IN>44E_KvV~B+1f6eBo236<^foc5QSwvzibDW-=j#wkgn3-~ z8#uIc@}7j{xNkuC9->QbIZX;Oj@}|F+T*)8ZO$99@<^T~@)o$YbLgfH5;${EC0S@z!f`4VSgIwU>YK2%=~g}LT&Y? z)9@_4+o6iNC~(#C<%_w=0$*pOGx6{Ym`~kBC%-t4-keTo8kDiP-#DMJRmzZfRCo!D z+1riON+RSfPA;$2LJVN@Ou+#z?7M8X_%R#2!E3Yf5i>@BBaK%Ab~Lmvq6|mG&%hpw(k*uFU?cmfN9^(F=%~44 zXkNm(|13%LmoH)$G@)@@FcQ*$bg?Q~fwV|BYTx<>+6+ppEmQ0Ex<%Kszcj#0EfEI#$R6sz$B=B{_f&J4aeV|>pm z3I1M$f8#Qs4HO}qQc`%5W_@hz_{WEV&oQ4vfVWJ>>kt-%i(2a z49S@7q-N2Eep%2;8Kq+qZ{RX3T<2U>G7|YfqmOf}bWCnBoAfugNO;a>-2ve&&_Y2w zC+$gwch~FAT>pck8nCX9%9pS^_j*wq8ZyLZZ)0Lew)LF-Si3dagl#U@qw!c+w7nhd zjCt?CZ^%$@k0M=PrE0K&q0T(=^aJ{yPtR)R#_uDA<`;(Fv1krlfh$++Og`u{GVBve zNIJaAdKO#;98P`w9#v1+)T-2HWUK@pplx|{-JmL8JV_Z=vG?)udGg7~B9&fGe~w3{ zP>t!JKIX|v#@Eqosjur0*tq|i=1}qmKLYP%;(mhc8w=?z=KtRpWi|)q;Q0V%{CnVL zpR4g`HW90!0D%{B1NiALu0@Ab(8GY8tXB6o3`8dv8}S#2{y67yD`&YD<-cakZjVOx zXB>qewH*rkPfESXRCGZGoYhbJCfVhiFCXK&XLx^cbFt8KrqPS0^MuyXb9+vJEQU@8_4q6>9WZYe7NbDp+PAqm=3~gV4IpEPF(Aq&sy{UF8qtm7 z21xhU^hF*f`Hj~cF0hP`{uA9`(J}aZxL@?4s)&!JiVph9NWMLRig|k}5TPB~n>j5> zHPefRc0vB=P@M(92KzpBP!ub1aS@mb9QBOV*x12c^OOdq*Y0=Jr=_j}GSP!(6@L1{ z>>EC!)~jj$u)T#xPZvk)3c6Xx&$igaFDOH!ejo-wNRLIJLjL&{J z0;ix=sD(d<)<23KI27%uaI%xgGA!o!S^>D$~&9BNzQRCk;gm2CCu@HG1Bwp$2mV;oR2 zcKpgb?2Zp6E#(Ot@Qt~lY*$9{sfi9awFt~2-afKCga)iGg>g=S-Ywp~MIOp3T zDxxy}eOemG%JP>@HqglkJ9Ot*dT^(j61+iXX!%Vs_qNQJz};DFS<K`BC7 zdq<@=4C%wG`nIy!%Dd$xW=L?g$Td#uopX?ev)=eM1ZEN_Q*fwST0DfJUsN4QK3Lb6 zxPHSav5>Y*+5Qd`em++7wG=EY7+zjp$mr-2V65gSFh=K6$N&biG!?rq=JT?rT!C<| z{%lI~7%2U}a-RKPp)CQ(7k4ZOfuJD}^QpB{K9D7(8c_ZxSAhL@?d3xBz)v<8rwx$C z06iv_`6D8Kyn0YQN7NG=ITjnSQ zNyq5IxgsJ4E7LEHVc&q1rj?*|3@cgb(u!fZ)QZD#8=$w~z^0K_H?)pcJgW7USOAH(2KL#afB+Maw zC#{En7pAVnew}c4&nEe|k7dT|dHh@oZFWbFW(M)(yn#sG*YF^TCS1;E>~T0me#4`d ztp^oF0?(Szj0c1X(2><2ittQmT8Bx2*{J6Pfw8)XIFOa+;A34Aqi{o&2w>pxpd>~3 z!s<|dcWUOS*t2lBy|i`MpHt;imrE0D*h8bH`FCOz#E@kZ*u>ClJ%D(F&F<&xs+uQa zN{D>)3=l3%!0iSj3}TZ>d+|5wU;i)j-)D>#Oz3(!4F=nG0ca~2xJk!)i5W(| z%<#p22WE}h@#?R;w78v>eif3^=0NnXScY=M!wQx2i1cQ=P!Nn2A)hpl8*N2kG*YKV3yVRMeH)$hb(7YyR=l&pxz73xP0iCW?{+>*7 zn!NJ3%)wEm$hTadbl|_Sj!qBYb|>8d+{kYZ0pgPrUyyeai-@U4K%<1J#5!!T+#(pONsyWf$M?{7D639OztMm1@fqh& z?!| z2$arjks4(>UU+i(et_k+_6F;~be53$YoLMF@I$WKjs1h-El$Hz68SX5hv|Bj2NS2X zNU`M}$79iCWMk$0uNQu5M-MEr#^}E7i+&7xvwL*dWOh=;xkx;=| zR-SN&a(Suq8G=I1eeMIwGCD7I#O@2o2^B8M@iLTpMy*2TWqy2T@EHE-zG{*;x4N10 zLzcSmqLUy%)=Yi2zt7h<7ams+djt|eH+Z9eoql+1>Dze!W2oCea8)!75G zvOM>J#^_JFPUP)%$xd{upMyT3Ri4->D!`mQyu#*2EcHv~5e1Jt*%F=}-K@tt~Lx+~D!iR`s5D2Y-iETKU4 zj4og%wn95O5!EOtjjJuDG@+sre^E-25Nsi8XUN`g96OfxP zlUCIpc27(u$G(!kNm6CcJRIcpV8?JE*X!rur=2Xt^REypeRHFRO$13TqY+)jQvOw- zroVdQdX`qaG)()PF%u_1n(MS|rSlLl+JR)pj_hrt$pX8lFi81SOzfulxsJBA?^ZUq z-~Yxc7WBZmBc6_ZMg4bGa%J`ILaJPL%N#F%4tcq_-M=_Bf2YZ}I$Cmk=Cx=eTWip$6Bkck=71Tb>MYd^qBq|z;56y=+Sr+)7CcRg`QYH8Rrx?? zY`GK;_$#KQ3|4k&+qw21ZWuk05jhQa-nNG?YyShep)Uz2 zBJ_sSg!*5^0*b}Gz^~zS%q3Q6Hl|RC0Ib1%!E}3kAci|SJp~~TNLs{zkXerZ85yq) z43N(C0D+vbQngAYS5!$no)54q1CTMkskRX52aK%1elD}0n(c|U&>nPl=zbsE>jOxZ#`q# z3XN?t+$K>mL`xtZm@1p=1SGWvCxs{9`v)bm%M~`7CwQ6gLJ||VCNsEqg5Gvk$Q(l~ zrvfb*Te4@W0J9zJ#-*4PzYQuj_Wd( ze&)RM^+cJLbjd(w%$;1+p7<0lRcNpP$~}wV9JMtY2fj^k|8N z*-i&naHBcpuA!uo79RaK9kY|NCVgu*@6l+_^Blz3Eg0bk%4hRm6 zXM>+f%aa9BWQ2=PF^Kn4qkf_OKT=5rY!p&{hu7f>ye>x>5OfJ=>dYfGB-aAaHS{C)sWktmMh7~lKp^6bv-C0A&4qllmd1@Q>E7I}TP;kU ze{b6K#Po?GQSzJg-mNR|)t?&(ldc!{RxPpL4M2S~L*j@6P1pv(R+==}*b*4`mo~wT zZl6x{H%tlHbjOLYy=e~g5TcH}(`e|D=1$y_4%UA01SOrh6Ali=s`wyj>9<;+ z%0p+%b4RK!CO+7@%<=krF+4lEWR)4oltft4Yycp?8)@vF2ef(k$diMhciCgA=!H@j zzxC?!rTsFtLjjSG>$k&-dL4Oda$$`sbw0*!przpBAW3X!_&zYKHl9{r$n3^lU}Y-+ z)FyNoaHJXc0NyYF6Gyr$_+|tDCo$JUgf%V#Gj~Vugv<=M8JJVQ z`0$ceJ_VWk=RdzO=w~x1ad>Ul5FR}y{J7p5X#iaKFTpT^W2o3n98bvvS|TS1UjwKQ z@aG?Qek(hSDbs4Gv~#)xrk=8Kj38s>S#-eN%B$Az4pm}q+becJPLX-t6A460bwWWa z8^1!6cc;YXcd+YY;v$BptR(b}p9EH$`iI;#Wy`--*v5mLQzHDp((5PZH%JM2)6-Xn zD$rn{JRXZ>c9V$q$15;3#P?Gb2PK>bdx82ZK~H;uje6?;98_pt=^fu^Us{jLZ-id- zes@3q7PmNK^Sc-)jC?#qreyYphlJ{QElZUlc3!(A>0MPQ6BWXq@5lIff;7)_^^wBA zwtj!XQ8Ku1L6}mOmP}wS%#-HRF`55C4dRuajTEbM`1|`qvuD%!TIR9waj|%C5dM7f zPok$H4o8}4116xagH#%1|Au`yKT-wiD6^vY(STa`2rTMXhmia?=}^s8>AbUoLR8WN zSk?sQX>bb#fH-0R##sP~uxa6xnA6)v1gDfwJ#pg%62Lo|-O<2IgbXUL3k$2RFJ}7lKFWFE z0FBJx?N8xb#}!^sN@rB2^ZZ8p2&gj1$Hy;INA#eElXCaC^v8>X-Lp$Y6dQjeR5M)v z4_*HqPxb%*kK+;H2z8K=?bsPnX6i(8j_q(LQM6FmduBysuauo5dxl7KkS$3XWR{gO zvdXBww@2&se19&#-}7>L{uRf0JRbMQZCr2H>-84Mwy)Tj(Ou=77kV#+Kd5sKBIVQy zZ#@m5dJhUdgP5*wrh5>?j7g9wA3nSQFPNotMhU>6n|toaNpS8eDBC0IMv0h3zz%{y*lm>qmT7P1$DTdY zJIBK% zMno|o6`B`#v3y=LI^Cj>=2xw!-AG{oIrogQP5fx2U0)9>3-j^P;G6m-tx5!=EYpt> ziR(^Qng)x~m(S1C*=(<^s?VC&KYF&c4H52$tk2%%6?&Osb#0wlj|XUF$iF^9lC|K2~e@Dg~*SV2lr^j^)M^SrR6qB4?F8d8(~ zf9@5IieO}920K-YV7s*zwQNd}l_|?CX;K z2#`_KrFpy`_Ch6+#rXKhg>D!5krx}}u8yoUihkPm!RGTNu9#bzLJkg| zb@k2d4Fe7o-ktd&O^1-zev6#GQ^*xpmHDI;7*UQVx^wR@q zI%QfZRK{%v!OG@evJQ+m!3WBNa5m8L4EXhfs_hFh?8Z+YNy4CIdkCskOAv$6`y+jNJR4qUZV6atE>get^eV^-o zSfH=*G8I7E5n4yju1Dj#eieYVp=4+up`hXQ_xGgbY+O;IY-m^-9N8(8g>Tp|Ty6Q> zimSnuI}2L$rKKh8#fy`NRptgCh*0j_Spf@(;w1yFS^VoA z_fWc@4B&Q%^t0=@<9CS*Hpw9(dm`TmHeEMd0~K z32PzcLxuwbHQrQ1uWw@O=ZFLKzj{(~2Hg^;`595`_BAVIt6G*`y7Cpio4d*4`_lfX zhoPb{*_*{)3lcs=5HZ=A@T^oY2vZc>rM9jUAU2raC?|jZ8|*(rHWxa6Te3QIWuPOY zi2o7<`Cs-}$a`1840xd?V!9PMTw|9S$$r4Sx$-#jO8$Z&$)mI~Ywtiz{+5^f7T`_i zXExZWyu7kq=C9Ni=(!yk&dwba;Jw)x!ab8yvBIUHb+C&ggw|&M!fPrY)Re1d% zgs{hNi|Anh$MUZ6qPiN{gi?pK#dr`|E9XW;tWJ<40v|0Z5O}u1I~&%=(lRo((eV0O zx=TQ@DRA-hGk;oNC=xt3Kjt}&lPO@E_UhfB)~8RiVC4led64nn_S_PyO8nzu@au|m zvE=r48Yd^GZ$LA7)zA+rLoKVaO8S3y1WWstEwF{{!%}YqLt)O37txXv6LTp2#$nXC zQG5F(Lctj7W#fi-ZPD%eTlD=8Tx70p9VMNsTh7FZaZ;0sUorQ6KHTd5T@`jkV8^7b zt+wunCb?Hywg?y5*WYw)Q3dUEjQJ=%_pADLrUV>$Uc zYQH)^SzH+o>+ z_zoKQB?^>503}PG2nA?&*S?Zq{pUCkVp0?wGLA}SUd=^5_t%doQNEP_eA4XlE<(Rt z+}*gqNmsAjB2)0ELr*2$-DOM9mU%a{r`V_bpqz%6i_zP=sZG6VXQ#^_%7kSH26~Up zhhRe5+9tZ5*?dzP9CKK?Z`M$km3&KZO$L|q5KCv_xqlHU9jvZiy#~GnC$`yv6hkG-R#em*vuDicZ@83@tTnr?wx;_=ojCHMrpd9C+H;!(W&H;@>=T8Q=dxV{7`2S^`gE;x+f~? z))A2fxc^yLd&)CX=B-)OMdKfAVp2#f#hHq`tJAcl*u}DY2J62c^m)S{=`grws*Bm>&R;&_H-t3vKoz9o zu@E^qIoa+%O~w*FrZMy6hENIX-VFhH^mNr#70)F;2*fw$-#yF$Wss6)`r)g|o0ns| zcCd;Yi)IfxKK%P(2H1KkP~#gXb-+Oq;N#md=DQN*t{eLx``ViIY z596*-epxA=sb@NI8Fy|t@4~7oSQ~p3(!N=KZS&EB)G*&D^NQsDcMJaGUC`yj=ur zT>QV6jD@j8-$RFU!;jqPSEiqJf6dyBN$ORaFx0|fk6zpl{z^wTX9e9t05QsjdCFTb zi~w0wri-yHQDBT6;dw&kVygCF!v5dm4sfS4E;7+o6%v)a7913`1p3&kZ>W^^qh{{} zBp#GMc(4fy?l0h?FgG|?Xa4JmcpC5tsqUnqRCgLT6P3?k!eWgN6)GDCMBNc1$ja_< z7BL|jFFTcoJq+ePGCx0Vn0;h;zN@H@AhiD~J7=PxI1;|N0*f9lWPO=giHmb%+RBU6 zP<^PCTl(Om?+$l!#;4oC?N*nsh5S715TL)gy*2JwpnsF*zzQX-(+sGtK3oIqUZ&r4 zQ4^5!x`Vn(BMU4O*<~-%mY#bO4W-<{G24T&BuVS2711hkhN_S8tPR|Ku%1XCI{Tvz z<_(2c;#Vbo9iex!`2Gp_7AU_1_J7a+4r*6JEaf0<{Us9PVUD*1MgSk*%PLx<5;+1% zB+Mx@v-d!kY2EMe*zoe})*HWHnitdy_etmVu`=!!bj|D9fpfvzCTEi-KG`tIT~Mde zY!|=?Gfo`za8i}xQCJWr5B0FDCr>M`O+US-AK(Mm&=nH!Jkr5&#av0+2|$nfAghGABde;%>t-@q zz)q^K*6BkK@6AQhzk{zV-j{DExE2ybRVOGYhy;(>SuI67R3bxQZLQ+lp`r8Ozm_6< zNo(*Fi|O%SK}1NvQQ!bNRhr7OrEbU5JkmY=^eeNb4>>uB?wv{*RT_^H5{uQeEvA2j za;OgF+zESkVJ6{j9G#J1OM7FC1KbT){qYfo`h(UFkL}*Q+j0Ioo8gOowVYeAnF^}c zPlbNSeq-4FiZ0=ud{cn;;X}$IJddx@b+pX&-#6eP%)qbzN@H2BRD~ub5nn#E7p1j< zXH@*+aSoUXx-{n`24F$!hY)VyXp2}c5|US7i)YZ2C&ysivEFBxlan*?@nbmH`yh~n zkc{9${O{WfN5tlKz_taxP=Nc0=N<)z(Wf9LEVW>sye67uO$lpmLxY2NPWPtYLL8r; zn>?TTbwbGrsI&&28xT7tp1))xPLZ0{uAaxW+;yy}=pCGDRu}owS+mo|yp=k5|A3mv zG3irx*=M%BY;@8J8!_{VBub+mYokn8<>6?^-RA?7+MinuS3c<@k8sTU9a%aavu<|q z-8sher`kmi$?Y$A;+($BTD)UQFY?MsdZ?0CTvByDIxWN=<@)*UUNJGTxkj3Qr9iIW z_{2mYBEbR;>r)8zE2^S9@wqN~o{8u8oqA}`y$P<8Lopra6d#0DUHAkTO$IR*K`}10 zM5p`X?s@nNQ~A(ZLlq0-=<;seHR`B`Tn@tI8I`kAVaU%49PEE?8P|HV<# zA^0*+sf|%J>XC67ibmp@iSCO*W^%X@wwoGFcsahgaJ&4_;p4tqoV}0NxQ@)*RiA%7 zBj{i?_h`POqf5<*Nx{6%Yrm$NK&;my&+s)nORH;)0faJzxE>htuUIhb#4Gz(uDNXjt4AF`tWC%HrV9V>-m42EXQ-n& z57{j7W`Jw+&YinIxrW(wO&5>ME1d}r+YknM-wmCjNLltW^B z&}d4pAeZ8=yW_ies|!#q49H2p^1mD4UC>?sfEL>e~r za|wrr${dytD#*@8?J$qI6;|ctldDdzv-tX?$m>o#oWC1vv)dyMwN45=kd~VJ2iWN~ zSH-sVv>b0Ou5&lhq^`f;kdOLeGGlU6Pe*NXgo;G6&Pwu_LLZU!u-HdUx^NQ4fO=_) zhWl=}AO7{7Am9?nt*z9H4PSY{SU(EN$VnB=UGY`REY5@ty0oKyoPcn(z*O+wQA;1k zVvG8HfEu>ISk?eaFLMt;yLY7Y)R1_^zXfR)_Vm8kt~hun3D*_0baXDh8>;lJtdmdQ zQ|rAZ?(6GY4q##=`$094=^o^Y;I^#O<6e>A&=azQ^S_-K#%rCt=8Pf{B7Di2Zi_sT zyLVe&xE!JH!9|W6r!_7$;7QHo4Ub4JP9%M|PLU&FC#aPD9z(BEvfsa{dOMa?0rF=(&j*kbs{D{?~?mFP32Cw&5Q|{}`CT;*l7Im8p;K zY1$#$$T*H;(^P%-!N$HWw^C$J7SL0S&pdprrpKekGI=RSToF3{Asz04|Ml`oG^1fNS!P zf*EkS*)b9AKSk*dYLsAKsfEg`w%5JAj2}OKg!cKQmHdp47<5AWV)NXmvT1FFBm^w8U3zIT#4k@uT5A3;=-hIgNgpZffx(bZg1s9 za2UP-Zk9;10fUDnUTWJGc~}c+-ssEG*x2YLe)!tkJtt3|1g_zZ$f&3_z8Cmh-m)0ftx~0 z6Ulz3oVK}w7N0gl@sTutZBJ@qkk(Ym7w`6cv!^8%x2|mpwZ0-?6{to>%ISS}s_o%q zz6Ax?O*7Z*PoRdN{B^KoHa@;HbWgA>b;le<{T{iQsGZ)5Q?eR1%4$#QiyfQzm zbnqD+VjPEP+1*Os%<)J@n%I!q=^){wDSS%1eghcdRpx1A+`~3X{6J`q}`J zh_aLdp|;BgB;jr4CHhD1^M!TcOC7&!{6HG2UE7aQ*$16c`b)*UU#++NxXQo8w7dOF z`6tDFoZ-bjr}Jl~BNddZp$I41!fl%kAde&(S8Z&v4MQE);DgmXr*B+UNlptKakX40 zuhn{fuI2Ll>Gp-QxJP#{cpWYr_UmhU!`v9nYUN-X?Ysk;p}3Uf=*v9z$`$f@m7dhQ z7`bRaM(o|U&*ZVn^yt5r#R%)nju=lN6c>QPrX*UiXlYFP-q_Uj<&d9%A%?@h)Z$j6 zZ{7?@P2G=7gwOeJdL;}Fo)~zc)B(6 z&i2!)QW=_s{fMW``uw{vkYFW_8`fU^ol$pC^RUPOy}4uv!e%gp$mv@xSacb8`rdD2 zKYxS)lVE(f&L#i!gUfL);3sG-xzIKlzNE(|_Ga&v_c z7~3vYU`G94z!1$6q~vZq9g>s%redr0g^lr`hUVpGwvaz^_WcZeu-DMWxRp9eqTW)i z=%jMg@?dmut>=e*uQMBhepcHA;O$b$Bd7}8>?;}DOUYhW=q<|h-odgAFY&WIAUob6C2-I`kJPL@1I&tq>iMvp?MrJ5>W-B)A$1i8-y6N` ztA3>5J+P{=sO(=T&Z0TEj~%?5uCOw*v6vk2OmA7wCy&W91f&!`4k9HyEU!81#eNJ`RZX~R3D_Y>N zAoIyxQ{{_O*5flaW!5NjDR@^UDUo-v_WaI%Ik~vFq#Ma+lbjB$DkEKSp%Ztcoe5KE zjr@?TMQ=_DHHaYsp?PJ-SR7QFeBA&OO*=`%0Jz%d6E5QTjAD0=gH?%+2+hC>rqclU7jbD!L` z%hUS1-mn&~S--`*X{7XV+e`-?OC}lYt14ipGAk~grt)9SC3>2|>)Rk0=2FL>_20Mz z43rrk?5=pNbzjHD|K7g76fzKlA^>RNlan+09*+z-uUwNlBJ-d7H^{8W?8H3(TX`Sn$NAp8Qd}jy38F}^M7LlwETeML6p5;awpX4mMt{^n-do=grCz~m+ZB0 zIfqcyOMnSoPU8?9IVEZV>s8feuV4q z-5U~-@mMf7f9z?PBzZe;A8>N+`6Wz7{=F9XTCQf~W1_ov`k%cKy3lJgH*x>y>aP|z zOP8`cCbK2)9aC1#EoMfVu$G<d(xDc0dC1*{(X^II&?j6 zAG2+h-8lbo_DA_)1@#ld7Qk+_?o{o2$NMZ$`^E-}d8> zY6HUK_ysk0yuU60KZDL+AlbIk#9l4KaUiI*f!%S`z<_Z|N=oAUI4{w^mn8}>EmEC;Uo<^L zwr`u8?{Bc3?Q6t4pJeG13r%80_w{Z{qX2=LPQesnX`H)ASUOa^#^H zX(f9fe(@q^VxNWLlVBRAHV}9t?zjjr$9BI*=~vH#90lwUspi@s8c^(}tmDCs@ECmG zv|!`)GVS$#7zTph^B$OYtDii%Yd6Cb$ASOA+fOJV0kr`5XJu_21};Mv`bqfBTTMwu zSbD0nUQ?{wV2D)MdgxC#pH)mpH5%OnVQ1WYlvAB^;3?(L1kX}8!R_s@blco4p$mt7 z%Pmi2R@PiAg@Ht$A#far!sl=AeDNT;3?^iDRlxD}TW8qj014I3FfvI zwlXqIkpiID&6L~|aXZQ9Z?lEfq&6u#8wKr8b5qmtH8=l-x*1njp)`5wuFwEuu``Xn zO80Vdy4!+kQNJHFmV%n3+pplmnEkkpBRPhf_L$}trG4i^L#nS%3V*o(5S&Go0$JOi zXL}3LoVs(D{Cb$>{>hUaJzZ+hXTHv847m}eCm^G|ui?tZ`N0&5D*~y#H09N2I@9CT z7QM#Ffr?(s7(}fBcbv`J=O4BRjVEjb42A#C1AGgkVwzI(y7)y#cAR*c^jOIhlB(Ka zycK)rPQjGi)at)DfbFC&KJ$?A$@Vfm5CjY#+FR(PDOOA}bqPW#;f0FiJ;npO;lozV z{=#3gspJ~%H%r4247CY~k&C;&l5d`@UW*F&1XCVYgK@y24sLvXR(|MUTWEL-QK(j! z$Nt;Bb8Ot^x`V090Terpi&B8H!U0?zaUc50h(BM%Oqh>?G}_>E0gl=1aGe|`D{;({ z58tiFmuGY+&|}n#2I0AYUvfl^puO(DQmiW=;7IP${KK*4kr8g#vK|t`kA%)=bcntP z?B~^RxSRj!6b^~Z`f~i5rw#G|so8A4Oi`xYic~wC7eN~p z-r_UzZUROusK%X*4^B`)=Mx6IAKycMK$Pp+lkCG{e`D=W*e32s(JxD+!~;-uqsp<{ zK_>QTQJxMRvPLCE&YBT)Slt7UYT5sVge43m5@o=ocek{(H1W|kurU1MM;swbWu;Id zWeH6E!FM<-WxZqMPf_P;WH}mmA*IHjZ9DCAh4-+f;_Zi_EAP%_mBi2tZFpwoWU~2k ziO){ISA>cko{iEa?d1zYbMBI?Q2*Q7$VhtBumIKur44k+kg`N-(ohMr6p~ng9M@rAg7i_V%Fz zFSG%>czJo(x|;y#T^$YC!O}+xIHHQt-;39#_}e@;(ucGUw}vEra#B>Je0a=Dz1sPm zpAfBz6phtZ?$!K@7jE!CuTxaRdqqiQPKE6agnpYqB*yhbMh)mdpQZJ^`0j8G-Q~5p zo6p!v-zNZ`;?Z?VGNs^$PBxf2!=K)TFfee6;Ygi;R78RCtcq@PEE!(Y9EdE#?NTjZ zYK=4(`3f#Rr}=ueixM%3+IQeo03)ZZ{=P}I(tB>lM=W2y|8wG-ip=io>GhG-dD2augeW;kt`}Oko=V>%jr*3$cCLstjgG4>KbBG}EI+s3S z7Wf>nCx)1dC;~npl-K)liwI z9`Av3B`dfmAu%X8Fq<|qx+(IRkJY6sL6fVy**Owhqy;zcX}c|^Rog3iP>Zw(IvqKL z)>Ic9zjmR-zp;9?Fw$f+Y2xx#qs!(IKK#plz{pSz-ObS~zt$Sn{rykNn?&( zpfFxOKF4Ydb_3M)-}+6|;3PFHzlj9ffnW3>h?<%E4&=<->F(zag!cFYgwwYVtYa2* z?#lg{z}sLgKeZMiHFch#{rjG^)LK9nyzSwE=oW?zm+>u6>0J*Q4R3AAk}QN48=FYkb%yQ;-hX9&kvlY(Pm~I za48E}r11#L`+i+L-nwk;X%kt7t9D-m0=e5;2S@v#o&7v><2A7;c9|j&JndV^av zV4j%A@He0e9ViELKpkkkkla&Wuk!x=`(;&uop>Q%wOXHlK@@qJwD`bD9MM!k+IvxQ zMgc){jf^Io{=(u&3|M+f+1?`HH2<2LYAjsiR1Xx1xDzK$k_$Z((=wt{zl7tuYsdC= zjy7{4vyH1@Z(1}F4C=6^W1>2KS&R0jJkSE^A7Qe`LE_2Er-X`gJonX^JzU)MMuO_B z_qhzpc5S{XiiVfJZsCWcBbB>(W2hus8BB7|R{MQA_fUCACXKYwk(9)RB{xP}G8m|2_(%q@=_T|BXqqGL3pc7s++>b$7)qGtG2+y_(?=emF*c>p@Z)$7X1%M&p|F$xqfEz~) z0RmxBfeH{a|NnM|5RD8TT-Jrjl3}$yOrP`Hrki*E_m$78qK5YVRoD+S;mVonh$|Fq zscDfeZ*m;xO?JO08132PMgwJ)sDpikvXq`88Xfp8(b(9-om@N3(3g8x0Y*65K66KS z04}hmq${v}>$rddXo@^dZ~CG(;ueb2ZFR-?SgP}WHv8e`WO~zz56)Rv7GF1ab4L}` z-pjZu#QLas&LF$T^hQEq-}?G`zD2z}s9-Eq>#x-Q9%W}?KhndVB5FkB3^~@)&JJ3G z0bh|Opc7;Y^2GhgPQM;;u7Km$1&dehr?pbPetN?M-W_5|AC~$49uOqc_6Y@Y-=;k1 z@zd}#18rM}u-ib`w<7(sSOeGHvJV!rlOG7~dR-Qn&2Y6zI3`)^QPtVrB={x!z}21u zukP8+O!0kYq(vqfW1b8~;K<4VmHw~vHT zGotT<2QtCA;o&IJ@5+T`?mJn|yIVYR4TR-4uuRZ+i~V1w&@!?7&=!SIeNdP?cke=v zBsjKHHrBXuCfFwrNx*+jaX=f@0QZ)DE!pKJ7@_pSTn&@A>a1^#2RP$Pd^iAd?m%H@?yN z|6a}w<`j={_Qs(Ng}YvlTAI2ji}Wq9E=&qKK82E3>J<0cTl*>?<%s}_bm?OAF-0yK z=wku#=0Y)Q(wx(GU0qR|xpH>NtxEd+URP@u+rjCX85(A0=AI1IGakadcmB3+#^V6u zlH-N6`OZZ}MjCg^w`hth9IZXHE8cbWgv(GJJr3Ush3okj_cSp7frxfkUcP+atOnju z6lnbO=>GShkbp@=+4+&co<{Rl<0xeU}LJm-DBMztF9B=^_u8 z#RB1MypytMsk!y0eCj>%)%_A5UEbCFkGF>ELWDmUz1-uevMXu6tL15jLG^XHgNH;n zPiWaPK?b^4T9lw{2Gz7-y}KPLFYm&_a9!@9ojjqxk44rKN<>>U2_r1=SCx`SkDjFt zUg6x0`M6k35H>yPxn!Ak?E2Y;e^?2G`Yf}WYTK8)o*!A+4F$U<`oF2dAd8mLsZ;FR z1o`0Fm5)RY`RV(od^XX$28sf24;|JZhM_dxswn&lgRX(WfmYu>eZI8qwvL9W)Qg9D zhpWD>k)L1BeA>2R%6F{2SVxz5D`~Yy$5X{PAmlD)(&fkjwDqXB)EFL;3Cx-YT%{UN zPB0`cs4G)(p+o1#*SNf_uhxJH1u^vp`{(xne~ByeeEs`^U>hj`tT&h+N|rvyNKa3H zLQ}J4c=*V3JIDpp#QVR5Bmn_C(jZajl-xovjX&>}zWDg;>`fr5Si$56rbW*a7DC@a z6`{crE8;$7n~f*~U6WgiRC&z~SS;BqqVRo%|t zkCgm@nq=_~q)Jex zOOO2Q83V%hqnCah05#I>;lNsJlmXv4goCrzy#eFpAQ&kVZv6>PfwCAr-sGQ7oAHG> z0YNcIhG2j#K5?HRzZ_jSCXqC!^-Oid;7v7wgD_5jCt}BT%4JSq*FZh*beuRoEU0B~ zdP*cv@tz8cRqkxiae$L$Z-hXkhKh3d-vJ9%R5$A2R?{m)b&ntW+7jol9Q=9Ojj4ae$K1XZ6681r%o~Jdz4IY|_F+DAR z3RSq~d6uR1bz~t0g7|ifCH?BbX`i;XHobnm@0{OS(x(3;Qivf(J?k2Zi!qwq#XM6E z1;$$TuZ#_Q4ov15W8Uaa- zI*|H@$kP0%;FZFw?Myc`sn4uE|L$)FQ~2rf2QsGf61mIqbrpLnffeUUK;2_TJ4x`< zy$Hw0MreK!v&HG?s$aQ$A0>$IR%BKe9Bxs4n{ua6OZP16{J4$XjUR4B*;>?Rp!&^} zHhfoO_W~w}X^Kviz;Jp&t=Ib}ZD+lt1dpDluuBLy=#<9!1{C1=6`VeDowvE;ECzTS zevgzkG^ru)ho+>kgB0(k;(rodKwN;8x zGVQ`)ePVdXZ)&|V$;XkVM2fgD-gv?KQ{&=O2leT8#jh?2h;?fxV|>q_zo#|z z-_*YYKH1dn9x|B6d8Wb_?~4SQKL%uw1Ja+KAKfaxEZmgyx982;8>nByOD-@-Oo{Bjik7TJN~o9H0IR6*Uc2&nwBbyiQnHi<3Rb7N z@rLD0QlItyD1>n1=O!8u0@3BmmxZqPsc{`Q<7{hhZ)t10zvs-C66Antoy8VDIR{(; zSfwgyZec;J#(2@nl|jwgnaRwP%|eY>&K-4px%R{}S%8D-Z-av6U_+#PB;o+V7ne7@ zv~BL(!Dk0KJ$W%?o#GZI6w@Igppj|3>#Fen7fS%m=y>q{#*-&c3fnC5^wNVCOo~RoA}S|@Y&DV@G6?&McvOzj(c+0h>gvD@ ze5g3u*XZYSa|Ou=))`*AAUlXgk2v!p_ax$gjCC&;6*ct)K!2h2_4S*lr>3WqXZ&FX zBdg>A(~|g5g~NwiVK7r=YnDd;H!HLBr36$}MMX!qA!9BObl6_wf#KGw#W=9N^3OIX zBPcG&hGtc05SZ&%@rd0^Wk0NIK#>dBmcXRm0bqU#2+GmFk|3VQyE1BHK1jXbE}y?R zZ61>2_=`t2Bn?DM?N{hCLv372&L_4+(%N?mvB=r<@x5}_Tlg?3Ph#+d=8``%7=I}< zp40i~>@$V25SASs^AkTM&(!1>s_omi?`Bj~D<~zY3l=ch-0XQICoQ8Wy3;7xy(WJ6&L)$G>$purh#xD{$n9`S&2c zc{>D&dq#_MaP#tZ1@pz&{dfL{ePz{BMTeZQe7Uh>4OPp^pmJ-Ax0G`~=e@eT{Rnl` zi-Uum<&9MxFY*{T*w(w_2(~Uk=a-v&U_TlkYXW9g;K`>a;7#)lcAKKA z%7PjZ8931L!Pzt*@A*uVmHR4xJ92e-xyvPNH%7JY@WTPM zK)`gHfo}z0q`!I4N$Xz@%UY#IWX({G64i+QU#8&^9ujnX|CH_!(2KxV0AhJ%X&e1!Hrm#PPT8AMYxk(9ovG^U4vi3;~RPvOW~f+&DnzPvOUiMHM34geY5(fr{_AboJ_1 zpFJjo0N4SHKKH@|AgGG5o5%Tj=kw=GKn^ovde;F%aUy>V0QCj0&jX#`zuz`#)*X$D zlBq*p;k2ls$c4WcCzih8gfVRe3z z2-(O}mSV$e4=-<7Pzr(D9?sa(moJ?z*6;xC$UXc|w}#L)VM90 zqg*EC4_kt=Ty8R)@V}H}z)1-edcI@)<8=!LL%U%p6Jzhjg-aN|0JbWDUx-fRaG^j_ zqPdl=^AlEvU!yrYw%D->4Cc$`r?R533RfrGtfsh`cKHc$A=OSC4rjni`x6kl!rT<& zuH@J*pb|Qdq=%?t`Y_r1=+ch-TARqa9A5@grso?MoPL0W9Tr=b`9G0dh>MHMR+Ep} z4;*F_VjAY+GB6QT7v{xp3QQcPvkW)1;z7q#(jq}yDU>Q6@j4u~AFj=&RPTR;E0i3m zbD;rEh398q)bg&=Il;l|86_*200l7nacj&K-;^kOe9v9QXZ_lN)0HRuj^WC6dS3#6 z{>#&(qDeDvAixjw z@kFZV$;R%SH*PvQk+ShJ@rd2ed7x=4zl$j4d+p0at)+!6T*b2!&G=z3Xz|_rS(({> zD2?5OYtQ`--@hwdn?N$Wps%6?yn;&Ml$ps0>IG76iaCNP4XE{;1c8is=Cu{h@xnhP z-raprleY9;Z^okyy1E{P3G>&ggleHKgZfy5gF%f`~pv#7$Le=Jp#L}SHzVzuz-%Gh!ofe=45092wI zvXj|WAikM;RR6g=tZF-+FUmMJjvPvrN|GENZwe-+N_|mvosAvRG&AEw@;_@Nbl~;m zmx-@0^q7({U{6fsS>)e!UzrY<2Xx#KTwkT zhZX_tLDsnwX>+sGk2ubJ4Q=U$)jobw zNfuQbX17sa81>iBgE@sp_;D>W^igG!ML%ecNkgr8TCE@7!DkoIK>l1gyRibwgJUek z@h(y`$BSXTHn9&EWGel5Nv$+n(00tU(RaiAT}@}T1ciJ>9LgAdC_Y&ahPu6XZW)i> zm9vgUQFc~i)Ww7GWbH^^Za5P{ju@%_E*YN`Aj7V#LYT?llCEQ*lg0~47cBI{>iSC^ z`|PQ|M~`obI-bKej&%b>1Qm!rHLq!jS#IO>Vd&SB{=*gn?Dl{RETP(?C!5vp{BTq&7o3{YqDXa&cIbjqj5lVUToq{0z+?3PLqPRecy}za| z=2$aZJo)14><`|alH)3^7yT)vV=X| zIYeuWF(xn*hD8Opx9h27dAO#hrwiBgfI4lwKLd2P#|u|+tbBA@cpI%k$8J+PR#ipN z!=R+4%YIgUj_Xn-5b@2T_V)ILuhOU@Na)1jJa0#YcRXi)Iqd4P@@=$uy!-K=TVp?s zQzo4R>kAVemzA^L62b}XBUs_YzjUml@5Q;{6g7K+hxyXDTr=nrqS24$Qo7e{KByj~ zhTe?N>V+Br7^ctLbx(ia0A@r^kS@A7Zp({cD0<+4&W)T&R!+{sQ4#($;D+aG^_)M@ zzBLiXYrKXj7{z?05<0|ZH<`0qdtV>5l08w8I%{4`JyoU48N9?)LA?0c|;Ci4xc|Hgz_ zQo!)z*t~N8S@Zi3{&E3euy`bsvA^?%Q~$rriW~`S1s$*-t==S&x8_HrX!xash&K`@ zg;*2qg)jvW)b&C%rl}GtgSDilN^aRn4ey1LGcX8+TY@FU;Bjwo!`jsj=Qs(vo9@Gt z3Gd7xhUL8Wo*NKj6&Qrd3RR0^KS6mMeI%}&;7)sDoxh2x;2LJWkB1xmIo2`Gcq49Z zACtWZ>%-i8F;vG$%`sGpm_%l?OySiW(A#l_PyGE3GtmijrakglES;8|e>rJ>@ww?}z2k%$ zD>+^qTgg)(PKf0X%+gdpQ< z<8ZboJxczp9a^QDuzn56qX6v^bBjOozbQ1=W~y0A_}W=5$o~NRqf9vP-*lRtXqb46 zKe0-j4lC9SIxVZObQSN*GtmRrE;(LSxsu0x4lRyF9W06y6zksPpV z5sz$z@`ptS8s4=maSLUSmT{#rD1r{H(xbsQAz?fr_Vt7ifB)*%-Rf`EZ$5TvT*}FG;$P$0* zq#92@LH8&o*)kqB&kj9Q?96og*spQpc#XqY=>n@Nr?S^lO256inZw4$Mu9>6_JfeT z#-Bg7e(LDH=Klc(C4}q3?TO=YA(j9{<~FPJm+CI)f~t-dnF76X;^9Kuc!9)=SR`F{ zTZq?QdwdGqEeT1P7st@#xWnAX{LQIPp=6s|T8K8<>q)8@7EBTegO#%+?*Lic(JBeL zypFKX{r7amdEop>=ADmgXlOtU;kpGj_8bLje90?tUm$YDf4Ia{un#lp98LeO(L!hH zEqWo&GLfne(_mknrOs5g^1mxEAb&+B+7WIjEkwC_p-C+ zQ<^Up7fnkAoVAPq+rs7RuVs*5m)>ZI2Pp!(nO9G zP0efoe4oO6t;dcDWY#@Ni~qOkL*YIed!cphO**6Y|F6J9a5T{0u)f{DC(?qV=`Bzw zU?|r&`bJPBYtq)?mbQY!7?ardm(!Amgr)LYQw^1N>RcP4t(m@jL-oKI(rxmcquAgx z9>U_aB?|I$wABl#@n;Uv?XjqH&czzt=8L2<#{{f`iO!L#C@Kw-U9|De_<+!=pj6pQ z5vUa$F`cGp9anyW5<_U61;Tvi+FSJfxBxt`ZBU;v!or0H;Z&D+xcjBFn+a%aeAcZV z{h))(V?s9*5)%_2gd2CpeU-QA8_ynrdTm z;f$iB_>u-DXI>)3>yJ#n!2a#V7$9oZN{-=wJ=j0lAe}k(=$!M1%b@g`3&;GwPhVp% zD6fta^@sd~*z0!&88kI-X9vU=#PKg+gl0k#)-C9p?%YgWs(k2^|0=aC$ItM7lO0k# z6d3)CY)Udex8P#o3VU5gEq) zlzKz)olt`gvc~d^Alui(-^N)`Xf`XMuf^5jargy%r+02M7$%ag^yXZEOjq zbMc>aN(Jxy45r|(8^vS^+s#g%y;pU*n`iL&{m&7^LCGmnR3=r`Q=@f}Y$-ZR{Hy%W z{KSl>*xrVaiEDOty!E6Y^BiZ@qo)`?0{L-n8$Tt^AuB>Ba_M^J&6ucytp7M zX@2a2NVfvQUB{52XQ#6b7dw_WX|3mb{XapHD4>ALCx*ZQxWv9@a-AjB z-`27E|5gZ2^ju80jFS>DSyeyHXqCTdU46L4nmAXhJjJze;?$&i$!L6kKkt>5*Q5vu zoygs{e7uKwVZ0woP3nq<1h0iB+|Yc}OpQ!IpnB%@3B?Roz?$_}C%b!kbg1Jii{9Y^ ztf8eYyzv&Sg`Lfw;P-%u=Fz%PZH%M9afZRNDz5IUII?=PhU{dpb={N1cd>o(A-69^ z6H2ty#L$ItJBaQEb56Gs*Dnr!Ey08c9T%gMh3UVJop^gJ3fqcMCyDGjg<5!8ouRHy zQ@Q}Vp(?SlfYho)dK9Y5mQ~bl5Ze7@X2-6KJmaSL8gC|(pI*b9q@+7l0SV;0TseH| zX1DRQcEE!uB$)UMs_0-!`bqr>Ar+I#iQ)w3k(OB-V7} zb?>=?JmU_%p(A)KUi>URUwfz>PhFk1}p8& zl-2eAm*x{o1GeKOpRjZ!XdF0h{%HLO=Rcef0>Y{;AOB&7`k%J*!u+|DtC81tYBgr$ z+t|jbqj`M3X3IPDFYBuP?g3Km7D_BupCuu5OJ}y&WF2&LY}anId#sl7qeCQtsE}hM z*1N{&c>Y(>0NTCMhWIBTPuGb{Q7dG@$EpS4As+Kfc0j~U@+P^Zgal%?(fjwOGAGBm zpCR~(`ruO3gZP%chCY~uclwaf&%mgkVPm({Lu51PtD(=pEG^u8mT#DYC?|%yr(r!ql zK)gRqydOWFlD1Rn93d%BM!mP1$_8ThRM{}4O3Z@(P!S%drFMp}A^wDaLe=(*@K@A> zola<}Js;T4`5IC)IPXViC44fP7CDYBkj0DzqfaCl82PIQ5&O0D3ish>#e8rb_ytof znPRXLqHa7nW!`yMfLMAF_e%0kaFt`i9)f%K@w(SSDoS@gfzq5v#y4vcvhmGA=%f9)*gI9Ygmp{V>jj5a$77Q% z5d}*&uWgCQ8*8gjsy=bk(qhV!=Z*XLv*nc*(PXjN*t)JFKGrmhtMHo?^K+F^R1~?D zM1*XpA+h06KoDN(V5~q~JqfB>KT?g;VohypEQ_$$_ zJi^lF69|L6_ppx8h&M!w?|2X`*APG7jJo&&?#A8iP~+-2Efus)oV(ehLl|6=X(|~& z%ECR7#!})sSW%zvXxDgmhnS)x#&^#WCUCt8RM$z|ca6j1y9lWY4!s_n&S!mAXR{0W zF42T&xr`nG`H;b5N`Tf)nT~3YR|964qU=^!D0Le_!#%&(DKOA?oRiNbeE=Bn@DXO7 zj6YLqpu&X(tVZ2-?j|(X9uM9%bWp4}h|*h>Pho*H@OZfV-~1Ks+7A0sU2q{zTD*W#)YU9zaOiq7Y*8IkOByIBx;Iq|=j) zno#f??-w&Q{r}i{3$UuQ?SEKON>T~w?vn11?(UH8?ha8vx;s=F>25?irBk|7N$GbV z=g!=@zyCYW{HQ4hh%;Gqek!sO;x}46>oF@_yFV!J$G% zQN~N~-=`8{hq!O`4oJ<4RZ2kO4mAIxNSM~)1ym9?tX1W|G1lC`WaLbgj-Vf!!IgX@ z2rU(`rH(33iP0|VvUT(MEdtoZf~6l}6NA4QSt_B*m)_}$gxf9G-gUcpKc@)}tc8PU z0U(X4j)|A2=W`>6r9|i+@>qRosgl=#x`{KUAnU{od{^)m% z$V;myC8)ts8Y*&Y>uxu_w1T^O1DBt>$ZEO1_TK!l)-ypyX2$lc;I$@9p-fOKXeNOT zDbn;yIi2a--@jucpkKb|A0Ae-8vsriC3J{;3$Y~{6YmukMr14Zf$2xc$B!R%dVxd6 zIB@uodb0<-UuNs_o!UWT%8CefWZ1hPsN z&^opMUPnGkMYs5GMrCIpQDlEcz>N+@@_~u3Aesbra+3RLN)j5g{eP&~;CGRz0qaOY zU17nEQVT8#;t~_6LVq%733xy5*ydixEL61DIhXb-;VpjtC2&AkkkDgq8{Zo8cd|+ZQ?{EXanF(|KOl#jrqt= z0K$9%p!vR!ZD$}9WcCRhl^C-0@Tq}j;ko{ssUqbP@eF?0A3%mO-7Fvc)$192{0I9{ z0n50+ckke+6tllP?DYTl{n9+S0~-lqHIzY^yyxeE{E4N6FkO?(K{uH8j>rW{LikF5 zwS#6-t4faCs0PSAL4a_Jf+7xd43=B!c#LiIpOaGf1~YaDl`!9vqt$vw06&KQEN=D- zNA;w{ykN}5)})US?__Bwp*d`3)XMh3mT2a4ROJ}$G zbdNP^^8VE$NYv|7#j5*+D(GaSq$27HHvsb>Mbn2Q-WysL<8FlM5_ugi88?ci9onyX ztw5`P-uP%!R%E8NVN#dk=4ril(qBEBT8nB~ODAl$kg8zB1{@Oy$n+VX*(Clnb zM1a%ypU6)%0#;FgR_rd6=}N-#ye^YaefqP;^6`G@h>1?EejBCTa+lNZoLrUuEUg!E z)*z=-J=S{HaB|Ro3)XuH=c+19M^>g?1Alru1HpCpimP9N{~Xu4;G~etz#~phGE2I4 zgBXB4JUoPb_z?W|?c31`O}YwZgH~M07$R{`&&HC(TIVepfG6zUEi^tsQd%8=1>ac! zEd}s3^%qF_#_=Jn5WX4&%b8Gt0>nsl35cb`|E&x=AxoOTN(u%rJ4lNB3ru4H5Uv~B zHF$05X-{JQ%U(K{5{644LZF#;E6by!t}D@jvuiEwT$-aXjN%I);3mNI@0iYL741^4 ziT~CCl}~41r1G(qDBQXD`FDjN(4lMqP_r8palgQc{0#K$3q0e{qjWqxctA_778C<` z1Y{sN+R=WD9nIpyab)(*pn`*_>iw@QsBz@8;fMgsgGL&HTfYHfq*$Mw zXRSZ94YW9uJ!Vd$f5J)$9*!cCLOp)-Q4v#S5S3=DP7OYOa8*3FLUwR)F!%$IObOue?Bf`orMM(pxF7_6(Ghz*<#<%LkQ+rQX&p^Y4VXK=JcUM*Y)L z$}J9vo66py6Vu=42)x$kH0D8-l`NiKUd5csz$Bul)dM1grYRBzmwaq$Y8cQBV!L9s z|0BUBd=Am6mVDqg8%)fcH*9i-11oO`1t0HR0A#YiS|Z~YG7#0p4%APV{YM(*O+iF>~$Aey#P$*Ke2dwwi zaTL&vVFN0HO2K&?t7U36I({r?3_APk zk`us;{aFzb2+HLk404I0iodc>o+QrGl>a73n%n@0LcX%LR!msCT{g2bEj7MNFyeB2#g7Rf_Eoj+XP#=6;`*A@Q%-K8b}6^aPw6B0Q(ky+t@#Vn z?)tYhVy(qJ!Y7tAmIRcij$Jly_B5&FQow?83L-uSLTHF67btFJ)>$Z<6^mJ_z_=^Z zM5?Y4P-aqgS({w`LXm*cm+QfdygF6liVbAGzK}oo4F^&?WUHk^sJ{OkXV5r8jD!>B z!IChrd|@gG&*cM%08WJd(4yH2LyK5td!1^CUk@RT5SNE;Mu4cy6^x8JA)V!7H4ib8 zrJOKy6l6~bSWuBL$tz)!;1vw>^y$;b9fJ{IZq5Qeed;-UuKygu#MVIA*t}OpATLe5 z#sQ$c#$QzhWq^Vw?7!hQOErjbMxAYf2Erda1&NGAh?$}vxZVhZT_KQ7P>?M!5K>b$ z&&pp=E$Ea)X10b>HoylTt8@qca)^GuEg`n>?ITbFY{d%Xz00U|PM{Ng_L7~3BjNnq z6&D{rW%n0^tguivpjBi3?B?pkaCf7rsfiZHi9UiH5V%GqmxI;R;!|qvT3pXKmyT># z4=gJT1W{k8eeVY@o{fnO{$U~|%=hP4h?%G{-MadVo#TVhoZIF_Z;al5&`74y#2#;? zLVB(w+!Z;2dIoOQ*MN~U_4IQyyNasn^ob4-3!%yTtapb)1Q<+=?_BL|flB zpN6Ul#bP4>b0`VGzf&qF5PG0vpyFYk&c|QR{CEfP|Me5cG`F>hmY|Gb&0f>sT?P=Ab^$Lt2&E1V z`TfAfUiw5|oebqJFnTuuwVCDkC%8Pjg7PRJafSGexx2et6GCYD9X6CyRHs4c`h01b z%#hqFfF9e?AZykIuHPcMN)=M|U}?yKJYW5^iuq5)^91f(S6HY1KBw*WNlj_VGJHEC z?1YpkruFX6P$jrxl{{pcwks;ad|ly^=OXB_s4~oSWZ39=QKk6|E}zWrWVX!u&$a;byZPGn5A>r;Vqzz6gWlvyquc=$ zel#Kxuq1p8Iw{1wH=?*Ez>=O6*%5TQ0Wl4U+^WFW2@2Twa4=tmSeTkFvzu~ z1dbawR6tgr1eJV_Am}|@&&MST4e$3gC-Y-w^*!|65$c40B+t1o+D!tl>C}}hxNZ?i zA}>?#oh%PbuH2bkuv^P{UY$w4Fx%UjcP}sDls4e^H{t^D#oR4Kf|qk9lX-ZW`flnA zV0q+O6~Sbc&AX}^u+WEEK_w`$SMH9Yjl);%CA+WbOLk+mm;A=A-ag&Ct_Kf%X5P#A z7fvsC&+g`n&Tk~iPdh?=`8lI-xkw`t5t4bw7HH*f?#`xah6sucuk5XYmR$-cZ!e_j znOGcjT37U})&$$CS82g}HHA;~WtZeR8)mDSTo8FmI2G5Dkd>Wit7}&`9q*-0f{Oqj z9Q1|+*U|ySQS7vRy+{F}^>V?c5WL6=Yqr<1Q_JOVY76I4zz~?-;-DH~5m0M0{4(cQ z|NchE_v1wc&6{U13S4g{SS6`zqq>EB#{(_lr& zKb+ndSZ%8p|B{a0g!BTC3S~77yv$w-`TO#&hmlXf;plyD#p*Hm^UPpj2(zIg@?mKJ zhfN`d|5QrHJk88G=NNp~dE1E}^9avQrWIg_O!lb9uuYOhFvMs3-|TU2UAL^<+j|3x zz3N5rugu`J5Y(vkM*l9T`}LXlo+iuZxapc*=zdRelG*=P#G!_G?iOLkqJ2w#hJCYD zI{Vwz!#R_fkv`{W;3P5Yy98Rqt$6nk-JoQ)pL?4FR&#T|v@_3dzGLhqp}jAd6?~bz z{F0NdjqhgTPP<{A&muHGJV9_Me?vp-&2okRw)D!bkb_PhqG2#a& zdF2+&BhEmDyMys(dqa8R3Y_ z=d01b+jRhE0QRq=z&`jyBp&V)9%E|$Qct`rQ`*DBRmbdwA?Ick*K%sv2>2==qg4>j z{ZMAH0#nOItlH9Py7oH(S8cHd_>TZuu7H2>Cy+#~lTILtmqNrTi-Upseco=T1wF801KI#81l6JE~Y0*LkZ#oxNtlj z4OTjk8<8Lna1TsP3#fbu3+BFt* zFds9|uuDG2c{nc=%0H@KEZ06N>PYb4%Q-;<)}_uPeSpsE{K`=I@@o~e-M3Dm72M2a zLLOd$`^(t#?p%*adSX<07q_E$^BlVw_TvO;s~LrwygF=MnzE|hV5_no9GOz)Q@4-v zFJkz!-bZD`zK6OCSwNUv@>p;w@vpA@cvd)@+}4M{@yLib0vb4-Fpno6hwO z0+-=9oOy=&I*+3J<92VbZ7?3r?9sf9AGkSBb7WzqbwZlm9PR`=Fbm@FkmbqHb!g)d z%6R~3xB#q9&1*Ssre_2m8yv)?b?%k13g^FfgdTZP+G~rLY_6+^m~6^a>wh;AR*f(o zyAnVWK>;#McEsSuVdXc}y_kTzjoF1<-$hWUW@qsNfsR34luQP#;x~0G&;ojiq_tqC zAU`sZ_&T?HZKqKq8;6^N`BK zhraB;Y6h&j@}#ZuAI|clo-$;+>ziE{^U_2;x~kl)Zd5fzF2%<5<&l2w%e#_{kv@rf znXrt1@8s<pp*A)Y(c7UVOw=ak*M*y#+B&nrXQK*M|1rI5m;O!ek{ z^D1E@8$nB$9RsX;$P=DExQM=Pi4X}$$6`>+u!X^xOgDZ82pf6&9zQskVLgD>;TVZF z%0&6Js*h+0ITD+lkv+v{qH7Oa3ki@1*X)N-pLo#(QbK`NU|o>7BJ#J6>k)^EU}UD$ z9%Y7)jI|n{iX(bt#;uS&5K*xK!sUrgpv^4=Lh_VU=s%@_MU}c!>UYW6ap-fy$D3Jr zNs7_h>0b<|_SFMa*Udvq_K*eiL3b##sAK%m*w4c2(-*h5>*+85@x7BB`KW zA;b*~9xpVnQOzzNX=&nhB37|asw>1i2pCt6GN;jGl`7d&x9@Ggh*rC~bCk zRP>OJ&3M`U?wCVQzF)+xI*7K7gcW z@$}gz+LO=(ML`|`P&MpLlnAumqUS_kfXkI z3mmo<3G+4XdD9_w>3 zK2NU3eV$kaKj*G}o@n_js1R{+l!bl7r>WEGa8m2p(bRskmyo>6rI{{(sZ=oFw!8cE zmoK}=F9Ob-pIPmn*ML}m#3jruMW3^y?*nfA=L_<`;#io4#f62$-ZHzs_oa=ghqep% z*@r)|%*tf`!64o9wySs4xiHH;o%V(x3+1nS?oTx5(xPZB{a##oNFHJzmZ~f=`r2nK z7i8;!On|KnNvKA-rVsRW6BsfGUs_KG_rTy`Ou+>E731^SJ|y}tu|wU7J>92GD72sG zkvp-UG6d?>*3AtC1-+4!CLlrLJ*u6Y(9pF@!coQ#-q1oXdrq`4YW%7&ay?Ud$e7is zKwK-L8rwNMKhQHl6{`;=VhH<_(dQ`6L3>o}NW_|wub&t~kx<34>lo*nFK-G$cZq)#qLRyWBAD5>ii1JCI3v-ln`p^VGoFML$4? z;c=%76Bcmri@9v43#M70TOYO;+M(#@Isg9p%$VEpIR!l3m_q>hFzNQzPXpn~R!G74 zsu)%UJ1^X>2^K?qZRR%ACGlzqTh}&DT>gR;ET3M&IUzW4?8?V6@Fe{KRxY?9!$cqW z^taz0XRJ(iSBFUFe<5%x_uy7e-Qa1s68gnN%|LmrN6+ts==$tjoMDFG?e`BbYG&r_ z*hts2PvnGJZzCEtAH!SqRGC78E(bcdQPm^`+=l8`1zSmnCk4x0oyo+wT%I3tYy})PwS(FD`r5*%F{kW5ei2%QV41>ML zjhgx;qo~0{OK)>jxDsZ+OV`oO(4pn1o@cH?=)$NfE+NDxfh9HpU1S1MCK4tRjIAGY zF4`uXs0P&hv>Xp?tR&USq+o{)@_O9yHDXooTqY~-n61`2)ZXP$$5vn@z~XjVP;!LRNi8F+g zcWYS-#gD$@82?x&ic#&`tX*r9wcNZHQ``EnF7R2gTS=yO`sqWW4|$`rTrexN5m^cJ zsB(yc678!~g;Rk*?!e9uxRKxIS$Q-GeGaR%UE!AFLd-`ud?a%HUj5wFJ=t`&PZBbr zdl;LxT+bVucxuvHfF?wgapQK9ynQrJJ~CV5lkTC=;j_8&Rw7bPm#M=wy+bpmR=6U& z*k)%NIdqwTm#j_DR_rCWt*Q5Bna2b*XiGlj6Wg_F=oEQWoDYCr=5v1gb5eM7J$MQ} zM^_GS_^x?GSVhihu>>EVQ>_(V>HTw&TgIULKDLiuZ+NvBkNnbw_nC-_PwR*A z0&~_kq7@P?fn`rT{`uPfUiyupQ-;E6$#*nOM!&?&-OQ{|R>l}|8{ip!YaKrRIPur? z@Ki_x7i-79jCF0V*GB4B;WCuxnz_|av<|@5O#vtXqWQ7Es~m$aJhffIVbIg@{o;Cu z$Kz~@O{9Ie876%GnE}-TY%YsP!A3%YRCam_L5^|wZ~eHLw1vyXXus_`ga|?tu5ty< zN>gVg5>i@gGibaY4K9JxiI&WaVt~h@(w5^WgpK%$tt=qa^ukGD!n})OY+A-qpUjd> zhAg+5WRz8GF|!YMHGN8LH$xCp8`I;3R%;Iycm0Frr{w>MOeL}utqIW*XMI@Ky3Vdr z%tV4+Izr|lR)qL4MWFM~5MmCdgr*#GdTe-hMGTEY*+VBAh6$$+ia_m7ZNuM#cwvtI zlk!|6WZAvBrfF9>)y_$M%(saifgb5s;T|DZDR5iRW>Y{&oW&3eZER+DT0SMYULu2z za=rWnh%mjePn;RZ(5(?*uXdP=D~bUWI`%Owg;{;6>AI_MoTs+L|>ps>|Z-R$VeVwT5p zQ!?rE#d;HXK>{`=ghar~2?_YS6A z#6<=@<9LN)y2t48$o`soj8Mk=DEt)v+ua(g5pf*3P=BB4+U_O4Nq&KL*Jl31WIokG zZ54Or{Ryvn=72FyoBfF<7i_?|YIE72X>wswob`*I?{IxTc&t=o!ByaBXgytg8bzn5 zD=fvav%-L(%#i3+)HQu+gj0NN`Xx;Tw>IOAhY!+#wq?WA) z=SXnHE8a}v^O5zS9<>}B-Jmg^Fs^dToL2=vCqh*iw1}tP+NzG2mF`h;Yj7QC2d<5kRkLs(8 z4fGpMSYrz03)rrZuAU$Lwf^=idD>#SRZt!!zQgRyjD);54NuDdAWWmSvh&whL5YvU)1$R!&WAQ3CpYDL+or>sj%|K2e`w+wR zZ+(j#Mz@@M*{TV>AI+Q9PA*rw**|h?oyHkz)9FVRYkaVc;B-QvRz?IeC`3IyQWIR& z(AB&~eR4e8BiztO|68A4b_X^EZ8p8o)MF)d>c;STn4P=&{ofF$nxv|Oeo<#`TDRN% zzcGH42^H}aS{s{Q2L{@_dSW`NUf47ay3y0|OvzewAl&NNC>6F0-H_35A}JJgZB!GT z9Ij{_l&QfdK?ELzBF@LWM})!4S4%F6?ph?R#jrRi>o1|jkZXZ-%)$L(TnIS|_&yCp zlr=0l%QDHEK&43)xAVom8R@J;hWOATvX!v1NU|dUF$I|W2uPs zg>pu0N&d(5glSvW1?UPCD`U=xx+Tuc*-y19uW#0Wmy2UCa@37n5N0pjrbb>uCdIer zouZW%>>`EYz9+C`2H);?o`E3}A{sf+HJBcPzF2ZjF(vQU zKdB9dd0%0Yy7+eqj5GJWUcx%IE(|!R*n~N8zr%z&$hg{obg91jt>odEdTe^0eKt6( zlT*T#n@i=NPh`O4oQ1`9>G%15=(mYSL4st1>&~(7=ci9!tIE{mZEff9F>*cfTU}r8 zC=4;LUbSj>su6DpW+jfc-maZz^kqvQ+^>9^ciW3(|rjmc)_v7_) zzB#WM*h?4_91_B!mRA5jWU#3Gl6!CO_wqJ9KU2%eElB)icfRWeKSJ1VN>ANPgs#~k zQwt_TF00c`hDK`Z^g7i-_CDJh^I6KBU)p6GH-fQV3!6&)HA1}3FXQfyi^))5 zbuRkn8A7dF?V4h0{sRs`hGpQ%ch$sWVAm9Kgkj0?ni>m%KpudW3vw2(ZecXn@=Z&w zoLkF$)s|F66U)QAEBi$Waf{rv7^?#78L(pIY{@#Tn>CoM)^_^xCRYTW;R_QO{mi#@ z2Aldw(k2uzM4qxd`?_8^HrQ1uZV^mzrGxQP?Z5)VvdJ(!n;YxJ^OTTk;o&N&-WPER zaPVW#BQi5*xoxz%XO;_?uzlRoC7oVCw7v%+{G(&V;WT?6Lx~0>O3i{Y9K&~u7@Yau zS3C67y&b~(h}rPWjG_>ZzXna<{QIGVk&}U85+Yfr>07(CS!iz7 zk~EthTYO=S??`flvpw4}m!kNE2qWH3Os)F4fLBOuOax7|;#DV(PyZv5ppPdjLpb6M zae4E82}9)ik_e4>Dt)Zt=BAI0(-YewLINZnHrhpvLHFb%gii>`46rmw-*FpB^now7 zwp1PPo8OUddS!)Wge6jbX~}j)jw;z>C+*XO*(~wUo3-qp^hvjio!^JcOB0QNyHeWN z_C#HKdierYIf>G!&|QE1EG;4Eqe)*r&dTF%X%5;X@`+!n!L6m|jl`qZq)?3QT+->v z+Z9=lj#KlOobGxz7pSVd1$9&rAqtP(;8BT_mhpIaQ@;2(oTnZxo&O36 z*X|ap?M@CyR^{r-XL8AF7ii9h^BzgVPAm1$@hs1_0!>79k##$dk0}(^uZdw)QMs{z zPtqquqb?BGF`%=0p>lCtT_1DE(9mB(l9!p=*q#$(wn))w2?{QHM zj1-Hg>VzJvuU+{&I{9b1syGph?at*|CQ%#(mIW4yw0I_QX}jT_p@HKr-)h8t_yZUg zg5P-b6)pu@zh)4YlD_ z0e>p8$;_ZbiqjsPYi+)=RehC_VuduGol;kI>p?Gp&-hp)G*x@^aun)=-@O`~zMt`5 zMZ>HUDtvCg!ZgEZ@uv13bEG*HNY)D1N`c8fDI@rtjx4G}MxGz|5>v{?gh^F>Y`o;u zccDO0wMAlCamw|3D_1yzkiEm?+Hw6CmCSfsU=aKS;_EW+Jqhd#u4)7aT6 zm2;UU@)dpviD600gx5~qTi-tJjTn(nE|N>+gF);_wA|p3+rbhKa?0JUEKcFr1iy>B z;@Q<<+|AW-+T)SMGSQF8CBu6kqQ~QLJ0#7bz3ILg?g~#B9Ay_qX_b)s{|y}qQJSbS zhB}@o&v`JtQN0Kum9lQL!H~2!f&buE(?j)j0|I;gfdT$H=P4+TbFp~KT8ul}&OK== zoOfW}nMx>`4imwvCvBSbHr|2i6?WLe-Hy=C;jf!Y$Etp^2w!;wv!FgvMcOT+;JjK4 z0_T2GuWgRNBRt4)EuK_-^?K3T!4<{um zytNSK#xU$G?|E3&SDhPX@=_V|3LzNZ9yvq`J0xNw1d~&yo7>$C5l2BpW?gRNU2*YI z&O+UHcQaz{U(VhnI%Mz9ZAsfFIFTEdycx@#U3hN~Yt9}8e3}FtlfHXI3}Q>)Md3?2 z6{hOo7hE>&IGBZJ7u0%L_eG8Ypcr!4baJ3hlur1f-#+L+TYfi~=R9B|N3)cO?epj@ zR0ro9bM+$wp_mn&4g;^@eTI$hIol`(O6?06nVvSb{f+ZcDWT729XY-SS?u2Hg;qcN zH^;Kh-Y6|B8oG`D#^qT7u07u=8zpT0}a`KxxFsW&u%~tej3O)R2({(pd zks0l{bv;SHaaZLnFY}(gJNv!NDBbB{C#rB3@vrApO9VsEOvy(xJ#w!uSJF;yw>1yY zSV&SJ_}8O^d_yfVJOgj0Q!ieW0!Ri*>%{Y6Skwx(Go%cg^w*|egTIA|m0bYoR*X};vByn}!( zF`lFm>7STe&RBx;>!3UY>kZfcsome(c*vvrK|1_{j6+g1HcMwvvM%qHGgJ9C^M$Oe zCXpblG$RS0`?LRk&7T0{g@&ma6ZGNTN>q(Mm6?1ApfH=8`;&m2A;vFtGddi^4`6&I zE%f^<+60l$VH^b1ZNBH>O$v{!7MFRUz7CO>M&Mq^zM&CK()bqJg4} zajY7bz@J`Po+HJzM1|1{N+eNkc9bCY0L=M8RI^w4i7Cn@J)#?3$jq5I>h}!wbH-80 zZ;LnQ1ftkJ^+Z1zZOhDQY;^kg-6PszP zq#XPyb-aJw$4tbIn^b#R4m927QEne6Bp=RFGxA*NU9ZTVA0)_jMd1pOLZ}IkRK_=_ zSKW6G9g^}i9n=NN*N<4LDWf%JMk)Z;p7^auoj=(y$70vEU%21-Hy2=iqD(YZTnY6H zffQr3M(+!qtq`a08YY1)7hpwZCSm*H;7G+ZFV<63_=UirnP!Vd7Ode2LmFxV1o(Zn zT+y%hS1UOiK7&jynACD5@6joGaFPDcGck7rfD?jaSbPBo1XHeEurA$Vuz(WA!>kMB z-i1u$x3a;Ot8@=_drNTPDH8ks6nW^7+Xr~6&4+l$ZgCk6^w&p#Y2a2krMX1}Dds9T zHo&VJo{wi(N{S{ySJJQs6?$p@K$RWjJd#4T7D@r8!sr$bzySUy_X4274sZ3#c`qOh zJd2hwy!Qt{-d#hJkL!?eK7-bvalW9{jJAr7&98T=F+AoK3$d(NLeih9eB+M;F3Qf& z0`Axt1r$5jExY?`7fa|1erV{EGLAcxTg@OwY+;WAro-}iO2-`~br38s6-@aL`2F8*uXCo|?|IbJJ{TD7S zghb;q7sd=HYZ#tn@CXV#%fsJOO{#@c9UfxnegBy^yj1QuaN_}8`{(rLl z@G1)WI{Hy2Xp+ljRD*;lXCNa$+p@Ov3E7=j;=uUMFe|L+bQ5fl@Ja_?qzV?7O@bC7 z{VGnL)po8DXc!6^*i{CPK%SBXEFksfgYuRMu#PDEXf_2vw&$lq%!OOYx=ax{iIQ|m zM8#l(uN7E{=tlW5^IHfY9gU4)S@(qkPHG-xssOR0)BfGP1-M4AFh>zVz{iT85Yz_O z4=?hN|4ILUAA-(v3*mu!56LJ1ML%%2aSe{8-rfEfH+;C9bh{kYE~*`(8_H)p@uIMM z^(MUbY;dSRkxEe^z;7*JFI#h24*!`Jzg^4vE348M$qFvRoHj3S2MVK_nQ6(njwhw9 z>b3dISQAW(z((Hfo^zXuw)$_0-q{bDjtdRKWJ?wP7p1N<+7=u-&4mkTJwAZtR6+1K zZ(B1`;0Lo2X4m^9qsxpFmXAl}9enm41m=0JJjN>NT(cC1t4O zhs&4%4|fkM=#U4j!YPycKYapPr~<%a;K(r_6Sx|N&X< zv;Z)~dWCQ7Z0ure%Xe7$S_i;-E}|d1dpZ+Q(0^i_I#H~n=tG|2639+&T^-=Y5TXvc z0J@MFs_siRTA27)@L=aN!x*9+j)GWl->6rF65sQ zI11l@?4HmLm=c_&WtcO`dd>q35;uE_0qO1wR)vSlgqB1w__mcIz*I8z1k8S|=FL*F z_Jg8<@n+_8E#h?tz#H&baGTX%^uuF8Oo7ENi+YPD$gS_x^TO)g3k@lVan(qn&47R^ ztyD9pcP1q8?bOb$O+wRBpnhG0o|3i23ghGGt09IcBxS&7JBv=*_4?n(BG(lteGaLm z%A2;+)AIVpNB-PW?WV%m1%a-*2(Qq1cXJQF_f9kZy9N5|QXVl`?RfpZAVugmO<8+? z@ZOJc;=(!{UCR$JZEVi2&G-4U6CV%Cld<9wjdtR{sLg?7x7eD{Yrtq6$j2#ZEgDT8 z%}o~Z&AKN)#ULFO#NJ`!4?Qlz0m}<7HAn{LZvZ%6>bW)LD0!ym$;+1Nvq}&R$)PaUF3?Xaw$CA#g_ZeHd3=-9a`n#TUqH`p=@i5i(5qjv2=-*sa z1Z4d%=P|%&XU73-TLa$hRFl(V>>j*!timW>mlsxWdjk1v14^vKRMG6&RWFAQcif0CAsHMcd`zY$PAHBNbN) z75B}7;UjO)HCVTrDb5T*&%C$OEL6bNiItLFFxsHIltx9gY9Vy1{pvkpVQ&m^j!B7F zj4VTy*v)2wqH{)J5=_nMdADx#PGj9C$}5x|`h1KY=1wijLvSm~8wpQCjro1*o>bst z6vFp^ILp!e!2v5RZ^Ti*hmHa%5&qmPV1BHjV_Vh3E0*y&?%$t%EgTHh7#J_R-Av{@ zOko#BxcvG2zuV)YvnMuPbYm&9H_5-^hJ%O0KLHn)9&|PBZ@}ZsDQ~~oX9qUvQ$XTU zaEQ71zZnJcf5^BX3rf1{uQ7=t{Hh4JFNKqg(9alpB9f{y-^o$WW1MREaGJ75Ep#o7 zjXv$MT%4~TBY0z}W0B=neJ~6vh>S$11WRXd^=z(m^LC4Z)m0WcURy~vJg)MKOn?Ho z*YSAoz+`A{TmJ%5Cgo)|df9>6M%P!TivW=e<|*p%?7P)s$70ij)X&`n>v=+VYiL#} z`tAcv>Nf_@GAkQzeGhb3WMpa^a2(%Ox66gWc5Au}vr00(?jJ{l5atdU*cpX4IouED zYJopf;mX}M7YA`Q%l(UaX2ul6iitKji{A+1ZE zb3V^{p2e>($S0Vlgfz8A5b$z%oRm+aFUu;skoEwlX2@(C$&vJ{#r?&s!*{)$&dY4ufrdpSII zKyZkmW_Z>eE3pk^-B)N3x?y%GB$e`@+vI=?vyry&gWdn_hTi19LjiIn#%IKW{1_5i zdNJ{@N660sf4YCmy%FBYm!ogvd1)&!_{-+tHiqvauc8t~B7-MO0}ufp2K98P9byP% z(q>^4YS+}l(E78>Eragft>=a2=9x>|`W%^d9`_`BoyDjLs=>&SMjEME)9_9`;HWbU z&imgUoe@+BB6#1mO-+5xLjLihfS3YB5iXkdL)bVxR#G+$rv z)0n879p{S1m{H05nE3IxB=-4~BnHJ=!eW%o5akCfv{@SC3Y+pKDnEd`xwSOM5F||E zeiR8hX#NV1p9jdv?noaMu9w;@>@>m!yK(?go|alHo!rHm+5TIIOlBxT2RAi_=6tqg zct#TISF>=N31EGee*G$D1r)C}8!D6@z`?@X{rM|+{Pc>)vz*7_*{O@2eX-}_wi<{V zA8?OASw3P+PBPeq2DJdP)7c8Y=J|>zD5|+Xj>7e}u5dSkevvKZx67BesJt{~V5*{; zIhNWE$hFQZiJWT3SuGhhYrbMhA%sblV}M<6kKQNeE~rR813*){}FoFtEAHNg|)*3%3ckMot6OvD}4TmB_#E;6ML(ZDtVI zd}va3D%DuWEOsEtx#wB;^MjwRo`PW*_8 z7zgVgZiGM+h}&01m%x&^w?+VZ&_%E)5Ho^Ijx>x^lr)fJLL0){U42{1)#$&l)e1`r z(L!JcpfgWHACdbm*fsn(=P_ zGL|zfp@)k5Bu7Fg5j~A~H-wOGx<))F5kO;M?dBH)i>1Pn5?jCJ6K}wVS^ER~O^fo# zw-A^Bxt99~y-5vvj35DFB5XAAmi)cf&0gV|ISTdg1`odz8rg9iy841vreCBXzvBgz zFXoY8hmHa3iA2fEIqa}6eJO5L5kn}B+swek(tdm24BFWRr|^Ti7kCF#x*7OA1Y%4Lz5Uq(%BgXKNbw9Z2&!+Wq`4@tKXGh!1+0=}F8}sP8feB}nPX zB%4#Cv)^}sL~~RR$zBHq;r}fc+~AvC_u}6{Z!^QvOjc*ANN#2og*L8&5&x*!5P~U! zS(wxZmso#7pe)d~6nj$z0qx2cqJO@{LjD~Y_=o)J@*Em*9q|)&G`AT3wjeCUc5Al) zWJu~BNn2R~%VQiB!PHE11PX2KJCWF5zy#bJN?Y2RwSt03nX`p*)qLuE=CIb9!fO_> z;nx)6sVal6PHodGH{T6yqRrZOU7!;ncaApACkGi1KGdKTOjAga_QMFlGAu02)q1N( zFA-OcuSv6sbGsN)p<}!8Xo4!Q#>dGI9b@hJV|fM-zw(Q z?hNZxM;O#3MC5}_U^e1h+;%p~#~kriPgX%1mP<^wTPZ#x%oeG(rqLQNMW!PS8S51f zYE|atLFpOhw=&R}%un~Vd#({|-dryUyzzY*aY-6NzD*aab^&@xp%z)Z0ONwe+9v-H zvGSBu38&cgUNMqQ{t$x&dj<^lJ*-fgUoa#jybr%OTnK_Zs>jMK+PQX>_P z5E3iv$le{o_fUq}bCb$zUfht#`UjIlfLB31PmLYYz(V*l$6sXoJ`O7#{7x+L6||WE z;vsT@x{DB|TnnJ>R$EH`R75Xr6TXF09>KwXYdh@Vcc9BtWU!P7`vUk=_qJv*7~y3P zR&$tsbI3UHziQ!$or||93#4qwk1w7#@Qif-zrl7C2Nm%N7HKJvg3PfrEzJn^HHwkH zL@bn^Wf<=80fjBP;|mL77olHd_Cnoy7YR&`Ls9QkBfVY(iRF`Y{R)>0Q|SB^?jYj~ z$CU=?Pb?9}m*Aw;0zUmt^~!dC2nm69BUO-B9&A+daA6qF`8%7?YrM6%2ij7J#!01l z_SRIG8IOr#q392^&{YUfQB|6V13V$G#1GX4$^F-;-O&Bg}?c1ELrXbk1H z1o~HPO9@MMUbo=|0V_&kMiXSLDv4m~5uq$wH} zcR_!tZ;+?kC_IK>1#NkWSg&R9H@Ty;;TkdBJ1|va-lj85=5Q`GUfjC#Y*HFh zs!4}C0iZJCfqxDpI=z9nD>dCU%QYbhxL@`D6+Fz`ZgPGiC5ARN-(1OFzWjGJ#%|te zx=8UgM9*Db_q7tZgOoZcxX@9CBT-!0MKDu{=Yz9}vk9R?U$OALgf)YcggrJT(|Akr zA+YF8W;=4f=m5>7L!Todeb&c)Vnz3YRg-~ZTU0~wSPCC4VebF8ou9^|VR|Mbi2>zE zn3kp-PuCsfX!0O>{>DH|P0W#GJlF&oBQ)L$OelTbg-*5ov>a5cGnC$Ozg-PsQ*@dl zr6Efvt)F)uVyqE1=^tk7iD`3_c_rn4i!P@rQHGHB?O}{+Clp4(% zWz#k&{)hVY_Z< zXV%bUcD00wUJa}RZN-9+92E=U?UdZGf1kUv>Ul4mNS3RQc5A{5>*I>2oXqy{;*G)2eex^440y2!Ffm;L@RAh z*R8BdUD4G+i+?|oXEc??jDPj3?4^YnSLIWtzOC*e6XQ+q=+8JD=yig7I>HF9hupU8 zqgwUfEmbm*HwgPw%Wyvj*nu}WAf5Ua#+0VaDx>gS%Y0N#QkKq2R1`3mUvW6$I#8OK zk!VNJ=5u@@c#oGbqrSuSk-zVS>PS-5BQ@B1@KW6H2y5pih8YS^W+dz`O6EG0_)??ZwCORfJ%W3s1>CmrMxl?`)UF>&DKba# z@o6Oy^Q6$Y(4=dYhgU?t($=-KR4-Y)xV>=CMlVH5hXz$ zAxGkOWP9`d81TqB`}iu`x(6c1)1hsW)24 zVEF`^BsIi}tIyS>C8X({2>e{e7QQxQ7XS1Bo$Zn9P*n(}eM!w|z<^Q1VYo73UaDXh z24kki_U-gMw7S)H59@a^|7BRy(rGpFTK=~RNzQS?d$1oL_|Pul4b2?9UQG}=%b16I z`6&VsA1M&|IQ#cHo{7lX=KrJPr$T!Vw)&aQFxk*px2lMfp8qCtCu$Tu=_c53ruAn+U?|0bx8rbMV%=gO4Q^R& zsONrbatD(NTg3Q|3@7E@+|^ak^ncz_gK{s+Y%AXiD;;wbV@nKRYJ_;=#zRs|Zrx-V z-ea4m6@ot7+*~O!8+~;AuU&(r^CKKn#sFcdrxHkcsGCxb- zJ*}up?!n;U83oNF(K%=@6dTVEl%}yvoK(gDNcLkXyd(5dAd>Wt zV)5$PQ)kwuNG65N_(AIjHt)kShtmX6g+dM+TUADOb-5!CDNa+Zq6geq2E;%M#VO&@ zE?69pL7RNB6_oDO5bs#&j5?AUy1t5xVjBNqNgKt^ z#67+C$Viq0)=s>=`u397j}fL-tx_9(vexWo8DDAbe7t1IOkOtVqZUM0z5cN~kER&k zk8X6rQ%%n@dNuvIY5YMqPNadNN?=5d*6UtYJX5QI_;@z@SNH+C$+p|yt&h}oHc9C? zsE^jN%iOu~1=bcF57XbIzPu-8U{faG^ggEPlS9efsO~G%hwC*zvbl~NJ;OqTjN&hf{~z7V+CZv@k(^tt}aw+V@*o3mtt4lN=ur!y2;=Zv{ORi-s7O z5G5|9sNk25A!lzuNPmu?;jgEmnM~;#gCK~f#cajU&l>T*qh?`T#>RMX8|e*sgmu~M zeM_vtAvOj#K7mB@e8{yLD_2gxwYlY_i5a@bnNrFK(f|wV}y`<`q%Dm0# zE4VfrSnQ%3Z7a^w6mmJ_TmOS5EJP61%+h=vD+snVM;^DA!X;qF7;E33rc#dQLo1@h@W@++HtS^SV5FLV6tDZ-x_kv_^8D6rxY4sKyMnuG!=jJ*Xy2LuM z#@RKve2(YGanHg?2SG)6Y|CP(5@;*jjnt zu%0~MnvSJHBd~sS2!Caz8tlY47uU)6`FG@XxAAw8kCbfne`9asXwl5irj|__YtXA( z#`Tf>i;~X9Q!A;KP_=aByk9JMBhx5cBVxX8=d^X)#6-ladpu$4Dc)_ z8Pm`}pF*%Lk(~pzatRYKUbx<;rfUTIZwRP^e1xj{NwjeX=oH>&ut6+FXNU@{Ym@8%F;oJwrb3b?(JxUG};|CN~U znI4HtuC86z&r9asaG7&>gVbZIFh=X^plE_iXsLABVMDmS>irjq=iDKDcN^2joF7gn z_2vTKgRW}wAQxIqNHoIccB1$m!6TQ)6{`0vt{p|PY zHrqhAqKVKFo-k~fUF`h35v-9E5MSS6Zw&^iPW*=TsgK@w#ixJn2UPv6{SU%&G^dB4w+`cZE0O=Jb0*Y>)vwbwdBKHV64!;WlJw=y?R z7`}}(0okkMmrn>o7^Eqs89L>!F=h|J2ubJ?r}7Ape1?7t&=fySi;G@s(oKsqXkhmP zwa3Eqdyj1S&BN5|WZ%>6<0}Ln!Ze?T^x);QMQbQ?G+Ezz-W-?(P)Nyb<5xbjfAPKU zA*pNOzsxq*hilcV$shz@yH8phXr}>JBW1o_-lj>?B=F zX@20@{?Vd1CHyo+o6C3{zWD(A|Hb-(}c==RFnedjlXYFTP5yx=s*_6kxnVkTPaJQ@^yW9;h z!w6;)wH%g)1j+HXT}Y6S7-%SIN@0Swt^!&=4&XWlH1wnfm9t`Ijqe{A_|F5N-}CRD z@Ts;X?KZKaj(jwU;`fPHL3^hrZHy zPV=YRzDI#QQS~qTM?hYZoliENZwENM46`@?1_CDFjaA(7ja zeB=M~9&1RU(pu+fJUA`|)LNi7Je7br;6L~p&XQ^(9gL0`aaCJ~cHxMQzrc2Rp1iZu z%ydEt&lr=rH~|2WyoE~G-A;TQm_-sLkefaO;u#Hl0RY_2J!*CQueXsh8LU+Rzf^+*EB@rC zEuNr875^JM$ZjLKOJ!L|pKL&*j;;6j)n+nfZRd?bG_Ex*KC1#7X(JvW-v0Hs%|+l()Lnu0T{?EcR-HR$GMtz@0b?^*V-s5ppVjv z%~gz5TJd~`yBM85L#w%RhKvZnuK)Wu{yG}mF-ae(cv&W!{iNvoD9BYK9RRJH1&4;6 zU3|K0t5=`dIq6xh3z%1`9QJ;Qp3lEvov^#uaPIO0oEt|(UE){E>3spaNtv6l{phP)tRXfXc>F8N@js7$-Ezs&>Kp|@%!gV0W~4{ z7|JXnL+d!lqVmVhWam`@AhIx|Sd^n6#!ouP%GNTds96V5fH7nfdp(LmXhtLLfMg30 z)pu*+-`pnmgtyZ(3)%hEw_pYYwig?A>x#+<-jKzjhO$e|nwcbn};K&ztgT;dWLc~>_H<2%ZlL_{NxCHT~6 z6)a*ha@K=8V!jHdV?#5zf}ap4R*>(D#`@_4PGh>1`@y6*={$fP{-Bxb2tfW~i(H`l z_q0?6?HB=qZnvkymD>R zbD_~k8oqVHq3=hJp*!vz6*tc9|`FXFW^J9l;QQn8324rzlfm-3@{>v_z|xFYVF)_4e=AZu~7aQxSur^n15yO@$%={d7;yz5yC#${Ow>Z(^B1jXumAkA8>&OHUJ-OE}snJnWB z4w27tpHw=m39Fhd20JWl@@NF=kerp`$`+m`_67XA>`Z%$C|w99i51m4qOS4WKL+K& zw62V69ITHt`5Gw`PoiCA;65SeMnh>Eo4g97rbcO+jRw=*Xc>cOxV6fwYR*}=>A4h_ z904jsF~|U}p{MnngY$-T9!xx@fXT&e6s#B42O6SD^@Clb4$FKEfDhQ?kJ1g`n*yJn zb@FV%(IS921=1vkg9& zptx!e)^er!12w{LK^}fs?k4xZ=IcaZ&!eSs3OKjmxi`jtl>@g-nCynjjmv!HDFEkAbAty1Sa_G;2zKfCI+Dc4wjpmX!~w)MtL@%6*;-u ztdl!+`IBY95h}F?pxXZX8@h}S0HO;;HpzwzDH_@`zHm{h;%W5Xc*eZ<429t#ZnXXC z*O*_n_|4hW83=ou{>p!n5eVbUlw)O5iU0X}e|=^rguIRidlL0ffP=~rR9yr6GQMbl zL6Z4Vs7LXMF$leGjDmh{n?z&l3_R@L!JdY>Cd?b~b4EAEo(8)%0BTav%FU5y{gKhf zwr^Q7_r>at?t%>Be}Qr6@i!`Gc^oNHcT1Xi(DHQ~sD2B*-8uz-6#%aKi9INcU_ifI z6^;gO2FR;PI32Oa><@ARsDY-y>_gwp%!EqKh6lgfH8Ix{ry6K!HCMy6Qkt}4E=Q*= z^HkT~6f|r=B;YCsyTHNP4LWN(ZnQ&T+SS~^J)t58DVnUpbpqA5Nfim_=09-$_epmE zG*t%L9q?Fl4BHV2fA?79cq9-z^B%VE9Z{jGf8+=tVFPY{+wn6Tr*N~1CSlh?(3kf2 z1N!TM0##0^tXAz);3u0>$e)3M-Ca<@bF{WC{JmrY%dms9=U_G0Y?A0UxeVLq2RlHw zE`y;BK1`nkIIf(S&D3a9)=(F^TR0TE%44qi5JOXt(A6FZNyN1kDW4=vwIL0F*7t88 z4|*p_xG?@Y^89pN{~{Co+d^5|>JHp95#}gU($NS4JDB1b6HNeu%{f`aJ&+8{E(Q)w zlW`!lNG0I$(Y|&W-0&2NHH^k$KHu{XIh*2m4tV0DK;3B<9%(jC!Ju(K6pyu@FmoF~ z5C^?b;%U#-&!Z>PXw$J5xpv6jXU8;SGKL+94W=&LOx)DJ9s0$@}_j zhb^@k6nkz$(noYaN?@L_np#=xH|T`GpWw zEF@*U--BcG;AyLJX{o*0dX*fJA)OdF7rsM*`luew6JwYZnHL<_+AbdC+VOL_(UG~* zmYBE_o+81^Xg;cV$jIU)*n4eu+Fm2WIZl2LeHZ=5@3(NY%dc%fR4IGi2%+N(*_|&j zm4_6cizp$hc;j(ja2L?3Qtb*2;65xEhG-%i+{gI{rD+330 zo)EY^L3HFE{(@^DM%x;JFk|fdonIU&BmtDZd=bLv3gG#~0Y$iKerAuc6S<~RpvwVu zWu}Rp2RX!)rUFG$Fv(9Tgf7w$V{Aki5cM`1Cenw_RfS3dDac-zYldM&4(TsWA5A01 zHW5q$&|ikyKL%xaddz@nr~@t|MPDt)0J{=%BdFI}i#NiiOrHHpZTy721wF`h;n0XYg8Nx`v$|5!X&1gMNCO+Ogh}Xme6Ts&qhEXl1;R2s<9qIJN zI{=<8q>T~femB;Bd$rGZ_+`9TmBI`~Y0-ID4|#&-g6W9;O3u4s?y;9oRLS6tkXV-6 zsE~&rjp7wZcC+BU?=!$4+%8n#@H1KkM6ZF1aeyI>{|d6zCrQH}Bq+&Q|8PG`jr<@( zZ@yk`iMWhof^$r!iS!vpCm66bHfnZsjQRhWDt)I%UNVGd`I$DfZr{Pk0^=$0F}XOT zvCB*@FF_ikFwo8aO-${_?#P2iG@a+l&HrE3ErISU0X^nh@r{ z1xj6a@Th%8T;+4dK-T0AV)FLOn0SvMd^8I8al)bsbk5KWbX7-fs&3k)7yhxmO?y^= z7dhtlNR6|b!o;$=?R>FWRpn*5w2!*gj$qS57;D@o)`Z@PZ9qs+1x77Cy+j{~s*RHi>nM_?+>w@0Hp9A%m1M0r4#)~(e|sSOpypM4>-2YUYdxFXd;Q{ZmA;{or}f`xo(B7p*^J;A_orL)T&6j1djrPOxrv^_&7zk}t&xnG zcGHj(;Of~LYxRBuvSpFlH+jJjGw-=aX8P!EZS)?^XhUwn=1%a^6eGv{V3i!5Cvq3` zrg?HwnDF~#pWK>aaLD(fuf;vr2NEyZM0s#QB)GDcvxbKC7i~2mdaUwDuP@DvbZrkH zabXwuoFD~gF!%pHF6jBm{{rQrS?hIe9zSRd{#6`(N3g}(=>dH0zXu|aDF6&CQQcP z<*nco5U10{amr6ByYCh0ki_A}eFmfy-TE6@M&QETqfaH7ljH~yWtB2tJOMhzbwJRy z1PG(EVClX96h&sE-oxP7Q1drKWAN!-Fj+V2Iqg!-dlhu4Z(X`?4T7ekt-!t-9w)$| zT;O}FtS*yJSuGC8;m(?{nAFc#%izAi1V)`rTBV=fIVvR;C| zS`aab+Z2oKy=1y=_AC4l=5>lbs5G@6Vj%S)?G}>@`Kgez6uH=od5e(dF+u#ak14?( z)vDdXz(olZik;8X-Wiv1Td89_+8hnF1PW8p)0ooGQ4Lwu#T&pNd6K95i@92JR`O?6 zyWK8eeT?Rl%uVo>p{xM}ts{V-?iPCghVv$)NVqJ+oyf;TQ^suoDx@N9qwMELW~)5r z4XlqrUW2lvr$NrW`t<1e-DaV+g1(4f(Fa4ryX7i6YeHuPab87}5X;K{?4!w}nn;ch z^bKuZQV3G%Mb%6{W!)?(q?!+D(RXz_6}S-jNI%y{!m_!_(#Jf3<{XJ;~t_5sWET9P$sKU)=lYMVd0Qq1Z z71w4C(^+5I-9Q5 zkX>@mCD(SmAo~wHXlV zc)6$1yxnr}{_>HnYj%xT$U|o^d_jQI&z}FrY^2WuI~jyJqZ`A_-*T2E7Q^d2Hz6KN z2u%hY^NA%J>YTW(_0eNwYeYnn~aPG&&{)^8=o9gc;WZ z)MwUGWX_?_&vLZm1%s_fy{cnwu%50pJ{MQ<-k4uB+&4&FyrjV`*Q*|DH_833WBHx7ZA zN;6qawtZ`#A`WCZc?&OMTbd6+AYb@d+0ibme&ls=3ZE$S=y&S|1p&slO8c{pHpPBV zzVj? z)_W)dvp^DtTqRC-CeyL1;i-d{SWK>@(uU!GumEo)16EU@27s3yj2NJCe!{SWlp1h8 zg3$812m=Lzy16d{HJzGOgqIS8pcOm8A5MV(pGP!s@lR0unV+9?}WK z6-N`>0wq9~9{mlvXXC{*u`PtvMVoqu*`^o$L3oCi5H1%aB#A~Z-n(Zc=<1JfDCdp@5oV^LrzdLD z!q#~|r-d2xx`uM0zYmoL2vf6`Cj6N`INUp70ER)0^=->})VRYYd>n&CPF|L+TL{h! zwwXsa5o8cz5Xc5Z3;u|Y{?H(Vo>z@ne8hu}YY2YTIhR={Lx&ZK(-kiqkMz3*WuNwn zSs3`NE_*bGQO$Epn=DKWHLkQ$#OWS(bL2!z$PT zrDUNk+{!>nl=cPBldxr$JoO`<3!J><6C+s*80;T>oGl_omo?Zf1E|sUB!&G+Y5Q9eG!=$OU?W8s zK#->4BqjyI;0*oCX-!S;S!*ZEU|Gm0mc+ZjZsCmc<0AFA9Ip|u0jYyH;Ay2D6S;=GYPmJEV|-oCh9>VFRHD_8AsMD5 z%^X%@otYZ<+%0Lnq%7+5`k+i=FMUZFRG8LeJ@@ko<}r?x1bSp>U890-e&UCXRyj63 z*xXDPSB*B}8a5(fO@TWzkzUty(9CYcQ#ky*QpPI4qVc((AsrHczukdvKpTQJ_@2o#E<+gu^kCS1>G+{)ex=4{s zKm_+(qV&qc=ljU3jPk?xEsm^@Hm{gS`9ZzJ#z~{KzL(Fw*fSGS8aiGg_<34Ff(u_0 zU!Pt&XWd!W#IoH8qi;jr>b3_!`Bg<>Y$_fP-1JU$R?~w#X~>b7BKDU8VV2>+FCzA@ zYC+m(n}^P&h-k}%#BrVeg{7^JqL6w`-KwDCtYr2jV%{QiNoM@enCrg1UG+h0(37pr zAg;`yy%aVcdGx$c8(*M5v~_90POdD;wo2!CYdjdnXTvuCG5JJyps48%G|e^mehrb( z^|I1Ho%K3?(eF}7>?j)+Zs^1Jg^5BDK*5$-D3 zFa?gUsXNz8D~(lJDxcBW97TPde6Uj_`ufdAGS`6-q-o*n%`th8Tm-TqeWOs*%HcyG zf)t22rclFnfk(2B;(8PXU$|jxk|wC|uR$?q774UCPM21YKB5GrZZTgd&>Be|iS{6_ z0#$J_pPgL@&B1OR1u+NYie>a6;Xby~_YAy<(O-I?KxhXbgl7*72%xmigo;G(3HtAK zigpgtoZ(p$l@dszbv~9-l2Y$GRnef4gtRn9VeDaAqU$9)>LIFhI*``LN=lOSlz)=s z)C4SRKWL|z#;QVO)O7}4L)_#oXSv6xJ4)iZ67~%A+}} z3!?tniQypvHRm7`9-9B<8@Y+6MSY7_$0J$8!HpBoG1j}aHDn(J-OtQuYEAvZ1Yns` zl2%_kF_LqGa;3W73K4}kp5oGk!cyNTVpESJD&cht1rcnq&6taCxt($z@_nLxg8vNv z5i>L$x*q4K6$JemV;KKpxMmoyqG@jNT_e+jIIMPf`$+VH+br?Bw2I|p8*#*cyk8c!g?^3}`??x;HIyzB0Qi+uN+RZwNa{zbMz?8Fp2&) z+(nVp(`tgxg>7R@s9S)1if4CR=KhGA&#CW4F&(?jS!lU`>=2(v+i%rMIm;QKi@;l- z7n_hp=SR9i!FMD)qt<)xFdUIfbWDGYa&^xGo*28D!?7Z!rd@U_|29o0Qb;ELd^{fG zj^$cgl{u0rTL~(rvd?LQ@Lip-ItB!j2x11xL((inDd$e-PMZM37w1S;gVgkDUhi;)EkRZ@;5Jj-q|DCxh#%)vId>&Re0Rp}}#sMY`Rwe=d z1HqJzcapVic1{Eui9#lUL4g6?Qh@>FapAd2t9l~yOMp4uv(Vfoj>1}RDyb{aR^j)Z z?etk%ssg#I?C4gK1$N+9q--0!PTM|mH;nihu$cO9CRO#1ETWq5HSt0-Qh8Es@u+{Pc0)O& z96N-1mfpH_w|Ap&v6;Ny8w1H9oo<>f1TSoC+Ea>K{lrsE_!5M6#;sQkRYClrB6zc@ z`)MV34iF(elyzLSCa?~}CMp0cUg)9kV5@R%R;|kw{aUbO1iGp;c1Xc`LpS1p_hjq% z%Gt7S5-tyl>{c$(^oV+d7dw}-6=xsYCK5(|BXd{_kUETD>i#_=syB(drlQ@c+y9dW zdzD-6tc$%&Jd!GqbQAk$4H?}wgg%UvOhI+C@yE-|fY71g0F2W)AI-I7rKRbU8c@XNm++Y-j21ZJ3+zU-}k%3Ild$779aN$QQ9&zl@jB%OdFE2&XIKs11s8|$5vizM%ArZfQ_ByND_tMxYNP3T9rp}k4i~Aw z1OEEZq}6{TnON{`vevkgl za#>S@dnK`j_53SE(IH&yP46B`6)GO82dV&S3YPWGU9)QRZ`1=-i>5v2o}#_%AC8~pUA`pj~MZMDYA z2nL*58d|YmP7(>3qE*i46!>?Ms=E~(f3h1v#O!0PO=6p=OXxP|>sI))F_5@w6;LtE zK=X)=w>-$L#;~spff`SLH^;vQdIcZ2B5%um)7j^X0Gmtx*(^6 z&Lc6Q;^=I{J)ynBbD1Fsp>`pFwPcXbGFrctgA-uTXQDK~ib46);;UWsLTak4vEZHB zNK~@FTt=9F<8maglnxy@OpNJwP40}8TQ{<>lDP`*w!UqB z`l3HlWvut$f;<~*$pq(;!gpg*E>_L#W z#-4fS2Xo`vi6&jAM~knJe9S)Ko>5L+D_X6b?Mu=>%i0*fR^66%&z8A+ccgc9S|s6j z#ZzdW*etMrG0hI!mhV#NquBF`-==oqL`01{vsLiBt8hQfb>^I2v8oA|ob1&3C60s+ z^+)*ggA!YAa@!u1+qxfetd*Z2l&0HhDtt9wIXm7=tLVA18x*Z(f|~bs*hszZ#?G|) zjh2J%!!x_0BZj9<6yo<-Sm%b@0d*n(>hB3=4X>|%BPT6>-TzjtMx!8dQrrM%*ti)z?wYXwKkqAR&8r*fiX_GbL$p~uzBEXGJfYZl4KODZ)o<|e@y~o zybYW@pH5S(y7cIJ4HjP&6AZ<9B85<;s9grXGxOx$`_?{7QRN=5Zb5@uNw~;j*5103 zRB5hh%^X_@Nm2X0e=TLMgh(%DcK5E&E-^Dur&&8*8njy=l7D^(c!R|hTG;VfUE=23 zeZ3UQV3D(=L19ExYxgN7&Own`n7ej}a3_;XUWTe=lK30Lt6mN%oFQD1T-pnY%8D*-m(k`#(WmU)_6vB~v`fX3LS~3$xap2Cu zx^qUX8X<{)O#R>A91%s!KoSwblxa?pHcD^i#v)!Je*=-$ke7+Ot z7@A6@LhYC-oWw9;ObB^LT9}d==!LcZi(#ypB?KA5o#i4%R?C|u;Oyq^*k2QqL z%A;kkL=-kwrXQ$g1-J+5IPKIg=T1+WSxD5Y%q?eKiNK^7mQ5cnW&Hy5t;=Qoz4Ag= zRN;x@77ac%HAI&BzBvq$D7`8EK!P9un>)ipZFbY`EV5%iC}>OGTP+?{XujrJqTxD226)0XK}v{(*|kc^VYQI>IqCCF)cXF~k+< zbo8?h{ZId6V!+r!EBuj^nj7C?-R=qE4-3cat64f$J{d{B%`3DSO8%P1tXCGHy+2#! z^y|mwt&|bZ7+?#_=P3A;7PTawNhvb66EcK9kVQvQl4_)0ymiMGCR*Ojd$*UF|NgCT zIylXI|7OB6pQc;#-NK4n+@_uUh)S?J+ILcrDau@DRq^nrhkkaTw6JEe1nXXA;dq|3 z%oUGzp&}mDNyCdnqoD*wb-RbO@{ydu@mpg#&%nUq!qt&gzxoUdONx2Cv?H2>V~0Zu zHL{Mf{+EQt?&vVsk8r)hzF5Mcg3_1wUF%Y~>tn&EH@uUojJNsqE-4{u*eoxL5-Mva zF&t3C!Edt3Sn0u7Dy<;=BKQNq861ph_6gtI6epan72TA&C91ttXvkCVxghBa`pX*u zztrLPKm($*2Jt!4pDES+>~}P)kBNxFEiY*`?18JEW6bx2v(;S&zE#fqz`T;<8|ep@ zhHwO=H2Pd;f$Mj=BQS#i9NmDY+gp0zK?5Mc`KL>J~xv&*fap>h$*YFq|NLk&?0A(I2Y z0ea`i6TO*0$JCebxlXQvR3_N5_bV)PHCoKW#$l>B$`r)tylob{LWzT{hFAllu zJ?3^~Z`$(4t?M}Rds$dQ;a@A4NU1@RiTk1-Yd`iLNvIV{V8%J`Yg5{zuD${X@6225J?~8C zvpn#npD6CP>H=G3#?35^of_L+Ru2LSr@g4GztB!%nBIIYAs8Y2Z)4k@-rq_#l-&s@A*R8OPj5@O@qeuD;?9tPfN*ol_G%k-c+!urQ$DN3~OEEkBpYtbG8i;-cXW zF6S8noyoH6i>45-mSU86@OJvqjnItg6bO;0(1K_g3+Z6!PtVY!oydd0LeBhKT&(-} zML$!|E)slu$lOw955EikpM$^@D}5LN6F`SA3A;^_MhJ++(sS@>zqlciHku$NMekE&_%&VqUkU|JC`sCZjJWjBbREnp1)xI~(Gj_(o z)EogTI#RqsC_s=Zf+d|BXY|V1^QI%I?FE)q(__+<@kGc!Vphp@+U>eK{&aU#5T^!u z&iC=*cshQURmuBkWnKwt$=FolOEPkC>_zq3sH|t?9vl{gRY>t*om-*4;wgbl2Ee4~ z+wNglU2>XxE zqkH4ui~xs!Q@$-VsxSA%3e#ssDC9`Oya|dS)$)Wa7m?IC>vAfZs_2l-6%r5%3*}9o z{=6geB&y(@RqZTbH*UDh(%%a&i;Wj%w0DmWRJno_9w|2y)V}_?C;m}*M{z7|vcM(fq-nSEJz$gRb+CKd71R1>|lpfQXzP82k0;N3h&L-ey|2I;@ z@A#RJ$u`M!oLrzDiD{6Xu51`iFy;h z(9%$3pWu7UUi!H0^{bXA9*UC~7*T3i;)Ofixy^jcO9kKkcxs z=}Ru65GsP*6BVs*Vui0!4il!%50EAHfK1ZQ_Y*+XXe%q6aCTyNr`_k;2xKF04VqIC zIskBDRqth#@n69sj3F1Q^`*h-ru7rdjE~e}ce02p1qNlFcq}*T){oTW zwtX((og?l8c0G!6ouG^Yr1Pgs;%PZz5WvxSi-Z5F89=$<4NUe0nEQpums6AyOmu#1 zxTEDwSEZes7E-i|pMIs7m;HQm5WIVQde}=B#k-X{du=bE!BnLXLs{H%N5p1e4%GPC zHwO~>1g-#*=Q6d(&I4=$cn!7x{1!(%;a0Nf`~a+ZZk3Srggpk$q2e5>vz4Ck2N72~ zQ7Kcu9fIZEe>|!UOn8H-Wq;l5b^?^ye$8!6)5B4nyEflWm^sGt<*+h%?c?u+{&5X~ z1+CNOM&#_xle^oG+y1!vxqto6nw8>>bN?*UuYQi{ffsC*#PG}r>VZmwtQZH*R6z~O z?6Gox;ix~6X!~_486}mTDwNH_O z`GDnHXEu;%T83v*Ln)8Urr+p&lno9Bz$%W1;@+217H4js!BEZ_B+OrqYcNHZ0W$LL z>XqjH9NCOT8F=`2i|q%~q)N0qXQ!c00o0})$aF8Zc)mOQ+VOOUt_`pQ^;l$i>y2T+jdN*ks+KlDO^Yi)+`VX$AG2xy`3Z~(cF=t0>Gw3)?X3!P;FlqjR@ zgSDu#1lsO4HEQIf@m_5qAF9<{zStcAiM|K0#;FG9{{AJ-e-6<4zvrx` zgOvg*a(u4_`WiIRgRtxh^NuJdFJ=E1^8fJc;C!#h%>Zy|d_?i8^k6Y)3-FM6*h}eJF*zfr}Yv^$@>)|QF3iV^_$!j}+ zzws`vKGO07{2$y`BD=5F!Nxc{08xMEw9+ni#%o$QiOLhj7N$q#_q(RveyaBkFC1a` z`b7#mm5B3_pu_z8yclmyg}~hDG)ZDwR^9S%#FS|znd~c+{ItL%oGH`Z&drPd{p-}LuNdz7Ml*$YyrVF+cWos+T*<^x2%7ucd!M}>d9c|DofH45jCFhn{I9iC~u1W>)IKtZUBgn-=YhJDx+9$ z3zHu3btd%6b(lo}rRgT6nY_o1Zn-lP0NU5G$hSu_wi%_FQD{(k^S{HR2Zz!B^7KCs z-5Z2AI0E$MG`U$cKF2HxO7R8>8g#Z88E|6o1fnH{V z%bK(VOqBzW#YBGp5&65Gg&w%uiFh8>dH?!h_*Z!CAD7WDRor#=Ako0o*pSu%dGZIK zvAF22e+yDZ%AeBnfLg`~(oHUUVQTQa!J zc9}~fIr5)%1}RPMs3N8;Q>;4>wYXfy!u+mrf>&zNRO_@@H=yqSjoJn1 z_s@fC<=tVpmsW#&9`MK8{?CWPcz^K_GXROnlhpd~WaN*6+=7=>c`hoD^MOD;;|6Pg zI*954)5IDOxVn6Uc_)&D!Hw5Uqw5Cp*H9%W^UoDxa*Iq<-Zw7+1MwE^5Znk{CsWS@ zVrH#wp!B+C^#zv5bn;G&Rn`rFsGscFH3?Gr7CWi=kBR&4_eaED; zf87Oe`Wp|Y`$<%d#z`)$D}J4B8^5`Snl2%1<2!8pegm4M3WI5%|M{e%hGd za~A@R^Q%%tApXz8w+s{q`vM92b3mTRSoC1|?!FdG+>5#X#NKL5E-gz?H^E~f0W6R&;;O($_+m}VTvuZ z$7=R1csHQa9H6r801|Lwj|EUneFT{(;}ms`9z5d-AOy{jGOvpxaD7B2q&Kn2_#N%i z@gIPiqG%^0;PW5}qR0O3Qqu>Z>^uSNKIJIOak+U0I15Km`hCQC$UmGT;dx}@u+rX! z<_dB&+gCrnVK{k$;5ZACg*z!7jF&}g0C}N5bzCteWxZ<|BCSM5TH>BBEm00~y!4~h z`_0sjON|qsVVI<4ypLs1L+ju5gU9(vH0_<0Ue?R?u;PW`azF_h(2lXM0_pMVKH3|2 z=s5ss<3|wG=Rt}43}_)oP_+aaEk3!q`YHCl&|DYZs1>0A?{%Bq@IT32D0?n}@s+2p z1XdT zKv!4^@woLg+G;2LGax;EgIg@!R{``w8OhE(5aJdmibx)akOt|nyDT9vn; z4EA4aaB{%BDdlzJLdU})yD?of8QmNqoN{3MyQOG%E~;*3<;T8QOIgI_UK5_Fhga_u}8=#n+h%2GrO{~%F6uR z?}O6w{r&U2zR&k5&iQ=a?|WR=bzk>1wSR>QMr_Et5tzMC>i1#T?1^_W$QVST;}+ow zwnNiu1%f!=?yU4t%Lp&Oa}qB{Tv8zDc7pSWB@B_}Lq*x;VHjsHC5qz-6Lq=~#z<3A z&q&FoU)Dg?PRp%1wL;~o6UKkqebI5{!`Z55WAejYNb&5lZv>3srgllU`S$?hcjka9^?Q>?t<;%*3r4gFVT((^3Z25%R{Hl_{T`ynNrnE?iOF zzwwNWpiC7Pi2~>1-JaUM<1sP*1>4AaqomtzOUVpp5Ge7O1Id4 zr)^z*>G_Qw+`d&Ye3je-Solg~ODhs$VBYwTdaf93kWKb7Ou{a57W~!|)UXsjlO1AH z(yZKCF!pSOcfFnAh}p^(=EY}LbHFf4Di^BdVh`U7q(QqULrj$^xMKvK(x)eg^c_)p z($gC?6RuGX3}mc8jPt{av+beH zbKt$`Oinhc$JWz$OoOeXqAJlMeEzIlRZz2<_uZf0KIrA}hU7suOje68+L0`-#1Vl5 zGxXofxd!WpuKqZ+c$H2hxnAzokg;gv`ckc^f0HVat}=~)Q3L@`5})@<(3W8t zDt-FvhZRT6m=s{3v-?ZtH*5X!xjlJ>ydU|Xr^yGhF7uK(K{IV}B+@Qu#e;Wix=EEE zlddxu1z3N1d0qU{We8+ZV^qQi;CbSji=1wDKVOdF_90{j6fjyGjfhIc0+q*0*mWHv znau53nVoz#&{JI3n?!zCo?y-4MPLb*EwD%nYfmeN!Hce|*29g-O!W0hLU*znPjiap zI{msrwJ(I@e8ntX*Hw*!$uM>In(;0b^VsI}G6nshM#lT=r)`Qd&7ip5=GC{o4O&Zu z88+g>!SZW8|Hcscj(EcQnh9hf1^Y(kZAh!;H%tKjP|CyTpaf(qr z2>wP^n*UqP!vnx(V+Q*YZ6bCa^F)sD_AvTN9E7IvCRor8bH!Rpqc~1MpF&2tS3CD= zQ&8VNZFXXtF#~0m1whOQK9`sPN5o3l0C0oq@LLFKZ6jhq*>b6r*2ISSXIe5bp52=eQ)koug2Jc%kLU(z9RZ z!`?ZvtkxAnd!c13H*sr9YJ(!iF;Q+kR&Btcai#_~TreGW1`gS>cJDtLB=E+4FFBT`7N7|x`_$9}0bdFmt54^W zF!Emw_P=rOD9*Tku@4x|-|~W;mt%B3!W>M>g*5Ft5w_&Q-K)^RCX#zcfU#S4(~?uj zjo1oI>1-# zTH}cE+o|Cu4%WtYvCB@srjl@!l`bbh%;(%Vk3Cs`9KZqe;@NXgwzRh1+jAtLX7dg9ZR{`0_M1f*gWEt} zF*I8gC2FDG8-wjOc7LoX3#!{^pigX#^A`^jB`;T8%}MgC@Ov+{m|Z;qut1=^@a0Z$bGd0>ZLr}GLz7{}0-bL` z-8>9f&c=-b`i$^^<~TWjt7}sSSXb}E)8jEaCx9!?SO?UBmjUX@PPveJrHo0<>=6J$FSstGv_c)Q6a&kTx%4uP@cy=^RYh_e zclF4xevoPdIJsyJcLuwVg1=;;-8tW`fhXp6e4w(mbt^O$rmKc_L_+GZab)5@>o#vk)r=PFcI|yAoO!E&&BE^|4E+xAC|$T}lCuB9I=(>PK!3@Ln9vqql zxyr~ccNLDXW@+;c0dreTy6zd$%?{9_X;ZP$Ov}_z?Vnm!Ab4-sM4z`{JY=w@x+zMM*T|8@r%5Ub)A~dPr>V zpGP~nOVx}J%A@DZG6WAY9|-4gcYENeSo+C48ITQxF)|{p91jrtBf476dS@W(>C<6I z6krih4z%iyynk8<;$wyKtmDnSJ#PJac~)5`FTZ!(7=wFYd^8s^AHd(5K&bsxOPgj` z+NBu6^^J{gclxv$>vJAyLmGkgKKTTsWw@Ln#jqzR;Y2=0R_W(ouU^hJc`Tj)JJMR<>0JRQk39FK z`L;Bj7ukWEyzgV1DO}+|imT~5P&K9N36AWoh*}MxYpo}q1_egkqHaA1vGHc%|!30#B zFdKqzw-EZp&&i&8t3s~8LUbEDLfACZ zb-$Ifug4-3(t|QmOQ9|(nba?|Z>y7IwidDf;q&Sgi2^aE->3W-DbjMaIn4sj{LFK2 zU2e^^3FcrlURMYs`lB!?*&S=cHC5!8K59g{pU*r1RqG{-KhH87YQ4H~*#(RZe?a;d zYgLbPClWyb9(Dl2bYa|D48dw_MtGF z#gBW!hm!f|CSlHXmahj|i!|UR>;a3>2E=|8a?2O0GZf`e>U2CgVIoy(7_UgX@)}^B z-X*35ctcJfUO?I_+j91*lRfnJx`C`R&sgR3^qdWRQ4@xcg78yjet^ZjLrh|H`2nWT z`o+uX64^Nka5?MHnz-706c(Z)Qfb7FlyY)657>pIBo+C6mrd)DgXHBEpD!=k^^*B->QVI}`) zQPybuY|I(Icz_TGtf2@Oxbd~xRE)jZcQqVHAx&!nOVPhj!ER#$5W$VRrGVkKdm}9zUrK03c@no z!OCnYDq$kTQxh@lr5cK%M~Cn%xndVYG#FdJ3cv)a>=yvsw*!D_1DoES`5QJohqaln zcY^;|s%osK8~g;Oxw`8dLWDAPT}3+OrcaXiSkR-ift|)>_>*zA15n%NKw6x;0uPIc zLD(*dJPDj}6c?a)?JR&?muFtbs;=b&i>yC@yM^USd`t_J-7|WgeglL@DmN%|Bh-gl zU1ok*;D#G1M!9)LzMoiq4eXw%(1Wx4YwRK9UInlCtgWl*VPoCg6F|zkMFFbhnRgUG zGzk+%C)wBj6j2R#%B*H8%~NrQRIrokT^4u|eflB0NiYxWM%$YZ+W)xDj9ltGbs{8h zCf0hNlXosGFE7}t4;;uPz3@Oaq~a7J`c#B5?>T}AoBV@rW2((bp9nLhLzX|jz4^JJ zL}QmyFAGk)UWgWFPyhv0(aiwac+N#aOpq{_jOJWB)B+(HEzs{K#AYaCE({$b!AYxm zC%|^I$^@mSefQ*WLiH=;#*q_|gMA6jr{Or(3W<259cZy>Hmz~9v%MP*&+ zKL&OvqHblaCZ#bt&!KQI=;1LkBbuc1bFw_ucIVGA-af1nVGjL7hwZGcKZz(kKA}bu zQpc=BFc9O(Sc{t06b9;J#r!l%>j{$kqWj zvsd4n=Ii;C^Zj2Nf6MC!pa4ETHwA*DR-H-0O7-T9?L#UP7SO~ zCi@jR?Z%iY&c_jONGpUy9()m#7{Ol49 z0ds!fsdTIStg&oN?d~{{i!PtyX#_1F9S~C#t5)$&toQ} z=v@-PL7s5d;tC-#a4u?2YQ9#-wJnX)E?(_1_nml}lbE20x~Rkl`NBI#&q|)KmeYSm z6Dm9qakKvY$NtqvP`w*JfeqRlli`OM;X<`Awh}XV>Q?2+SztiXmfDA%`}L5y!{5V7 ztSg5On{}$D-#@_i$d^M%WA(qDltI`Rlby0*yyYOsN>5(ijt;4klSQk7EM=13=i1n$ zz0X|S33aHaDTdPZGq?*(MXXv)juA&-fc{cS+HC6hC z<`~H>D~9^mI&nT>70Z@MbE^mvqb$WpZxEFUNf@+wk$dq4DYFgo-LRRP=oP=hlf+U@ z^U%1#QKSvoI@tsH;+%9gGS=?k1>;0;SK^(zc&C_#9-)hgC;6L4jp8SQm>eL7g&%8{ z<@UW#IevEv+8G^a*rcruoA#gWU8|<>XJ`SU8tT4`+pn=H4Cz>Dp$52%##p=F{X#~P zt&v$UK>C-A!m=<>oY@-0H;QdxwA06LY*PuA)8&pD{FAj_v0Ic)Syr2rEBIToPMlsp zfmFLZ*TvtS3dm3wj~t`3XYE7TEDJmBcR`2TiXIm%Cfy7oIXRpXcZ_siI%SienkT}v z{Pz~FY3?;)HC$~w6dYI3wum$UWtPdcCAo1FVZyi+342g@F;$-Tjt0ZMCnTctuyCsW zeirB*#!Kv=k(e`N9j+(fxVOibeKPiD{Ep|o6HB910Cp-n=cSAN)Y%63uBJnk>0YO9 zG7=otc!E)?^p%rX>mHn zNjT1L){g72i#^Ba+MZ+a!jO(x%pR0WOmB4HvAdsbyh-C)=aU1)w$q>mz$PDy24rD&R`hkyAPRe=kf>t%5#cOupsXL$ zcmg?V3HK!6w(I@^TwKhDpf;!|dE}KBd1i&6UFCi=Z78L!0gg#9(Rd;s8!Pi*uMXVX zoiESnB)No0Y4j>6-HB)RQackVpqq*dlih0)Ha=?j zE1>LSsRx}mBZhtG@VH=?<7=9YUGRHpB*2P-g0Z%eR=%EpR=edr#X8_o3rHdm5H*rd z&toQ%TvX?(GmT2;x6q|CocmfplUMf^tIxg&5_Yjoc3}0^4tfC5fJdQfiHCd{xpi~Q z2YbnN_0p`j;t4S$kNhM}T4-wDJv#E){ai=(f9U#wOBP#980Y)VW+0CrXAT$!@9o_pDx zrpvG(!_q{?htcVjnSSBXmVH@$>Dp=Wf8#|1soj`i%U!#hFRsFz+49+c3y7efJFYhG z4!*Gquw37#{(quI8CZrI1Jg9U>x#xanI|?#%@;M=JzdH9Yy zbe#PwjHxsR!T{cmg1!0A43+gzxfDCm5zzt*U}6vIY;Mp#IgNgOW zW6s|MX+X}AXxDnsEQBxbcr&>lBZCUFfpZ3+jEd#4JQ}uqqoMN~{DKVCw-4xMA0%T~ zFC?j!%N4F88JVTG3Mi1zTkQ2Lz(js}s85d{fmV4xcTC}3KHc9yKMDGtCH(Jc#RerD z8mvefMxaBMNmZDq!uof7W7q>WMyI}EpeQ+g^ZV30xq15ZKj>U-rm-~QhrKT(O;MUR zwt;1&B@j!Q$n$WCLCDJt?fww}MkHLoI?TV6QZX_Ewb;i?7qk8I*K3!aQj{GU6*8e- zJumlpIbUqj3dXc(_Cfc6BqQRwx)Qek%aZ^V6rJyh1(5d957?qz@`pcr}wtc z?UFlS4C~mt$DKXqqW;{nYoKsy2lUGt%1WI~SQY*aFf?o652tLT3(TL^O+ya^kGp=G zfUo%oh&s$O2+OwgTDKIRVK)Jja*NuY)-Nozpv-yD1=&LrM_@ z`ojdbx!+dtJiEcz6pOFOt7-Rxn+6gyb%~cRchBQcxWgE%RGX*pf z^=%(Q7lF#mL;&>cV-TctWSMw7sHmmp!_r6*_2zPw)l({e8DEVJ5@zH4n4ZA{pTjim zR^>r%uhN?XDj&iHZ7atfrn7B8sWy=y{F1%%TKvfN6a;jDAp7(`x$X=l`%uKd_fO#_ zQ48#=^#*G|4hc{Zc_31ItHgVy5f8=b2T*&53#1+1XQ&Yu=9i3*0{M^>OWT+{uMv9 z?IAt?dpIDbsanrx5y2lNILhA$4FXKHc5PIJ}3=PW`Okfzv@2ztd<@WsC|n zN_GfXpF!hiO8o7Y;{>;PuOAlZK9{)lnpYM+T9Y3j$^L3whTe^oi>A@O%8apeJ)fQ+y@KK10Q;qBcbVL zm=|WzWl4rUeFo};r=AMvhGR+(3h`}7gz%uDe9DCsulx9ze%Wd<^5>bLp8|L4WWW?y zk!u6aCH5pZEk5ujM#sXgqczLq(KN5NAiCq&`xtnsw8t*CqKZqMms1z;r#itc-Wx#F zTj;F{pU#JCg#yH;wHke6w-lrX3@=1KNxdS+tmht3a5blD9S;|Auu)_3-@633x;6lF z=>TxV)|^vt;XQhQc%r_ccYB|P^OB+4bjXlp4ot}0u)Y1m32PQ{{ca@e(h+c9oclph zcK**bK7l?9$f$X9H;;FhN?h4+^w$HB8U`{3n%+R2IQgTXbtQ8a$S6MP=2=-t-`%dT zVKM-gJn-thx)DEQsS2fGF493v@uSDkD|p6UI0wY6O~+pLkmzb(Nu-N3A!^~^2JxZ1 zWS8FwxXNON$odW4a6;r|H{)c7Yh!OCQO4s^=p7oK%!3H`NPllDv{*)o~Vi%k2fgj1P%$rG<<8Fz^B?V3I>|Al26J^}^)~Ks_A%NCH zP_^_I{ppjeu9gk;Fi0UY(NMu-gE-k8+y+H25t;$yG8HH|w6Ym@kF3f8CGqBXLY$xQ zd+~)Iry7Wb3t9KJw#fj{IvZ;ly5(Bf9Q=ETCRR``1XI@_6hEhkBaXCacn-yrD>V^M zvqUb9{PQxg0(*df09Ef(Pk9*Wf+@It^DCykwK5lR((U=ShK+|{Gg{}>mW;@w-miuK z-kTvbMv ztBi6NE%4!pVT$GkZXeyvY+bnF3XDtQ%X22owqM?j9w`vW8`vxjVdcE{50!C$1|8{kbAwUY2 z#u-c~*Z=TZad8l)8lWHEh4ck0Q8gYpmMt|RhYsn59Mf+H@?gO7xmvtpHc#|4@$;DO zLFWxPlx8IITg^U?1m#kgzoQ}!aFcwX*%RkuZ@8}i8f*bEZk=y|-n5pY(9ZfHIomDx zKN}g70c%DhG=vc+(S8412y9Oo34K#DOmf%$_C@+h=wACFNu?rHFzBJI;SFcO%{$WZ zQ|CA_ehTdJ*()y$nFJC33C~fe7I3;rm^c1j9r*fq8V=U#G*N~0|FJ`;KSaG>9-`3G zT;LKp&(+3{lsWN|P_U@N_(oyheQv8aKD#I~=|lfXncE0{$d}+4A4p6jl2Fe8(|(r1 zH#~WHc5T@@ak=WI~jn^7s%|gfT2blgnMFw0znN?_oL&c2oY`egk#;-&Z2 zGTH;P@RN!x1vr=%Vcf7OXlsPOTK_x)0_t!qKp^Q(zrHtv$6JmVeqHFeb|1D86tHpa z1rFm(7Dibu4)!frpGlbR5 z;ofWO`2d2<<1(1eNP}j)OW;B-xNM|qXFaoS%{=HLc`av~DZUJDPOqTy)KJ3_%9)yD zfBzzUA(V&A2rzZq4&+Zid2-zp#O+@y8(Kg3-9uh`R5I^S(uI}Ca<5XQYuY2?$D?4I z02_Hd$otCW>`f3g&=#;hvt ztm%h^f)72ol(>4g37KWk`kBqo+Jk5;HV*ZX%KDVkYe}2b`|kP^1#=3Z5z+x7OfU+9 zRH09lhXNr|F3Iwm6c7>PJx5e>(2M|_peW-76;0hPiWEy*ZQ=-0W*M?gg&|nrg_n~zV!Bf8}#ys%BFasvNuSW1V#zsQNu&U0V%li z74X;JO*${RsVXz>@FV><8?dmEHttV`MJ!z*lGC6gbILcsDQ=Lg)R8D4R9P7c5#Ut* zbhCfo;BMI-h&|b$o(T6;kmO{~wrV@ceg4^*k2n=56{4JnIiXKAs5n4Lso;tS`mi05 zrG*aTGjC-n*^!$HIFU0l?{|d2t_P7M4fxbhHaWjH5?IX*FB%=z0BZEWpfCJl1C})E ziT^f@8X#>&>QLo!!XJdm6c1^x9ng`%^fDwgdgo&1uzvipm$rR@2nHazg=I(Hs5|96 zMCDOXk_nPM@)T~pXeKrWeQh?(UcE+}Fy9E3Z8vxeLZCFHbAtxA5VRmGxq@Ubpz$$7 zKvhr38{1z{T0a|l3B}uZr_aea1CJ$KD!@ACTJRXbGw&fV5$6_os**u{G*#|JKuBfNK_{5V0(M6@=gm-EMi7+Mrx`UdNid4ocq)h4RnAZaOlA@2(_r2 z&`~8&sX%CZ0phtfc)d%Os=}|5PnZI}mcQeG5HF{cF7SVmXxz$u8)j3wc2?gl#;m?+ zRtQ2Jwlv$g;OzCD#&rv;f|{dCZ#2~E$286CPeRTP?2J zv{^J+EFhP-yNf}K^S;vdiNOT-y{Iz1S{#Nh_j-9Vj?2NoeZ02IwEs6zRnIo=aL@no!o^Fm56d1WYQS|CA7^qepNiI97X zifqf5S2vNw6g4nNtaHlQ!3w&fzgLvHT9c%}$sFWmAWN$;z6dl)LY?(`EC0q2{nmkv`Z!KEk4W1*HqBVtdB9PIySirMdC0n$+F&t{b~b^}iVdl0*!2yh`; z?JYuGpp)_|aJ(ba<@!r`{AKWPu1VESDt+s&qSJOs7~F;D2AwO0zn;??Q7A?xAyAQE zROcc&g4EiaWpRaar*3?Wklo@3BBMrH1;%(9AujFAm~oM8AXoZ~8AG}r42Mktco;4v zdg{u@iCnH28VyJiqkzD^fK4Z5g$M6A+)^BW5PK_NCb}-ZKj1ZJ`poDqUXzp^_Z$7w zYZHS(n<7AFxD(B0$D1A-x?6x4F5TZ|y~qO99^Boqiit`F+cy!F*fnOz6c4mQ2dI^7 z;US#tELPcY)3CAL(EZjHzVT{7$~m~hD5Rk2<54JMcigdIE#@0OD>)9oi02z8h>?-1 z^vk%=ZEQUfGW352jFWM`WbCC<-gQIx&%ZMLDt7XxsH=s$466EXlc>4p0zj#opL# zs8yUFKL@bF>G5Hlr8B7Mk!S)*>F&rDUG|d6RLDP?bfBVCLQPL@= z^U>V!k)aR1Ozeg27}oCkBUEr2$CxBPtxK|Lj!%{Y=;8G+7eL(l-9(b+rlhaEks z-{ZsPH(?_L(3qup_wYpAM4+q=*@Z%TYv%Jv(YR>o3+y+n$4jWqSNR%$!y@Og6ko68 zUPfDLd9}IAK=vN=d{HCJ%~!w^_5s1kppugDF9r_u(pJO9r-(WGxhIi66;1t(Hxgos zEBG*NrZUKX0odMXJAeCzuxY#oGnSue0(MAdfgW)FAV@T-rnSZhmmeHge|T$yf9NmbL-oH#8L_k&N||3C1NPc*bP^yL|x z^ju^?e9F>-yVn^D&|bz$d(Xe>287T{Aj7p1s+Pr~YdO!PSMlz5B*?L$6ZXMHIUiR~ zP&zyw+4<+B-D6YDv(iy_V2ccK+$vn zOy4umbJFM#{Y^So$M@la&zWB+;t2?UuJXCl0yysh`cr$C7cdb?AJnex=9`Nu=qrLF zyMp<|eI_pRapfZ!Zrs#)YA#oei$zo(aMw_w=|r9sV`|^u&$F%th^>*)HFTD3;L$H= ze62NHb7%`$SEFwdC^<_k$V$K}prHHU0a&XfZZH9`-Bg_oITy}Bt!yYzj%m_+=(V+D z)$^WLKO5x>5Nb%X^TRMkJaO=|LE8RdonqOUb>;YK4hO}f&4d#SBuPcTZR*sHmg6t@ zGZ-{S2^!zGw@dr0{Hd&qAcHNhWYJeq%daKL9;gX20YYvCjC_QyXPtMzvJ_%+%x|!fN+xw^f2joTIEWU2j#kOx5hVIwut_-__)M)*2?;9^DFybquW$Fyr(s^7NhL5KBiP0w}oHxgill4CB`90rN zafi}vdkvLRQVbiMov^ zjn@@RE2>I3(_qBGVaZY_>_WyYZcaNt&Gqxv*rT2CsTn5g8DbrFTqK?PtsdQ<2<0S0 zQ>!hmzsZbu9Tx06PMt0tM4~{X*VkE(w@Q2WL3eBzQ2RC*END!BcC|6Cr}6+HlKFB& zeYeK!BG5~h@OJJ`vJvg&`M|(`^=qW*6n{3fHN=4ZaZK#q%^dHN-DG)uT*xpZ!R8ER zaB94}8(7R2-Lg&m3Mz0lvXC*95eKx_u~kLjR>Cw4RamsYP=mRs?+2v68-*K7)yer2 zNGb0MLzJ!ot`3upvk1p5Rf}+>D}282Lg>=SjnZp|t^?cl7dz5SyXK--SG?)yzkSA4RH8r0eS2V{*qL4h%cVj#8GEi^=qEBEjG<5n#Te?{QVY}6jgJk06u(qz9 z7t#Z%$|IYHoW%Xtuw4M)Ww4!>8+is0&I)<|xA9O-85mdHC=LAvRdK$5E_}?Rh9zGL zw}Ni#bBacC5ZBQa;hnf3nD&3NQc@v1%tGs><4K+>9Z-@e!UJ0_bXKtObyku?XA&y} zhp7p5X2aWPn2xGBqW)xg0<^YzP#7+iHmQj3Bcrn4t?bvC1604j|iPo>+=^v-G8Z=L(gI9u(e7 zZt47$DImR^?-W8{G6+s3K6IC2WOJ?B>QM<-F1UWO*Xk%1carjfX~d}srrcyL_$?#Y z`*(-~0e6suKG;b^DLUu!;w|Z}ZO4Wep__;;EZJjfadgXiJyvap2gJfzZtqh4KHf;5 z29ETJ&~I=j1;oZc%HL`029K`iBTU^q9U7@gW4!uCxAuCH+rmuOeuZ4qce|;INt>Xf zY^ctGbvbVk1rXTj35&)BR$}7SRFM$&=cEN0hGrr|8pFWp^9(XUXm@<_)%KiZ#5=(t zwFNXY3+}zK+FQ$g`g#^Hugn{tR?H2%5(2Gs6FtwNfq(x9%H{+rFT|V0)PXK=FkH2k zbKhJ0xnA!DA-;6E*h9WLl(Nx7Sy21@v{4Jp1=@rv*J1^;eds#oaI$5xu#g186F(aTv~L_tT&;lsSD$eQXWzBPOMg=9t6$rPu`~mlcdHRAez12b-ju)Vb>+D zbh+}l2{o*|F=xn3@}@vPOK35e%pasd7q%dDIwXSRwRTk;){o>IeSgB3%y1 zAUKi~cjBKSuKrV6L9FKAj`}YpiDmgf3SOL6h=KCfqEV{9CzN}Xs#JOcW%8XThx_&z zfMwgSETiGymOO)F$MO58To9R2yKyQLBOX!to2pbxvBd zx8jai1YwuyJZ~OMUf2Hm`kXqTzzG;@!Knqz84JRbG}>ihZ3IXZ9#5v(u7-sGz#D%| zp366;;pOR&0?Fi4=OK>;vdN7JW;_RfQg_kb_%S&cXvl;r%Mr_6gwlIeXtSECVi#t6 za-)o5RmPa)&wFy8o}Q|@?SMILVR%m!u3Rp3A2<5Xuomr0`)7VdO$D)zOS9bz%Dg~9 zj>Uh!MT`-<&p_v?uxIl#Tw6;v@Tt`gs!ncb!hF1uv6LuBT!t{_X_B5_!R9;B0SKXX zD6MOWp%Jfpd6dk^PR^Jsq|%%741xM1!Y+C9h7HYgwitv-;`; zNxKbHHA}PZV3a~Mlo+U+#qHM6zst(KFE*ij1X|;*nErqD0 zN2GRJxbXxp$9vkO>qP`0V*8KBjPV10@dy+@??rMr4(xH4%7Bb5e)s5LQXH*CQLwlw z2lfWUF0)0}Qx;V8-{P|@F)K+Gw`IunJV8!voml_!MGSWUi6Q7xr?VWPf)Q!GGPAI3 zkr;_)|EIgvMVyLd$y3o}n*r=Yt$jTe0WrKCj%2a)BPnx9;utXl8NYcg3v*{;y^GXd zltM+4a@-l^OgYV)0G&j8l%>WQt+m*C_&k-Ed8_=HZHGnU%K~Q@bO8B^CXWZBbjy$q z)PN#ODsMFDpKwgdTX%SG{H7-btVm}eMoXgZqbQjUD5JBg(zsdrxyUru+gjcQ<2j)ruB)WC|j;}UaJw2$4rI&gB`R;mAM{N5Q2Cm@7E~5MGP1K<8`uo@L zM`>&4xb8^uB`D)*eXS@Z4#FZ&BdQx-4o!91ytw|~fQV$@vV3r2xI(}AakE-Jo0wdDq0n>Kn|)gx2RWPRE{5vR zTjzQxKKgniz;;+6ZkXo%d5*KaEX5Kz-3U@alCC7<{kU7gfS)a$cBVqjn_MNQPrOr4 zyG_nc)!q?=?)ke?N;D(p2#US&(v}#>+kA}bIVNr(etotzd8ZdBBHuq@en>UXIfQC4o0{DY=3Zyng{E1+qKS$q^`x|dnR1y`ZMpM zotnc%gq{FJlRmg*-}@UquiqFBrm=e<+?vN)+6~yh3(Qzu5?3xkS`&*i25_%_K+Qb6 z@?V*!nl!{g!sm(kML8Buegi{4qK6F2Bd@eqyOYX+F8BciQ!l<)0_)M%+S9Gw} zYP3RVu-oloGhBD+4vp$FAPKL{-S$x!nu_jTUJ&i!J zS|bPIl?#b1zA}Yi_#(zms6m3NY&5}qBn4sV9>`>?3$Iwt6jC8Gx$PZ?dAx`e?#N)@ z+Xwv_sk8p=U6wSufN&OlP}5apjO4RXX(A}>i9VE(1-aKD;+D3QBLw5-xXFH(Lc!Tz2h;T%Y|o8IQ=v=OanIa z>EXpixjtqx?$x}>;)Ga1JCmFYX=-`JsFuKxRXG(A*Qch#!})aLknPX&q<$zM3d$zr zo9S_ZM?16Z7_5pN&rnZNjeYq;j=#~Hj5HqMYuZoJH zkyVrb3Z>Rm_f_!w@-}HKzN;8r1~|HFa0k#em&DJO2z>s(bz}j{+7z|8fNzYJ2Vh8HvQx)S%_rY2sKJ{tOLpZ}sfA5I&EHA3J%)H(Ejb z@n^Agt--E_S(juAqN;AM(}4INEE9me=^-=&20_I0c+6@dppD{n0e4yiuLG(kFrIP> zrrDvZFtBG)Xg9eC+B0&0m`U0td>2g?Bi#~mQo7K-m3xR02eW;KWL6;U4p+ zBP;&>w)cLp?tZV<7H_2!w+$rVhLrCz0s`D9V%-j&4c!~ab141?r{cSk)ZnB>e^cKT zC%5GRm(NeW7d&`6QuAeCA;RT*fy4KOC0FL|^5Rp^#}`&-ErB^=zp(jEAe`~bzXm=# zkV1Eo1#xeWykg3^ZGH?Wx3hdDB8o`8?rZ)l1qh_EX$umCg9B#dT4#Hp1H~0K(=M-H zCLovNN#IRpJ=}k)9b_0*s5>0}@~QzeOP7&+6`XUm0ihlRZrR>2Zg=9X_D`v&g0zt5 z14=`oVE@LgM({!TI7P4ytr_ zgK(aZm5;JSxJ|)3IB&+#U3wl1#9S6 zfRLO|U?BpXKq%j4p{xIt4J=+Hq(M?X9}-q?0XXIAvsl1VvIHZw7}kI{>YP8wpqRF3 zdtsJ;=xnIhQr5Li>wy-_L7MUEJH0xu0zMSVNWFWz4_(xX1c7<0G?kd?@XP*=EBrK; zM&VkACqN0(4%QXcKn@k?LtqK=MRv_IbgB`>6ZTmJ< z$M6epf!B4=u{V7Vyits+zu!xiMU_S2lgVMFC%|X$eYN60u??_DMxd5Tl1`W3GgkiF z@W?BXfV)Z#=!11Z#h3<{*QRk4`j|B6hmo=!B&$#0)*d^-^ECGiTjr&=7|XWoZ-;mW zX^(>S8O4=*N@xfB^@C;Lm3tyE{o%Ji$&&b4dXM)o%L_YCB|+eNh*g`5*7aKrm4TQu zbn^5P?nOX=CqbT(KX>@U{bJxZ$j~S@3fPs)l3(_$*Q@eUB>A28;Sl$l)Dl&!nO!IF zNMDu*y01bIBPbgmA`7OONT`s;b_PK+Z32FqC5T)_nmZ=SbC*)BpnzoXPb#(i-$S{L zovh_i`YP;B{RkU_RRh1RAHbC#3ZU7;)J%oYU^ovNL6pO{SB+C%bTfpL-qbL7(enSxeDUO1J8W6sF^l*6<$}VJbuO7J| zKlFP+e`WonH47auUwE6CT053olK|4inAE*e)4)w9V0 zsJg(`IiF(kmDE-+L8o|mZe^NOx8+;&fzJNo=i}|UA3!JPA+w;l&jMn&Ch!WWF!Pw> zK6!d?cRv)H4~8!Oj(+rVxy>QG>s0xQrPlla_=0vI?D*Xt9)X1&c1N>Lm&}Q)#Ld{0 zF4+T*`&2L0=J>1=?-<=<>qVfwLjh4;N#i`$@v^@EzJ($939S4;PU$eu)WP3!aSv`} z!RcbjKxfO!BpSp9M5PTpVUD+*i?NOtb7`(mP#zf@u=e(t{e*`12kPR_(h2>O%kXDO z7q=)Ne`=QzXjY$`6dPPi1v)WqI7i#q?|i?Q*+fcH9dziy(6Gr8+4>L$ypv$i ziP#hj9sU5AS>tAxgQRW;8>7*-j8I4sw-zRMNIO3GwW4e1F{AHdH+%Y8&#g}9k@T!~ z2Nv>i%i&|m+~k+{N4ABesds2haMsQ&%|70@ya;NAmslU$-}AqtKNx*EF3>Ss`WfJ` z&Zfg9##tZSq5@_cN~RoMOK=R9-Ze@aQp}r#s#gyXvL7&zkqdSoXIvNtU9y(K@{qMf z2T=5#r58&4DRXu#iPbp@+A0^Ei-}Q>snxKHP2v7sFyr?99C`P*xd?0~-93 zy_1mRCMJPn7#z>62^{zKdpFy9Ij+AcefIj;m(va2B)SEi#V+|B4p2>6f%rDA(frP%5!jbssd(W@5qw*!g7avT4);OW>Tpm zs!+11&so4hc-(o9u4HmY-{4K$XuY?mXPkXWG1^YNVUhd+}nL1DMrZAX;nLCL8H zpw;oAj9&PD9oJtkEJ7Qf1=(aJk-^);VdqK}6S#Z8q)va=KB}C~!IH6#Nw&OL&T)6V zr`mj!6zTqxrZ3sI_b_bVHsz$$J-dVMja@gkTf%iN6UF*FG(}Y13va)V;DyN!UM^X1cVxU*^!&Fy$4_C}-`(?Fa?#}TFJ`9)ZTPcmS6oUA z8wPZ3r8J>eB1T-$ExhVXF$pRVxC~wU!N=t#8*d-WF3cQx5O^#&%nI*f4BFlM?$T;= zwCf<89Vn|00k1=YC-?}xKJXX4xpe_%J8@(CpcMgM3X!8{o>B6>p}G5$N(M!YV_&E_ z9_~`dvL*D$x|TlQ$%~Pj^OkdM>1WHR2^6Gb^I1w|EO`u9oylj^FIoTlriZ5ZI!i?2+({4kUTE zQAvVRP)p|Sf(tutvtGy(gRgu$3DjR8*LD5?(1ja0s4j$4mw^m0-tht@Bv1~;Rho<# zEie8$H(LO)Qs>y|JnZa_k=tgI#KU+`m!$w(59>BS%!CDK=8T)7&VfH!{Z=W^$(Ykf z6NsjwZ?R%oRF6coZVg+!tw-Z%`^TBn?D~m-cl@4WjAui*G{6lCvaKmAhjMIc)~aO`qSW+T<8t@^!CPF zmHdD5BD*zSemgr<)$i}5K?*g8^1%LxBg})&K=E7<*aJvfF6y1##A@9h&7AU#BVL+q zxezddx}NVpafuX9Q0;%=_BUL3?Rp6Xqdv9|Boet$!5;dk(ng8CGS-ta$GW2#38nzN zyw1km5Px>y-{*}jlL(jsOhp@{Pp|W&gM>wrx%0TdO)Jl1#eLQZXeVXcO{vHO#a-h5 zScxe2>dD;}kF~=Sz=pl>-Cq3toeHs{Y*xe~d9?x=BtgI&!`mhh(>8=R5x+my6ay|c z?J&yHo3YpwP63>PpVw*oPOZ*1LFB3{<*9BtMN`-`ypTIV9R4OZ;)@AM!$Y@T=1XzqyNrS8)of z@;p{`Yb_-#d)t@i(k@+_nz z^xcmRN^hO5v;zkX=}d9~;x3?oeWZM$FMyzJ2C?A+08q+|kHGgkVqa(_{l@#>!$nCg zU-14&di#y1>so+Yoy1Cr{WKo*HOe{rPMU7R6WC|1jcBN?MN-@@?>=p} zbG4MivvXeqzD^G&rbnU1%M*8-XV-xR-S9zu?C`*U6!O-i5*c~2!aA)E$knV%ng7nu z`rNtY!7>CRKuGdYDOy$GSGZbqQZwTRNfd6AQwfkgBB5mr>SOz4vfH-|deb!EsRY22 ze3tc(I4tlu6kK^$ZOC{RH2=4%C5-&2(Yts?0;A$L=%V-lkwx2L5MI>Dvre0=CKGD; zzSFC41-1gcR*Rsw07t?yz8}d(wiJgl!S54UxbeUoG&0RpExatTmW8g-lA)1BXa#bB z&gsZBex1>$xau#6Hb z;bDpcC%lmqlU*4P{C6!{)^tqV337=Qpekdbe~2~Wd4LWM5%(}pLoT%Tm8eNJ>kBy! z)*jRYOYFvGXt1t=yBx^uNNCwQq^_~HnpRMN?6W?1rQRBB#`f4${h(Mom>FSMc-TLh z^=xy&cN00!J#QCoJwc2M=49YcYyyhXJSeCNJ;p(xW){wCEM@NS{+~G!mZJcv9Exhl z0W?9xaUuqUl-%tykx)~D5FKgigTDc{GcrZL#^b0jZN|MB=`74=_`HN{U=S<%pXhoEt2Pm}1-{RDISRWpse>Q}kv35e18E$ZLE-TdD5 zgs}F|SbmZ&uccK1CL8EMzY)5gKJ~Uh-wXxE>n|d z%Cu09K)o~?q(G-r^*YU9L!v)}zZh5^!|0UZ0cPvw5;qqB2f_fnc@I&Vc486{OK>JX zSQ{?HR%rFWQjcsDi6JgnA)PIlwiMh(bLB{Rha9zDj#UoZDIL6pHKkAj`Sa?;;X4=| z2>d^`zB{1izWtx0bIZv{P9mZR4N62rbxL~=D%DAZq=`<8=tOo~ilov|qCtC*P-a?` zhC;NYlr4Tg9b}|o$bPYGpnjYiI(sC9FL-5E+Pwp@re(m=(%ZgRtgF}!GY2^ z@Rekg08phun}jcgT+!|PuM(|-O*n!VwQ(E}8Rm5dig^Q9j%#0=0op@x<*}13kb{Gq zL4@r~lKV))13CP|Hxbx3IH72)F*-}wL9eqAvXY=tX{mofq#R}JF6YbKtOZfkh>dt^KJJk>*OVGqm-U55;@`Vx~>+2GmkNf^Vj z9Zr?oEj=A|MuncN{ur+9Jl7BrV-A}&tMOuSW_6~e&Z6p1VsJc6obe9((ec_d&_la% zzor3;Ki>J~rXvUU$5|vx<={5eyTYk-bx+VKI@zRP*yvYmM#2ydXmkuivwR+8dYb&~ z+2fkL^jZ4z+QDGF!t&?>VCC0%G-t%_!l<2c4g&BJFrIlYxJvk6>E!pPZSY(Emo1GQ zNG`1Doxqj4P$s+0nws+T~;0M6$cH_Krc6)YyMFf|s?FyL&=S_#gxZk~#uq@ak z)94a1&xU0Cn%>;`XvZP#w4O9LQ1}+@1$@QpV93m<))8NOcMY|E!I7W)YC<*n_{m{4 zmvYgP$Z(BTJ`(rc0O(UoP;`~9l-h4>UJw;-N&lX#A>85LW?52a+ z$P?Uqd85aozCG)g|1N;Bl~10*j?dYna-@muGGLJJd!o-U-p(?sIN4346LeWT1W1z( zXGbU;xs3ob!<8s0*Gp(iV|J1I!oa18B=r}ua-H7wrrC~>op}dwK9RIUJLp8t%vbo{ zGRrMF&A(U)!<2#u_w%~*WILNUf`JF;di(pxPT2w`G7GCH&tC;-qE~@ia#0x}Cc!*A&ya-*Q>bvq8l}1aJb*=pWH0EFR92mLH(#W;qKn>l_fpRJ1*y{+b;d5l zp7IK^i(U#Nuh7C)fF|JuRzKxSaPbOayTBl>rOo$1u>1mD-0$5Kg``r^bE{q2%DwL{ zWA+W8n~6~MvyIB*HjEctL9->$gI`3I%4&YCb2#8~cyb39%DOxRGSFSD_+!Xe=$g2w zpH5ZcMxDTlL}9cf8ifZa zVGP3f4oTVuZ<;^dv?E#xH^KmyHi8`KpEqOa3=Q=FKFc`reD15+zgi;~k79P@+&ISo z+erpAeftpMREK*!xH|nn8Y$`wc|=e$MrdpU1*~;pRea<|kKkmnkjh!!vjg zpHH}iQN=wWHlY4X24N!9!s<1ejb{?{qIzWKdLTkS>pNlva*IMpe2#<*cSMjw-kk-> z{zgQtgfD*T$a7F36plV8U!OqQQjaZwoA|QMO1P^Fic)kpp7_0%g_p3wQ0Fgne6hwF zDqW5C^7Ui}lAy+vHs1FS) zw$PW+7sK$g>=%O5=ndQGwVZnpdu#D7D)lxHA2yFCqK4gc-AmJX{eA;InO;}VrganD)_R61E z8b1h~=fCPwXi*38g%h3Yd0)eh*9 zBbV(wvph5s&JkzH>zy61K|UONQn_{6SO+vxmmM<|L?b^E8gMkI#N0!`fqwdX`_(9m z6N{)So8>(FoD~;4aa_EeFqkwcNJ&x5OK)NV`iT4#1=>#$j<|9o6mujsV?X;6O8J~_ zv13)x0TG84qMde(8X*y>Y0MT^TYD=`GyQNMG0TK0z-YB z(#~xdZgZ1NW@6bJo)Ga(tyWZTOz87KaceQ*>75Kq);dEsQKz34+O3Sn#DZX7C1te6 z{j5|&XTT{qXL?G~QU%ME)t5`p;rrHA7k|+W{uuA5e zPNdUj+D+o@ln~)%+BVVhMV`ibB}wnse%FLTt;>H4YQ=4@X6bEwlefU5Q@_<>NH?*P z-zDbC%UE*{k@>OyDqwc@DGD@IwmHpBS{$fkL|gDkr!kz~P?0`RgNphFDrZLr07ZV^ z!r$(nPXGA%8vdr|?L1#LDixk(PQEv)KdHy8j$EADH+WNaYtj=V)aFce4S^55riRV- zFr7(U`Y`Y8=LN|XuCm6|;*4pL3w?uKwg=|%4xTSosY@+Z>1d0aZj5Njyo@8%V8B;<1**QkB=1${r;Wh2#LmNU zz2Czq8W3mgXA|fkBJ0LQv{*sxOy`cume7|n zww1zsjL({j%jq{O=A^gn?Q5$bX|LG|0s)V?L~W1I_`bh{%c(&qWOuE5YfaIbN*ckNQ#Cl6O#U}7F8`1HlyyNAY^WejUQZH`xpA`N(NsPE(%ukxbhsC=v|Mydr zg7KUg6EmsRGbA)+#`R}`(i|j+wj&tpQ?h0+5+_Nih!>Fg0}h(-l$Tu)`4G$dsu}10 zeg_lS>j-t8v{*mqwsG8a4cfFM>B>$>OtJx)n1SnQ=tU>ayI+XgSCP(LnIq+AVHCEI z)%0oF)x@eGpJlW;N%aAFb!|E=K%8<@G|+KpcIL%NF+Gj8IxVu>Ksf%+$~b>b{%HyN zfjyFT-HVZ1?0D`%$))e-^Jds?zPO+>^z|L1bq~3wC!Gl@maFrZkT&^`F@pYiOMv!t1P^ukIeTr3eVH-6>2n@~>S^Jkmq&Cc-J1jy zz*%#5A252JiC1&M^F8%Kf&Z3$pNy}HP|ICXTzcrV(@ALOc7oe=Tl~^x!A~tkVSzIg zd&{K3Aj(!UuXTee>-u9ng6jT3gy7lt{p!Nxn}#`z;mVv1H;nE)7jGfqsRLYzA-Top zekeRZ@BZe9<$9kdoQW@@%+?=<&2in>p~c{tSuUCLqdZh6uWoeu1bHvE*Uo?T-0lJi zQWn~wha2_(KCuvvxSTr)cEhI?5ruh-P1X;gxsa>jRgd=$dd+9VH;<}aAb19<)aUWe zCl|yT*zv!L4-D{G*I`kO#+7fnp}KnG-s((MHjmS>h>`f7Fq98^Ean{GqP7 zv*LQz)6|Dh?K*Op0c*3!Hou|A770~Sp(P}~GeGP_Lk-W4A+Un^WXDx&-KePr*$JUe?B?*T0}j8 z`{I#D+Nrl5>$NIP6izJ=+$zW=ceNnJG|zc+t9E>k4ozIdn}lg5TAAB-<<0=~=s1yT z6Nbz*6l;Lw_C>8WcJqaQNZaH|ig|VqE&KkxF|k&%7&?S5ptd~)-~iXryfkYch-bg25kn`!U`~uS>lx<>+v~`n7>ExmQ8>nvSD>&@~oBJ_x2dq1XNmQ6fMee|F)I(n^eI>HhxMn4^>Ti@L&+Fn?zUaok_II9`S)8B~|VHHbe5=p?Oz()6SFRjS5mgP>l$yc~sJR0L%7P9Ub&e5SPE)LUbl0pMp<1EZ@8&b8l7#=JLj}s zd-{)$vNUSlaivAHi|+5ruha(=GXT#Wh<%Gug3SGWHiZkX>Zz-{V_rrR|G`HWBZf|{x#15TjP^d+h3V@jzor0~XfBBnEsraG@WPz0tIch96Hv@6xyGV0M+2O^LvlxqB7V$xSy#Y@B>--)A#af2h%x+nx`uZHe26jO< zc1F`^@~&9>@k9rnOm9AQ!(=NaD>nJm2q(pUIIEmDHt(&G$=M&zuLcyvzCZa7{47DP z>C;a>o%nGZf^;{&N!MnTyw{ug`rDsRJg8+gF>~AIe1#^uB}I}wZnDKBrM8acnQMA5Zazui0CqW# zJGE;%X|x4}IQn)MLu1nrYJx-cCc2lU_Z?qN9xt0n4|$Buh@P$iK|%BVCFx82=gE}G z1A~!y`_OIkWgi!Uc^^Dt#B3&qk>Gp|>5B%bFGRyz5QOHu?!a}a*OZIQYg;o=4*K=4 z9iIWmc`S?p!jB}96TiRARZ9oIk;4q{T3^P3$@+>5`B=;ruIoI=Uln}`9oFEK=X z5P0o!Oh(z)$k9JaWlUm2p_kvIPR0wvt4pMl#a1tt|K&iK$ibsTP5cRIiNrVMnzsc1A)rA>rb*=# zX1*{gF59#s5P-!&q|!M69Ef?%I~HX+fvvo#96W6@Gd~OQ7Ug7KQx&JlD53M2gOFMz z<`9_FE9gG$fEb-EINI{>ezGJ~2GFxCARr}n2RCxE^`{&{Kk$u+Cvd=)E!3E z{S9R(GW;Iw7}W0>Nr&K8rF3{_BL)VkS|R1Q0av5g*gp5(ysuwrW{S^M-+Cd%tgMxvA6!;~xaFmefS2Cp3 z7=01PE6EKbaI3m0na<4EDGT+O6|PA_oc8IDCs!t-8#6$c)Blc9g|GH zSx)kt^3UA=ido#)w%^a*bp3rjv4=TPDV7j+mW2EjB58x@}E z9(yuykt_CJ<+3+u$WI$bkv_rg)DO?2oSX{3o*&)t$K)n=jZdk;VQ`-UL^cz@st)nK z_D{p5T%XJJA9D|gY5GZoo?vkhKABqHdv;`g*Y-t$;B){f_==vCg*M^jK)<|p+t#Ij zedSY@*hwy+FY-a3K~CBBd_LERJe|`7mkePZ^S&b2E$6b%*x;5CF_eBZYJW22;T6aN z2TV9H#?g=#hDRPUsA##^cJ$yG=6e{jEC8$bF*)9q63@1^Tp)P(mEjBL$F%@sRM!N` zWkxi^5UZsN@G^Cr=2a5JJR1s$J8FLkX*@&?yYr@PegqWAU5dkQd&tdyqQgJ#GGt$a0^wV1wDI1!`vxHPvM@b`1|;*0;$( z+Wuh+zm?vbNPfj#?uU^ZvTuO70ae%{yQKa3H~}{L0R-7#dGcly%EtjQB^g85V3WqD zxeIZdX6y-yu!sTwgp**Rbi^MQL5?Sd;87$Ly61$YZwTP#BVT9Fijzz3tO8L)B!kl` zXItO7KxV9Biph^!1@D6d3Y#Jv2tFS*WF#5i3;mlT&TO4%xm)vouWsy1^)AXUwy@t` z1Y)kkJ%0GD=|b|%7_~H#*Pfsoxp%bsw%1$6^;-nT%_$u1sKhI_Ic+S*Y%c)t=?4=L z?tI~JxB^DC_$+t=6@xv_te1$(CWc3n2b6*^N)G0v4dj@^KfWfhsiisX5t25F}WUc;|zX_t2;bdG8K6wW%3rY&MPvGr%Gq z+hzA%;xQhjXBa-5U<2*~xwnbJCTrI9E+8SFTY3Y%pki%-JAT+~%pUD?6^WQ6v9~Mm6Q+z#D@&hMe&-errz{dn zhF@mgFW56u`j2lu4JD}0Nv7X7C|Z}Dde^ZVM8WgO9T!hXOU7A^;?(~h13T` zk2VqsC4OZ5DhWPbv}uN1%j-+>kaQ4=b!emW8ZjUc#u2yqM!W%&c$M-dTG3Y21wXUVB_^# zl7;kMtIgW-D7p^B_S5gzjoPetfRBSN>xQUtT>N{N@af$FI}Jazr&%_<0a65sjJW!Nm)ITlxDgyJE`8A7ra=y1-|N*W%*G5%SDx>Dz*3NS zXA<-`yTtRYVRhX!5m9P=^MG$#!QRH)l>5g^7jf%SIxG?{^!?ay&EM(Y1G0H!ud}8U z8iO|*d3KDCR3f!<6Uww))(`;{ZmN+%grET{lT_fN3SS6+#;DoqIl^sGH_4g83sAwvbzAKt)vA*m}_} zC7l$dB!d*8bhLqWl7W4YGl$FL!M-nUK(~~Q6Cvl@wYpx2FWO~ieXl_jyhZahI5K-| zQV7!Ax2D@D`*wW#en{CfL5Jm)2Wt*O^LMro^XZ5^PIk4Nu@H$l8MZk>&I$xIn!=kf zX+eO}CU`zG8uQ)Ah&*QNIfQUq>~8|&VUAU(!(Nk0KOgLyEz#SFUb>zPc0h~sywEv` zWw%1s(XWM&bB$43h(9b|FX}ZItX6pf@ssSt#7eXt{0oGTlvXLAJ%E~I;3@1s9Ucw5@+#yAH^TH zRNw3>yYxyRilRW1Oga#pH!X>N+aO0hw#kL642qr^CM>5K8BE4UXCJ;{Bou=mTjQHp z9j}gDHX)k?hifj{v?;c7QlT%)k>)64tQ(3_LguC(n#SdJ@P!N>!cQ@VhS_bZ?9#{? zEKS2A^}AvM$$4@f^0EP*XOARxhP=$j7f(!CPFs^SFEk}<^PIC74OGt6;8La2(>s8K z?f1Xr#xunTf{Uf9N6tYkOfs34{pbE4AUTA{C(3Cl-aCG!F-)2Q!1{pg4_P<+zSvk3 zFQ~{-Fp?R@bc;eX@QFiret2wR?)$k(z2kgrj}nC;*uMq3(CTk)_f0bZBD^f~QYibFYGS@uc3$}yCaX+@N^N`LaXeOA|pQi}>zPqsV zSvpTSZ@*_%4{TSbUkv5W8mk_aj}wgLY9uDrE~%}(j^MKwbRWYBsw;{Pynp<@SnYMF z+m8#%%m}*jHRP#XN?Rk}4MVxB311>6uL#X$&8#$C6KWQatQACAOKT!>B{Z|WJ{q(M zNd)DGN5s*>l(!|kt11ty-;qpzVAJ>FzX2apaoJ4;O z_;g3Y;Qc;saZA9-m5U&V6}I;;@^Ko8bdZU|C}7uVvCJxXLs0vyrrzCo2*}Iiu5)o~ zkb_{7QWf$ozS5PRXn0i&_p*|B^pSbQoWrMF-Sn3yk1Y3(91w=O7=&7y|2{MZD4vl_ zK1#9?5nB25KAk-aM~x|>EVftA2~@th{QPLqHi05E`y)jIIQYKZ9T(lf^ShA&eS=_D z5NN#9B!h!`y83P#G@EDY{FO9G_l@}~pMxFjj&#_^=_+^z=|rBD_SCU%s7}{-e?zU& zNBXt4z;S6x*&RBqvcSfVRpq+l8Z2yeM6iWXCOIu({)_;~ypp;6lk+!6I~vfwVTehw zz`YgO4Lq4sZuFx{Slg`?|1nH;4Kg2!d?OB(eidU7c}gyZ8d{8S^Cw1#iWrgTmi)^^ z^5boyEomxdzZQeNYq~WeuSieVeRp)|p0U*2&rrdiECl!2u{pti?~lg+Nsrq|ibaVz z2sYkP-NuR=-}*VoXK&ARYvP(b1@^S?8%q|kp_R;&xdypq*h%9DhU0&*nu2@ZS-`P+ ztJ>Xpev(>km2GQN#Vhc~Na7!Pe8~{2w!e1i7sIqg*XE*$8-Quo*FeR|tP%Eg+T6ht z=cobLGM$KoB$R%r6q$$h#8&L1>DW%aAUiY=R+8<7RpAN?g8J6(trn4y&{Y&|VweDcV5%*?!DPnJbq!mu0#VPEP$jlK0foa2 z0wITAeR_FgpKb>&Ua$t0#&t|#mJ;wEJPdPMqlgUaK&ADNmV%ww%J;cqJL5lwlsvZ8 zj>=ooZc47X+ulBYxIbO2#a~PxrOSRdbE6Max>99x-n!rO!kf8BDwLxc+wxAoW@?37 z@9W0GJ2Ml5nSF)#*A3Y1IKz2NI~tHpkB(>|`}dUT%9&V|9rU=K4g4DC%j5lJ(Hie> z^pswt6N3zV)9dSZRj;zg0C6z}u^8 z#mC~l7o?4K>#MmA(E10?mz1^Blvoj!i5cOK=bb?!?sUG{c@{}$LMUo=cVwk(o6L?L zwlzppi|wVa`aDCaeE@=*4=-JQ^x29-B$;1qz>W>qd_K+3)zCI0!t!Lb_19e0htzAp zi0aXKJ7CJMYss%rOjxh+7-4^emZj!HY*LDqzN1VLDTYc|hwNrrTW>e6r|!e8XtOn) zWy)TTt*encOz=8!?7GARTk8F_t@Err;Cbj-a~O~twdul&r95nO^m#KyaV~2Mi|?2_ zTJ(}UUs|My)s-IUjw_hi`ds)vh7Zl~+iWDb8^ZDg@3WfB!w3hCOsv=gU9&5Dt{;~X zLv`^>y1zk=+VpEcDyynhD#d+6$#{)rx|h<>6#cw9DX>@xhnUMn+2s#5)6|ky2jTGe zI@?H;vC^SVdc=$wAnMKnsIfI*fgG6(KXKtNV5VEaWHXn=;g#gWuA`#DH|KNHL!d7f zH6p`&g1mDoU56&wd&)mDV=rU9{w(P$)HlB>QO38ZI$ebY4?ADDVXLt35}5b)jVnj|F|0C+M00G-^f4jtRXL3&KugqDQxqi#C7>4z|f_GIz7#G^6r0K~f zqQP`rR9j)1wXlg}2pq=AcXzHY`=?gpQRyBBlJG0@%NqD{fAUg4E2ARK7?xjB zRX=ZuAwxqR_*mr9`f67?$CFFzZ-v5XiK5g7ZS$#5IXROk zcwCGl&#OOgWvdp}D*hk+(w)@KMIsc9jcTK-kJGORQf^0FX=_=UpNVxReRuNMszSqH zq5+I3sWZSwm_J_SfnIZ4MXW?~x5DEUht0XlV(B*xTbD-K_(dBL{{)$SprjrqP7Nh< z$WcenDBDp>qi|YmA>@k_`u zFrd>i@;b4V)k*4+K@=AIG-JW^DQw}z(bJt#tlfkw#5_vgEBNWY1JQqeG^DAwNMjip zB)!!d2@FyS;RnayF2*IQ^aU%-TtGlPZrZm`A5QwShk0(Ho&jQci3~R;^UIfhcz-i% z4~O*96hzjOaPyj>huLJ9v5dbSdo^zP;FKZ~dy9b}fdou~kRaGaXx;w50gi!y(T@Uf zLRIioE1o9#C_+4@7(qjXfOPH8*LFX<*uy&9_}{gie3F(0bFO?%+xpj!@R7iilCa3J zBB}!j_C(tTt&Ya<5u>2r{PmU9(`-S1jSRt#gbuofxoaO5ZMozzz@dyQybr^rVliK~ zv@oe0nFGrCy0ia^%Lf`pJm-csad=PR65hYF&Ic&O`VWH?k2+0$h4~-YO?^4pZlooI z&?r~Ra`V8H`Dmqmr@Hnu*bvjiN_^+}wGL(* zKuN>8*?1U!rIm84&LcG9E;l+5oPZM76aUOT82wPqQ{d-BGCXj+vqxG*eMZ~xlN=<9 z9637~qYGT=*t6YZbPq0s6n`ux%}CiylKzSp%wo;vxnGFaSU!EhMlzh7_SX2GjS&`B zCV-)ObiUnCF0A(DCXRf>NpMCTwTj0|c3}frG6!V5~2B{14m8!e+*pE3DNj@n8 z^2p?(*Ps*AHvBhC{mXbSh<^LY`DrhAzxpSRT-h5R#ui)J z80`Z^W2Up?dJeLZ+4n`$T)WMPL>~};sA^`rlAUVSlClFO*WQYbgr(q7b^{T~Ks09Q z0r8Df$6jZ#K{OZ-$76bUDh`k=tcA$idE;5Fmks{|cK^Yf%Q;KoUwDBoZ3SZPOjoXb zwD!*y2c4ia*7v|SRuAF)vH3zL#t_lBJVYr@k`S;dK>)m!RlB}aor3XXM~U^AfWCm7 z)dRC0?;iV!AS&tVXl{IPdOs@Ekb}+A+4S44nnY_1vH0zr`BoZ2pJ4?G5&=EyYM!aE zbFs`+yogp5B;@A83sk?aDQ^;1u<9Rj{=>}8II?Ke>;SWf+ASi6W4!&^S%9BBqH=cp7e!P#|x zrt+$9CffzRTT+cN7>{x0|3$_+K-avzazgcBzGH6>GRPHW)=~v*Q4oKLCcbli0A}n5YF>tbONtyICNh z@O-JGp1;6UIeW0ti07y5gBwI}-J=ld@6!d}L}Df9Wi2KA`VD?hY|Au!1nT=h-E~he1lYpVy?A(2L|p3dT?i7{PhFNFIL{kj-P3 zO6C1E$lyXmsGk6o0S@LXN_cfI!s4(C#8=zkm$kLp+u(f0S= zH7YXZwxr7$sQH@9YA2n=NXKn6dkeqA=~0h~wljAp+vJWFI?hYT z#s!)|P8Gf#x%{DW2VIGZl~VShzt~*jR|zRp}LwvtzM+;3;ONTdbaaOqzDv? zMTEI?o`rpT3qgSqhi@CM?7)S&FF{tq_YRhf9d>vY2APV!zMB~SQr_wHMlz|4YllNa zP6gZsCE*Kr&h>Ua(2U$PR7@AyNNbz_t;9jobR>mSkK>8; zl(8qrtu#?GFfL^!Kb!X~L;ukEh$ULH!SlK6e@m56$ur z2S&@`rH0EcG2T;yks*o%4AH>YGHsIFqTQ%|NK0sfTn}*`5{HCwTgRn1!KbLS$(4TK zO&I>W-uJxAye7k|QFo6{&KDP@P(9w>Wcu!>->l>;0C!~yrP?I<1}E)VIaPp4Gh z&@7cW$h1xR@)4)I?7`vSS0n-r@CB#){8d%!WLV!Va9JJ9QPSksU_mHU+OhHo(obb{ z^#m>hCfGGqTR1s>ZV+zo|B4bv zh1tdH!@Q|6x`cx>yd{>t9Hic63m0BaZJ&(nvr0X`hZ_daIX4!z$IDzmPE5kX4Ge$I zu|x6-F98Eq!fd2Z!1q}eT2B5pPtHGMNf-*YWYs&a28L;KX`q_v*7$xwYI+7+Qy@fI z)&Wo88%c=Cw}AH|<-o)LlWy2WB5epN0-c7{j|-ZEg$Uqd+AJCde|3(_f&^CkI=h6% z+)nW7c$r-tq}FQPT*L-RQ+ZhWau@8bYkl|ZOel4Q!}xKS?lgfEb<8unCqC35!sWz#T~o*i&U%sUw2jZzEfxp8O4^hoQ_ z0#e`bbv#4|d8=mKZL6@$vuJzUa}d!pgRh|SyJ54xlAt`LBQVZ8>TvJ>VC~!3NSAod zI{31;Myzmm4A%)4=$^-R+Fic#&Kg_?g)NzG)S$k)yrohcVyaox;f-kzIJm=MCdhwl z?lil7n$SP@F-GEgbKQY)eO1fk)E*U@F)7xmE2{sqh~XJFOxshNwwd zym*P2?>71|1N|k$*i*C`8Ik>k=#cUR%fP9%svi0N68ppfRjMAI8M!TNGIUlS zG;+HNtyiw|>0j6N&lHQv%2Q40mcSZV^9)4~#X1M6-bpeFEIXPf+%Dyr-U|$p)mbCU z%j1drih1%e7mtX@;?)jD@zzidcb-bhIF7nJmydr7?h`42WK99K3pH-<|Iizi&(G3~ z+f83|Kl)PPQEmNGiy4*i8iJ=F1yteclgDImh2j%s0NzLLa8)XOVoq zKGm`&YXIDn*{cm^^d9|_y7E8rWH)^eQ_?camq&zkbFm62hg&Fc^71=uoLLVm=P)8C zr@CCuoG#~-5e4-_p42hf1#$|Av07MV`(cptryAqA7t{OgkleBHT{?RId?dv%fJwrU z=K5ETQ;!_mno;YyriKh!HfRfq4wTtT&Ez~gN2=JbuROvi-D8&c%;guBj5bH!j6g6i z!IZ_cM+!c42MsqlM$zXPMiJ<#vrO=AB)PD!KBt^IzbFBrJ*QF z4Wz&PBj5Nj`ya?8pz9{Tc(kYu^jO=%5Do_%fGB?~_K3|e$zDV>{l zBVO=aPb@NxuKMhi<)c*?aNgg-IC6rw)@WVeU!D{cpNFV$hxT62+tR*c&X8_C{Gz{6 zOAboj1T#K1q&Mp#3Op$wUEQ401}tq4e>J{$dAx=dqV~NFR%MW(!YsS3 zm{sG`Wf{4Ho})h@0X1Zg&VJtjdbPgob>cv=XpTkqxz8*_)lLq!A%lrCSWgVpRTLWR zu3yv?nn~SAPcs-$WxDsR4jSQssz0d;xX`Z~IIA`~(yx3HSok4-rGryL&OQnEIPtmF z@kt0Ik*J-u=jSE9ECY8MIKy+D|or@tt0f0VVaiWyr#4> z#iq1PqE@kA#Vlg=e?T?ajmag>Uy^t|4ZKy~J)@f$)(A>+C+MG({gId)xI7AT+r-VY zt`AZ*6j#o*`?)PzSHmnsX#?%+o^4_dYHTsqHQMIaQf;|0GaYEVZ7m`#=e$UNs2?Nj z=V)}$T<%DpMWx5)w#VX;PAuNa+$}XrsD3S1N=e>Hqz%=p&ja;XWGgcETguLq1*A-U z@x6Mj%T!BuZIe`YAz?YapK1%lMB>xJeYVl|C$+?RWnw4`N`{tVZCDYd{R)gkz2nk? z^CXix&-?6?P3^Esc&A?@aU(!UFop7CF!o45L93a?!skPOun9CC`>ECOTNi>dj|{3# z-|BRJT%N~$Xrk|7fCTQ3hBHA)rPiZb7@Fwz@L^VS)ZfzvM{6j@l^yfnQB{<3o_I$X zw@zoV4QiDqhKP&$>W;HeXO%Q*OcFa8W^XoVWV4v{!(c!q@+_TVFqt1#JdaL;Y(c*l zh7Xk&H^a%Ao|x~SHOol!XZ%W2w28l}o|R%$4O}1RWhYP({%!RfhWZ96k%wIio4JT+n-^Y4W`Kco}zRZ-i5`F{cP>JdKZ90wD>6;DH_L69~`L`^mo}cT>IK(0S zSx=yp-uAzj_C(3Eaps|~g}tEGm29u`9#6L9&||#WRv7T6-Y*c@B!^y&N7n%TTf|Ms zLgovkBcLwv@=4WDRw0nuYu zMtH7$jhm`f)Zg$pG4ldsUx`S^`>*Q=ez%RkDPh|p`Xz%2O9;g^+#1VNO^UfkF)-?BJ(F zoqVcY(B;LrZCR1w^ky=SxLyr-oQ0bRD~dFF0Q5uo)GJ#%MB>*Ztqzx!x7~Q@a;N3h zr4B}&g>JUzia@C4k#b+z zj;RPk8L6`%;i34cZLty7SrWa;@MGbtKZ8_@wvd-b%Wd;bU?@iDOKLG4NcutE*F7%h zl@v@2`L`xiD0l2>@jXB+tSvhxg6ss*hIi%|=AnN|_UoR_X4>!lRGtJaKEcJsOsRgLp~&04EiydA>4xGPK3`Ai zAG(qYeoT&WIYg`4qtj6oTVVz3t2EVrhZyT!MWR%wYJO5g$b#hOylqIaa!U?bolbaA(liRP728=R;I7TP0N@lS(ZY(#LWrtYu#Yxty(wSM3EVx&<#wG99&QM)o*x?n@Nf8@2P z&~N0GF`whmB#pff1g4nW_7LJO-1EU#v-&+ECSIT7_eeZxshT8GY=VZT%1Y&zHeBk0nl)R)O z%yfRywwAiBdX1WZ>y^$+vCU_2Da{I2JtZ0#SFYp278+I|;W{io!{|rZSQ7PLXFL42*0r%G$8}117ocn<(hJSo*YXyr4hD2;HfbziLXmJnb=ZB1*P)9{iy{pcea$ z#CF#_(6pRpo8$(l9PlU&)ksiWcG8VQScPe<#`Kstx? zOvPS>woL0(MD27WaJH@0Uy`5rWaqdflprs~L>irBcbqaak0Q~a^{%-3jcVjEir=&^ z{(Ai+`=3ix^wMYao2DpIh_xzZwPEyh1%9W5X6@)QxsC;KGpz_hIL|Rh$oMSnYMTjX z>L#P>3w8qVS%SG*CHb9U{Jx&_(+0^V)b#2L>de#h0)uGP#bxxTh7Z+*Bv^VCCuCQ) zd&GS70polo@W*Www-X5IH(C-ffIqi{5LSJgD?~_ zCAx}{;hK&eA|!HC=H+tzq+20}lZ@Il`b}z-XJYo$q&WUWAtR8;dGX+d=d`9VfMpCy!Rsqh<(CL7crKgVU3V)po9rcY}(va_c4-e;iU)nw~G(<_qwr{ z_@1Ows?1(^X|iclR$olIyXWhUfh*#^M_slJ?YLU8_l-VnrAK6(gj?YmC0&{$0q-Ps zDhY9ic>ns9m|O?6b4Q*yeD!~JHd}b&|ZzQ93n< z1h#HF^oja=<`#ubB;G_1Nxq75<%4Sw&rT>?`lsaMv5ZDl0zO2lM(Qe3Zo)Dq>kPa| zD;=PT6jZ`H1!wAo4Pm$AgLZXDXJFwEILg~EG=Zf<=B4)H?{8PFU12{X+;_3_+=;AN z$q48oiRRe!Eg3eH#8OF2-zRh^QWIA8J@&*9`=PKw2@jed;%q}&diB% z8>tUd(qvr66;7u#iaG=1npJ^|NZQW082<8+nS^BYHbYha`L-R$Gsr|`V5!xUkQHbL zQa^W|$N6)6akLVxvzyAJlQS0wSH>K;&dEUsp9i$Saq90G+M|nhfR-52fhPG1uGvMz z-JZ4k8UJ6+kLQqC?;{a3KR)WnfLIzxr#P+k;H46O*ul4_mUZDc%{`rLRWr5)oY$cJ zHH2lq3jr5-Ehn0i(-(Z3tH8#oxD~x_gL^74XX5C>Y+gqwIQflE#K9I&2W9$l&_&1=xL-z*ZQKG8sAB5~*Ag-w#UFkR!+C+(XFPiPi7&UfJw=}Adk zxXxM7Z0ySf-X~uMX6GA3Klk%v#C?(CU(Wb+4p5=og3jP|-**I5TSGaSNhG?E$#+9c zK4sAQZ1+Usq9cm`8eYxP=Q&xmkN4eCpRJD;V;_?I=07^|DE)6W17jn{Yzd1!$Q?O} zxR#S~j!yyZo-Tdq&N+6;-WN{8v@&93Bb=d+Iv(g)$oN9`h||EPtm}172jXxBmR}m5 z9>UNkUjW(QjUo%179Ku&bJE)2W(rtV7W*g}hXc8*4HAaah+|t&$xxIUgjQ-NqNJ~O zni#HcpKo&(S~5gHy|narA3CS^iNK#S0bP^rCtyXZ)dEobSvFtTa00o}m(f-f%ybKgXipM$h{>u*mWz&&G}y=r9}{-pcUibAFG30$5o~N{oS$k#0?T! zSm(w^FFn=Xs6jsJ4jCh>~ZDw=m20CLz~Zo5idF0Kcs9Qk>H#+!RBVMo)Ba+^g+#S!005bsLczzP^?cPTS zRlB{~yx=i~92)Q!zQ}Ak>p1((7RyEXW0wY-8{Fk)gwX)6AH0s4P5*oZjo7JcT-pB% zq5wXlE%HO49kEFMb%>yac4o)H458{UglP@?_-1viC860I%Y1Z>8#8R(o-O_PqcfG{ z|HU-UH6Q;3%FD`Oq)G;(Oh!io5#0?JhlY#YPQ{T2Y9ax{7!YQAm?=zdOJ$8Ng0b^= z<`SFSCESP8)i~6+Y{V3{w{>^%ay~&tw*Y0xD?~y z{;dPrIe)v_<2Pe9+d2-?VlLeZL|tL8;>F!k$eEORv4}j8eU~;E&z#JLrtpsRMO?t`lec*dWKH%v_U_%Bk#F=1<+-q*T~#g0s=tbP?vK3sg0@h= zB$(sOAlMiL81!+q7+g{Smb)QwThr(nZ#x^x%L7I$C@Al_FLAD&YsQ8Q&9r~mx>q;a z;o9(Uz@61If-a~h&>qn*h#5_}VE8C|k8X&_DNW6}nnH7*T$0+fOUznxb0X)36WkY8 z2|v0%?CJH(u8Gyqw7w>{My_Y><=)Ke=Id>GpSPy>uW!8d-0qHoLtT#KLQH_<|BHm@ z+<=F@-A#QCH=Zju4;v?YJ*ol1K~8)0?Ul$sPdlZEZ`+~mdF)1%T_>fjiY6e^(zr$U zbkHcF!(BqbRLgwZQ(2Gz-uDY;7|vO^^7xhm0Q`M2HqsxjR_??>Jvjrrg0}7gb^HLFh zN$mW6Ron1xzXweEOFP6FGXc-}?M++!T*0$AQGSr=GFy_3w@Zs19~IS%E#)_Y+zQ@w zg=qS8>z$)BT{)FQ$kdGOICn?lUK#D6MBNmZ9D%guV%dZbzyJ07-Z(+78C~@^Pj!bG zOKPQE0?F&Wf0+SN_Xg-$IEzvryl%MwEz+MFPgsN-Ens{-U>rx7tOEAr0&z91xT3NG znwx!J)**eKgyaHrX1#9sDYy2@vgges2WGLE6{u3<7$s_b;X${Cbx_QI6|#2z^P^Ak zew-#uCM6S}{(VQE(vi2M?9D*mVUJuCLW`&t?V0-r0l=rV&}74u_zDG+_FZ8t<5*R{ zo49hBkGBhV-X^nwFv{7DxdVHpBdZWONQ9UrXb7ofAbJTO?ESSM;`-6;)}x=}{;b-3 zw^ZUZBx*C9?J4(i5C6bu(0%-WZe-`#gq zfF&2&{TXEOp)*0y)Lv!X0d!IsAg^-L`9uwcd;Tkc5{=A_YCKZ&bAe}@5LaLA$xG{w zj(BWN&S4Yx4x}AqJkmbvmpqOHSk}t@j^1MC0r$Comb*8{W%svJvKM>;_r2Xk{p}^v z+FdbWXl0Qjw+BUNTcq&n-vg|$B-z4KP=zAlupV*c4uBV#KF{Ujz@UQs@EPJr{^ z_?BYTWV1bdID)a=paN^$6D9WLpBJP|!|7HEo}3RiJ+}rkxO!LU*saQL z;Tr(&L{i3Gp|;5Jv(u;`PTCbKIv}53GQ7{~jL>W{u7q4g#PrSGd+OO3jm=m`!wp{= zTO3YSK?|Qc1a0kF$6^0iHeB)-zJ|}OoWc0Xs6^bY18kg*uYTVj!P}bQRm<|BauF;1 z+qIV1)$?Rs65FrdfeH0zDSO3n{bq2n=dt7uLI=|2C-a)~&E?wPLb_xkXMv%u8!R^o z%|6ztKbMajGes(#L>ocOhcMdSIeFvb=D*L@ld8l8ZHC>@s52FpH^Q84XD!t}S<=30 zY`%!01rhEhyts}H+yN>h1CE3Uyd8>$=;7Kqj2#^3vS3Gz7+mrNW-0ZjMjp$@PB-OK z*wrHI1GUM}(g+q04d3BcT5-+bSex76N;(GU0_;i(p2=UMgiT&KjBag)(KkIFb7b zBw<)q7W@rq0^*ZKR)u<=ciuY*2!x1=AkOKS%|8ovGwo@F%yVM4?Kl^Q#WK7R!M=%? zBtu3Qj9@(V*HHhqCvcdLCI_687wb~kN`0Bav_rt{QKAsTwmg5me)rf(NJ4Aj#su3* z6p9eQ8||EWX>{n2u%;k(IBDs<-AtG4wchwwCNB;hO~J#0w$NZXK57NPq4r|qg{h;D z_{}2cxA&(!xh$6tUBX63bwbS|@}PF~2DI>B@Qzc{3RP|j^G=;}2iNaXcZXgyuD&L* zpsH~#fHADHMuJJOcVr6HDyN0(t6fMk-z$#D$f-}yi~jS7c5smJ1QeS7 z=$Zv9p;&r%n;!q?a@M`P(bmLbpD!NJ791ZsF;+u;P(B9_uS|B@Dvh^q?{XJ$dg(TaV_c>lPSn35ar9GZLx-Nd9=YJLIg4d|Ge{_f+ zL}I~P((||P{)X+N_O;-`*c=hjCDHRZ(TMJWu`|Bx1GXNg`;b4wP*Y&Vw+LbO%*Cp; z$S~Yaw6pLc%WBj&j}N=#enplOT7{QHw1*|NQ}1NO$cglGA)6!*pkmmC8=2Gn1h4(y zcK~-j9#r9APkw@~26jBCQR4GB!=&#XJ6SoYaEaQGJ{wm>Nw-SmGq^hDq}^MO5^4eW z+Q^?2@FJP01S^oV2;<}~tzQCZDr2V#=RwqXn~{5Mhgs*ClCZX`^yvg^J;t3>j46A% z)}d}E)Z3X0*P-+zF0d?(74;&J)G zj?GwJjZnZ!eEftXza){PN_RRN`qH|~hdyf%QU);!BJ0<-XMG~Jdh|SRv%-+*iduK} z-P1c~?cP)&k%m!gMDR8Qlc+K51AC$*{`abBJMfd#(HO)IR4n*8!V|YI`YRvJlnrbq z7kPk7lSX`WV1F9*NJ(kBb;0PhNUk~V*?7tAK=8Amr>jQ?b#I5o)J3bPmyi7NAd=?% zJzJ51zcb=Ke0O%xn~~VF+?8I?v8{j#9#P#JiQY2ULBXDry@z)IDx?PAvk)kn0%wV3 zU3~fv84)~Ei3$cgn$6qj?#z?@aQr1SD^c=Fygs*RXhBqda1TzDZk)t!BjpI!sn7j? zR@e3=IBUs_7znzZ6?tgvI2vHH|HsyK$5Xxk|D2rSC`m_JM3I$IWE_o?Y!XLGI7o}k z;t*1Z(vp->!ZD7SQJKdGDU}i@LL4I%j#-pdd|z);-Fv@({O;rTeb8~v=e*yq`5X)+ zIu0$MUY=+}PPjX86=ts)R6PZ8kiB1`cHXuHPJ9u0? ztGst-mL)oy{NX?Bvee)zGeL5>NCVWWNVEcEC(AmNDmMO_CI}f}&=H1mwJ6HjL{#^7 zHvG@R+3OyE##8QBl7c9w3oP|SK;X7KS+>goE{t}s@PiqSl_>|aV(f>`vFas^i<&>C zK$v?E_8oQzScRD5r=nWH-aYkEBea|rC$iEr>5?k$Rf;@8x@;wyfVH(j9uPP_N%c?b z?#$03XN!IjGK6zzRbj(YDD!;&N8kkra`1ym=FE_ENvOV%kos)2RC8U|B>Csf1|h&p zc!?Lt8VsP@QWw+Yu$iN0`+Kks`w0l_7m8%&%~V(w6)1EgdbQU`!BA0RwD1?nU_=w;qzSv|hmx^>@t z;YY-vNfxmCVC!5c8np^mZW)UE(=@%8sXzbdIH=3C6Z|Hk}Y z&GkOV4Sj&jo(Ds_zLtF1`9T%W20ws-G(+1hjr87@AtpXD44r$hZbD7-r3#z^TX?Vx zsH$v{s1a-jFCN#DC}@BHNY50C_4$4$Di$My7LIyXf#j}&)mkZ7G!4}`V&zdhb|X(( zna4j)M*)+X>GEXhWkCc2-CI5LI(FBtx;Nj(*rnxZ+&T!ec^}xmv+fer?j_ezje(7Z z2x133;%qqm&1i~rjs!iYZ`(pJ9{yk%b+5h7?dY9Z6Q*B>|HVdV8Ap*s(qI*w8l4z{ zY`$ONcYGIS@|+E59-nGAMrcN0Ev$Qs;>zV)^YITl;CM8tL~~=%AyuN(1%;D%df~(5 zgH%K_Fxb12XhqxzwxA}CtF6oeU7VusAFw(dyg-Bkjy#r9EmO3)*$&T6(sK(;tS6@c zYVSL^mWX)ny1aTSa?26qPYDg--GZN~+Z-S__CvVHhk4I7qb_CG&2q_a1D$~#yP6mz zQdY_LyQ*UkPrMnd(1MrQmYd#iim`Yr%%Etk5B(ujx#<-kt+}BskZ7)Ymkx@kocmD# z5&Ai*4nU6S-qeXKBubJW5GL4V4(bnih>>{x#aN`+*aO@-7?+kaB0FX?iJE?i)?|26X zUfmB`rqG>h`yP!#8O4u%bObk-Zvv8iGo!&WQ)=*;ozIQ9uqAG{a-dtn$5h=knpmz3 zWV+tLA*8D=OP#L)gZ3i$^`JZo;)pD5v@kAH8``3Lq)%hj!^chHPm#(js2ExYD3JaN zZMsw^ND`m!qycQM@K`XiE|aN{*xqH140!DiXJ&4MaN?>LD`EP`wcDWan3nJ+6W9ll zS3OHd7EQ21IRT|;HdJv1s)E3m)z&PhLJ=y)QWkOV$1Ok(K2DZ6ONkV!6UlF954-%T z)z4wus#*wgZWbtU^o(hq{-aJc|9 z6L0CXakpZ?i@T4lb94nZC7KJ34NdYScRfDYAc-u@Rci>#>iz7XwHl0^dIy8LJ~a|< zhbUZA*~peS(01~VBhx6BeLcP{l1xtk=&NFXBH#0m;NFJSRHeMt`Ika*x|e# zhx#;VszyyoCfz*FUk8=c=!Wb)$L3R>u)<O)O-WEqteN7AdLEAdR z69U-FqpC2+`Oy{5Zk(b~?SVXio$i9Wx6ms%EBevrV0+OuJqXMgFvs2LvE1N|36JLE zJ)X4eE2TXL3qC_E^q4tx136BRJvQyodhGGS1C|JbLzJ8QI@eq@hWvu${I~8Bs)k6HI6Ms zDZQTdu~7e?K7g~kF1X`8LbbETKZem&QXMlY_LR~>O2SRvu9x1C9IS)m2WiO3;^6&W zHM3}bqC`LGN@*eU+jev*YZOl-utJy^nXsixgRMjn4^DUNY8>Pnp& zMq6#I!}DQAZ6h`FC)E3t#%Ct5MSlnz)w!z1Hh7`=Qa|5NcikpCi6zp;^8{X^1Dcco zVPw{53n(8Z`_)pqUc<9=1b}*GRB*T{l3^rsNCCKDFybsliV=Mt$LZMsyV*t~Aa;A4 z1&<%5D_%wyIhuY*=(ts}16O($kpJI-E?&SBN|+K?HiixojKvC3WS4GEGT3gJns9?Y z!Or^dai~Lt(%+VcXVocR0(LxMyWMtHed3@2EX9^ho~=(S|QE;wr64-jtyzO4Ih zg}@l6l5Ypn&GR!~-;o#VP`ou|ioO0~;d1TcerV;!?Tj;Q0|e`fVv6FQ0vnF_O&RK& z0oJ{KO3k}(Jj{phRo-=(R{zW(eIP2t@;<6dQ#FRru z&+@9e59~SeZQ!DaH{}*k<8H6@#~9PherQ+KyAD{a9R1PIO*sZN&S>QVWt* zhQO^Q@Y*cG>+MTDVbP(5k@;3Xl~j5M^_wT91WpL-qZl(74rWPjq@<^kwxq0OfD0J8 z+B`5B??qC6Wz~C6Z}14;wT(A+3*j+FS3sM>+lQllw-c24Oi;M zN10cDS9ELG$ol}M;?wFOg4{6L5B8u58UAip*?~PmRN2empyz}{?1Ka8D11CYH>~wh%Z0kjpcS$dc zXswb|307(W-yjT3R!L z_JDp|_(z6pha@H2C`bbeB8f&FZJG?h?|s zyyCbdh0gfuhQr(GYzu2V^it_eI8Q)u7a|f>oW%KHB`K8*@;%t!cUPaT&s5@i{Mw$(^S{9lri6t<8&P%so!Zy%(%F`80U8^qFZO@E>9KF=g17v;L5A(L$Ftv? z=qL;>Amzw)!poFL*TP2&@!OEo9(d<+Y3_@ZE2q3L3eh4v;@=rX2*}<~7}N%Eg@UvS zgbrSFT)4Q40>H%hPcs;n?_)pTm(c`i2BgTisN_(frCS8GLN#q>dP>&FFB+Ik1uigj zu)B}C0a(@+J`gWMKD+t#Eg=0iG@Ww97^V(gG8~I`JYTTv+AjAU`b>W$yM)<}+kjZi zSY)9733JGA@R_#lW;3(B_Ez>2DuKQqWFO_hyoG-=Y^@*PdkX&eVOm3BWHnnkkg;ocJNzU zdZXh1Nwy`BWd`L-c4qy4c5B#}#1ddcd2Gzp-4!8gb*@c!S*bT5bLJwB9C$G0zj_W9 zs9k2hf!0dDlR$+9@$f3?2HJuv7**B+^*~Evb$+VU?M4mpT0y@33}z6_Y0va5rd%GQ zF1StLPhDuNM(cA5PrIXv9&Kl#cj}^R+SQm%8$=h4f>8(WqoWq$_mvOC86Jy@AW^|W z6XpoJ&|afY{Xd9h2=*vIHA>(~zqKF~FlN!#u&q0lj<@a2gF?MZx^w0EELcjxR0M(z z@;pA-l66e=cY#m#gScW2&Esb1jS#Oq|LmICFPevpfmV47($~QbP5A-ut#w0TR?gsq(JPeEo!y>f>zXCFND&r#E!-u z=45IK{Z!f7FisJTR^ebK8~9beRXp-+32bJ^JUIyEH`b|q2!B!=3w;FkM>h;5p2`Tx z&ka+AblGzP>>+PMymvqy>RS2Z`nO+Qvzwh3oxvcn+decLS-M7H!J=Oy^0`o)vy50Ae-iR3L|=Kht@A329&wAktwv%{M{HU?7#vGIw4(4|2Fe@}3T zrm!g^-U`gzZLzFw{ z+6JIv9z3Bo^W*W%&o{8Fyz4sx@~xLvG}!HyEr4CmqSiJ<9EAk~@|=$%DD)P;1VAkg!DB=_=5EtV%{OOYo@ima35_9hR|V6K2{drF zq=Pto3jh}H!(6UY*#h>-m(5FbRD992T&Af1VfMOin&9oys5>AN03N0F17_FF^PHF` zRZf2#R>fmv5s)n(elFuR2j2Yp*-(|A5?nK9)v#fzM0^Xp>&{et4%>)0=5{8ls%~E+vZ? z>QHX|p^IN-sbDM^&;SLb$Nkjd6{gSvnKx*~tu9&lWXtvhlVxZwLwA_fd@as*1lr^ouoz;?hnd${ zGi*Figv<)q)}KdZ;cZ%lUa7D|?(-4;yA1x7Kvm-|C_!Q{5as>3t;x1JrAUeo z$+9o8_B!6I!!kDMByV-GqOjL8x@cx7BmjRKFz% z&SGKy_aXd*v7Amro}>BO2qxIavq|2>pou(yQ3*wf7AK6gHOT6tOcN29! z&y@o^%GZG+DKoOv&r5JZJw?x99Cip^jmo*l55{Hhkq4!wJJgfD%#);(Jpcl7ZSP~P zc@x07qE!yo%*O!emjmKdbVGPVVWg91*v8VS5fvm>8Q6~$D@CFeqjY;pcDQbIAxy|u zXZvyaC1$H;tmeVol&1RDz);s8r!2t^PB@$ti#6bdiu|2=1}KBAD&?Z1#oxWkC1p*s zh01-6ZSs??Uh?sB?LT>G64?*?8dGIVO3#ng?pbySK!OXMhS0*~oEKobCI^#&hsZe{ z`V|Tpy48{o&eiRCoYq+H@Gr`Me!1`aUfT))py84xl4CRLa&OOBcf{)vcPF9+#(`lo z_ozQ3cJ!RjVqrT5NIwYf{uX(4LgZlR0FMe>MRPBJY>}nHfXul9jTS4hYsa z>iVRU6ON)1^VMKjsl{V}4d^SqY6J}@N*jGYEo^_z4K;E%V8pnuOc^hmjo;R8PHKFGst4>jZiz_B`K$4m?w!MGWWi2}lW={ghU7iG^Si&G+W zdm?uI+>i|rBI-ip$)^WA?ARrJUSw{|x-ei$i4&dvFtfoY?!qveAucPKLa@6BVjeaZ$qwdjl{?fGS6}T%<2kgtNHp`|hFjw=~W1D$dMbN7R znrl_~S6;>scOgA{3;!w)SROlFeqpWmSXpeeI*ajRQe#&Wz4PMQ&G2Kk^`#(E18t90 zc9bh~nD^_*(rb%G1QV>b7Z%QtAk%Yp zfG!|WRE@ZsgYwu&LWG)4w9)HiBNsKW@Hb8AZD#n77I7n7HKuiQA!Jvg+ zPtmQ9k@?{P3=efPTq<;)FCM(0=EVtV!&0>$Jb!Z_z0AaRr}PCM&@`fo#Pv1r9@rcx zI0|v@5HQ*;lB1A_I^)wTKumq4SFzLc;)ZfT42e~Gi7}BhB=a*&O=z-4$gp?ZP9g^p zAYAsP=pEkZ)1De^^F^SAOrR=r)J)8<~l1oTeYw4z1N_UN(Y%7=G28*+)L+Kh1Ez$ zCPW{D%NSq8g9&^fgD)eMsq6FyR4geypS)}Xr}S#&(;u~`AVgh=p`Ba3Y;>1`qq;zP zPJm_hHpsB-3pY(-jn+R`NFH|$o*A0N4nZ(*7rsoS1zY3nu;!C;8CEWsFpLteoZWnq z_{pbWbZnhKdL_F)-kS07qMz>L&1tWXXVZvR)a3khGgQA-pU9@w>O#4+aHqxjG1&7i zDpK)?xS{6er#m3gU?L6Y*TI8HIaqcnz2U*&29CVZxM6^zE**FI89kWEktd^2TIn>T z)-|#-(ZcpDQYFCN8r{8jy?)M7B&sDlsxU2xYEPjGcDLmgkF|QZi4^QS)?*IF!)E0N z3o^4J_kj&}6cCs`~wImQbI(a2V1!_bUBbfZVuSR-;$*0CRh~C=c9i zpEj9qJA$0vAB+?uEj-Z{prG~o8`41mk^E1jTDnh9Bwo{n<=%Y z*&hPzcPp4j@7EIZk&UXfsj%z9HsYj4}|DD73yF5!2Sebiaf~O zNY@B)B0#jM^@^;Y{|(q+Qo5<1Q$wg2FR$1}cV7CbUG*cQc|$%k2@|a2KTTzPB2vbr zhAz4XDpSs-FT8^WRn;fBTxaY4AX~pmckMLre?#(09rr+>TPvU2Et?X@t@DDpC5T&x zbNwA_f`~+RL@jTyWp|c~)oCLuQs2V6{=RhkL?}thWAf&T9nckX%fDCAX6Dp+Q%mr2 zh>^TLYEkA}g@XNPkhX8oBi* z$M{R*4)tzw^-hW-=MGd(&g=7|y1ei2()%rktw*hk{XOI4-I!cR< zZtq#+4vkKsz$5si!pQgFv0HTcQ1yO^+Lb+b=@^~U4P$EwZ+?VX?jj@?8|3NY5{Ph!TuD=s+yeZK>%)XFFt~6xr3~iLT@!?5jJ?aau z@*1AdCpW!~z?nS@|NZN;Uk3~ymV^+}BqI~OfXrht7GgT}gBqgUm`QeoWpU=M0=1RU zF7sSE)%cSNCO${~Ty$mBR5~t^=O#V8@4;+y_@OI| zFd-$f6~^R`#Zu|fwlCzdyNlP_@NqCsa-qNY+;&lC|%3W z8+XtG)`Lii{jsnB)*DLAPnlL}9~IAzyYTZigE2!<9qC9TjE`)^gFcvh{eT6=!puRc z%T+o{e7|v`_VPvv{qD!kfTD1gSw)KqDIpnZkWYLVdIMxPb2CM9x~6#X2+WL%9!S|H zbtQG^oMwNjpm_bzVF^k%Daf#MZF@))Sn4rPUA1~Y1MB8il|d65%Y|OS;azXH1PxIi zbA3L0kV8wawO3Qp*)tb(pWj10q@NWy4iZY1XF9PSr6-FCCnz$#U%YmB(i#&EbQ+$i zGUu*tzk2dR<>l`H1SuH4N3Teg3O(R44!b30VVVo+hi*@?5vVG&8}rD(z!Z!%a31Ga z>kc%Ep4a;Zv-V&E#7KqzD{73z_;{7{JloptWdF79$bQ^|9v?F%1s`fWw()#bMSJnL z_8gx9<1DcOE2WygZ&YgB!OJS=F#}@L=MR?c8*(Kq{zeG?MC#52giIy2-%XI!)Ytk2 zpmR184u=QlhvY`FnOQKLbZ;LJVM4u6FL?UjB| z($lT|OUn-09TM^p&UnLf(-h~O^5n{N&s86v=FE;)aNGux5^7}3rfoq9?EvV2^9R&2 zj3*Q1cEfPbg8CW07!SSfp7(duR#;Z0z!bi~QR3(a69C-ahw(;c-;Ii$lf3QuC}sPv z#~JkKoe4B{+c8M*%X|dTucy*vM3>{oMFPv%$_u**=`c?l>=q9+!qbNvjWh*rJp&r{8(L{4o zt}Q8exzOmCbZ+sx>EDyfZosRAh+m_UIB2lAbh48V^uTW>CG!M&9?j~wNqA&?7N=#` zt0XPfBW)iBI4>m-22t_;j@SXOvAOuc0zK^ z?LEAg(YRKXpYnsd^-_GbW&f2)c%5_+G$nJe2EZT2deX*zL)Kwo7;tV}0PQQ9^_UP(9!D(G#4kyoPG~wgN76kdi>9ZQtf` zXOZ`G&x6a-(mTYwJ*O^iT~QRReZII&_FGJXb*OHY+ED)N7*b<$`H3ID&+s+Q;_1o0 z!NFxd1&m8~$366(u3G#S8Mw!jh(iN(mvpWh^fUhjdWQGtIfMU0ub!)q5%O5o61j~r znY1C3b&G=~amV^w3*S=2!7L%awHR5h`)lG>d6ErBl>^Q2H~>rFtpNfN##IfjojQ0g z#_2scrnn*1vXN!ViOQ`iLm3vf!&;xyCiFk8L}TI);wn!S<6IAK;Q`W30~6Z(w%XdZ)8luh{ni> znl$cW&v><3ISnQ~q7&Pa%BYPQx`)NE1^57CqV9-clYVp_KH%-+sYTAk(J!?Gg7KP5 zzHg&CSzAhcgk%){Os{_;^NZxiWWzi3(B5^o3;kE)`g)ZmpMSzOosDyo5jRX~3=1xN zJYq$@jdxF?Gn9<%4zpBZwI&MP()~R()OUSr%Nmo3zKY}g4)EaUKAbA6fZcU^eB^QM8 zW_UUCs-2D~W!y-vD!=})MjShZ|Ddm0?1N(2{*o$*A9aB89m{%Oidi^mbBI=>#v&vR z2UKjqJp&on%D@Hn?@v=q}wxr$)#^_C|N>3#&xJ2l&EmR0xr6Bm4toPuB9vg2K-p1Ht1=eBz z8W<&pCaVd&k5F5_<|;jZTq>~8Z96_xI|Y=hv^Gvmaj_vh@%NWfSp`S376j==hv&4$ z@b7$k-gtr9VR4_%tS^#3;%cwPWEpH@31t~x#5e*_+ci$q^GuwKO6rsF;B<`2`xCI6 zZnf9!zLVb{@z_8I7ca3ck$UMY5P19Zz4yswsos$ch5GpIY` zre7Z@X(;-68?}$iQlcq|Cn{6gJ1!lf=XK+_)q&Ha>#`KY0()|PL=wDv*NxSPVyE#z z#>%xyt3Ekfg4oHnK?;7v=UQIshyj~CrHLf2N9#)Pk=&p7)hAl)d+`@bW3M+x<9o}6 zvAle`Z?}ect*@P+ZDF+g8@)S1Ub9uA<;utI z6#0A8R-Xb%-^kY+Q{S`~bBJhstvlZTJ7g}w^*p(>)3$fvrJc%ERvULL(uv#(-6&9j z=u*b~dG((y`JHCztMf+}wew6nw_9MdDw(Bs2_tjU)RKP%)&Q?u%U_IB219u3>;7x* zW?>ewHph272eHQvdUMav2e5<3F+aY#^OiDZR6rE4DCW6<(U?>t$6bSpudwzSeE=NF z3H3e61TKkn3EZ14?arFft3B+tW3S-9)^0t(FFCMJ;4;3ZE{_jKDj-Jh&2hwr)${!v z?tF5pGXShC3*`bT7U2X^rf%?;g6d z`X2tr+deaaw0a-Cp{2MD6EX6Z`BK-b^KV5c$?Vvd$Fik$B&MDD@}Xwm%z&DxRp8QZ zliv^M8oH2%?7ygL_fD}+ro$hqL zvs;B%TlZ!2GuJ8Ju0nQRf<3*!JEwV;E4TP~nTrAkb6a zh3yOB_KB1IZC!Z(uN5n&YgDio@PbCQo2FkYvLZLyQ}&pFpPQ#%oW%3QU`FRq#9>*om7E!imo2dqNz0@j}BUfyyz3T9XUeQTglp9 zZ?92WzS%07HtMzxR-HX`+*4V~LI&vqYZa}LM$kyV6?%_v!~SlWlKft?0!w3@)ya0k zvfTcAd8yXn{;D_^JI?KndR3d6MxKov^fek^9jzWaR71Bj%}vC^*u=3`J0kf97{&YH`%bzJpkKQ01^z_C1zJoa{Ixn)Jj2LWOV}S6U*I-}8}HFOf?b1OVsuG> zQfrh#q{&V+d{H|r_EQNIFGV%ju5pFYpNR|6GKE>U<2rolLUw1{7{)9MfU+rg!~Tc7 z7)sy$YmzgEJUb;Qokm)2FJmq$QM9e@bKIfVeu;Ct>sr5&<-TXWnXYr;>uSsz@VsTS$$YC~Fy87}NMc z#x5!459X&y5odO2PQB$lw5AB7#V<*@YNR9xqg@I0#BNsXLTu2-OzK^NooM|r{oPAc zULIto;&DceQPwY2ohI*Pb*F9&!AP=_IiX=HQm?`IpQf(cl=X}~4BXDu7L$2j%9aobC8Cd;(XE6=(2M59q!;H3Fcsi+D zvP7`quwMBKQyBr(z$bWp#*7|5=(aKS?+F^3aBVOodOYu`S72ihh>#o0Fr;o=_?7u3P# zjky!r3{GCwfK2q%z?lAbxLA)9{Ahsbe1uyJH-A46Is)-{4c4|#9!S(mN)JB`gf5PqNQKX!Ip~&pr92mMzlv*_5Ruwm>P#?JS zKb1cJAe<>^a?81~mc>qLH&bW^D{xChL0b1GXU^t3d8u+C^tzsXYP1@u$%;d42_XDZ zV0~elT)k0(BhIA^>G}Gz5x6Iv={ME&?xz}G#3v6r5G06uQjUM9}-j-Gg;)ZE%-UqaI||JsTqN^>_ld5=2td zj6!x-XG-XIQSN<>Nq5!leinG|>=@rG`!erh|E+{&6mFL1c$Mm1GQj3#C};dod(c1JgiPTsmxV^1M7yhDR8g&%x7b(5|@)|VjCLCJ{tD z^_Ez^QcUP9G~2MRN-YcIdin5I+Z$>;V%S6DJ5%Hr&kxLnF0#9A*|UC}ve*K!qRi7f zXX6LajkCu5&qcy`<|SA38Wd`;&M+FqcaVZ#cJewu=f`-;{`j5bR< z(O2)su_~E!_~XD^acN|0_$IQY^LAywIoYtDxe?#B@0Ie|_36}Wr5_KvRGEJ_V4fO1 zbVxWcKzBC{_{jy_Hl6#yZ61d3GWwCBw2+9*E%vL4S$m9>?+wMtT^=2ICU4}Tua_s0 zn0%glzgMhbPzpd~GmDmo{lL&Y4YSryuu6=XJ@s|69a|=3<2Hu>=sI$BzMTN%gT-xM zvvqyJlFV<^Z}knVh+jd}53m6A@CkZQ;`grqL{S+fGZ}5D2I|>e=>?EpuA$(#Ot3@4@ThG#uNCo1riw@MqR-D>IdnW+qRhU%hn@cfyWS8gyF^ z9)?U2yv6Lw-cD(X=GX97TuQP`i?l5#Q^hDzIxk4#x|(Z8bdC z39RdHJE3@ODtKmOUe7SgPTQ7Z7BGL!U4TMRl=lWvz4&AQU66Eu@;pUvpG0Kh$)@bM zSsh3*J^_p+|5_|h&OH-_@Xw%GVcvLMmwDr?p4DXEnjY8|#oMsV%nS}IO4AUXOmC|g zvxxx2%(uMurFwia;ClgN zgroZDx%{%nAf8(%Nx)C zE3_5@RyZuxlGd)q>ywz^?1sz=(&5M$zUkiGDeVFdg82dOL2Ijv-lT4yy3qv|fP0xC zpKGG+0kjbmbrNA^zr6_{+)xj#hQgthVZb_?P%{=M!~A#~Y_sx>C`#>ArqUfFU4_kSDUehA?j9$$}piBi({1H&@ z4?(XXJGy^*Cx#s{6^(_o8riJQbOv&)I>oqH0N~PeB-?UM66X5ITFo0!^P2(I2sToH z-gjcI!2%ePkxVV1BY{2?g+z;! zuC8Fk%uzVhBg$YX?+B}KcaO@GWUqgxA7mW_Luc^wA~8=@31q&#Tp_UkBNy`Wh2wYp zmsz;RbO3K;Iby5k-$Wv~qi+Buc{=veH;u=H8~tr1swY1Wfdehu%n6aZ+4EQcbzIshW{evbgZiu~f+1Ti$z2DmI3?c$K(Gk#iNI1(p$M)t_;O~F~<`JgB>r^&_vq)wj;oo8QzSZM4 zin5WM4oz0OVL*LX>T`aux4#SSqm|!>+J|=XKU1^I1Y|=lu)AIco*6|JmikS#`^rKYgdHlxV_1~W+UBT*aUn=CxCt$)a-pE z{rAla!mvr70%$w1zH{94BV@?;;W8M>sV2#Ro(;d^TVC_Id^65rYgZ56phec5z<+AR ztpsM>i83~@M2kIArTwRo_vb-gY(-59$lggp0x++;w_m+9`(D&T%U~Pc`;PkNw#2P( z11(CS7i&!aLJ$)>csY8GZ=)PAk<&3r&|C^%NmQTLZ>Mitc0^Z%{fa#H76@or1A^)y zKr^wetNjvd*DcPw0&!;U=mN_Y3Ygk+U;(lgI3#*);KTDt`2C6~G{)!Amhc042Qy&z zx5fe}gjO%tBs>2v5%w-1wG7!HMVs)!=><~0lc&_r#9tp!NC88?oDD>2v;oP#e;gQ~ zjv#r&=AkJhfk-%i)F=Y9twGSpDOK_3c5vgM!9P*=ws|!B20>xrTMx(x4!A#d>8!ZD zX3^B_`}$Sy`N{D$6?;BH{;_a<(8>o%e$Wf&)z7=X*t zQ8NdqHu?SS`tfis5g^o`gD(mYyw^ZxcZ0h3;a~4T!UaeSv%rI?VnWxy6=%yJ(^`0- za0OxH6w?Ga9!uskZh$tC$%$m(?IGiEZGc~R0r8B_#Th`*|7I{F2`PZifDsg-0k$ex zwfzBe&nPj42mwC>QUnS>jmvNRdohhLn~-j{!CO#by4_kTxV`D`Zvk+6c@Vt{j~Vl+ zlP*=j-`Meg9{}=ckZzX;<fk7r=RPVj%7J}gP2PEtAEV(tRcjK>Qrpu=ufzIB!_kV zvsyAjZWx%ZKLp#5m&^&X0E4Fh zy4`{EGgGm>$>DoJF1Qr1oDvyK)iyx4N_!G$6U0$kC6X&ia~or zBqIu7Rg>Z~r=oyp4GbU3Uu0Lel1xBhz5}umN&!>daA~luYe5)AfJ5<55yG=(7N8m; zyk|3jy^wR@-&YaB*Z>H&o{k;{A-2%?BZs^HJUK{2=P&@FdI;Yh%c78|K1Iym|+q67nHm7OdrL%(5CHh^9XAMAH$`#W8cl0rXHGY>)$c8|3>A@WSfc z55J(K`({0H;XXG51QQ9_dXDZ5S^YOGzf6M@1SZzcssh2IfPMX}EQ$o_V|-rqtzSt* zD4Te7rSyH6<9{z(xU4`Z^jxN8387;JhsY_#KQE?!y9|UMPC_zt=f9hCcI_NW!3P7C zbO|!1M-oB+ZNFN|xpm|3Sc(RN;z&QUWv>$RC?rb|(l`o#?DVerik#y*5$c)@$SAXy zlI2ngayP!J8|R2Qf6|(n@q+@w6ao`S;hB{CW05P6JD^kpJ8f70@;ay!rY;}>Zv~+l-F7Ed33|T* zrQ_u;r=eKyDv;-ze?6h&>`D7#oq+RvkqLWgq*W4mM`;C0MK(GeP&nlRZ0)IZW0E27 ze-0(!Yr!$f86E?;>No?U?a|b{kuk_t2!gOo5#V<%PJlc!Qm`4DX-7kieP z1dy{s4xoRi<0jsCPbvf3a&TE<_mNkcCvowd@oBtWbWplQ_pf|>0ds#8V zG7bf5{+@e;zUt*&8$h!HjVu2riMWm`nCIYo)@}&?&=hAVVm?8M&HDaA13FTRr z`}af%$^V@u0ogSUAnpv1?a4bob1pv`AVnycN<+QNpG%IJmmBlLBH_Svv)nXf)C^Ap zOw`^(fy6V+$sVd4HJ>hEcB$I|_|vVlVLfa3tEPUEkq5hAuv5S5llcjr4`SQd|o2>#=ifQe~OtuR8Zpi>I4 zC31cuB3WHyo4sM+%uC|h4qx{HP!;z^$W$I3rQp6zU*VqglGY1<|HB7|4>f5+2lkYr zLz7$)EVX4%nr}yo3Zl?=87I{8J$|r_@&J_HAek1Hq`^idy#tzIWkhw0P zF<&tt#Ll0f0PPNBdI?G(W>EB#c~Suur3jkxzTOZ%;}@3yWhJb(aTs_VNRJso6x{(( zbMd&C!>l^4kOmgDSRe);2qjSs=rY9opH#-WxIe{E7(Q$mC>N@N^Ck2Vl0Ozz2mSh- zynY}FW(pLEz2%7@&UXkbwH-jMn(c&^kc2Mx_P2k=vKk1a{Qy|b&4|PRgS4P9JNbR+ zi%DWf(mNg=Veg53KCyq_S;UQ)o}O}^ZMl>7f=DyO4dSog0L^9O0_OCh9_l{?5WIhW zW_s}R7Zf}L7GKyqtEDzun}|Fv1Cr56R2gRgrV7L>ZvIfUo*SV_$b@V;qKO(=blpKK zkA+{qY~k;>jlPg!US+S*6h2U~#Oo6PSVr3y+Xfm!D=aS@RhTUVtowUJ9M5>>dHbA>m18z=_d{yDE(vOsf|9O;ZLfHr>i@e$`IP!kk9FWX9;B&}>Se8~X zth3%jJ4f=;?qC0p&#qsVFU5e)$q^8fIY9glXck}n%fY%%bQq>c?~wv5qM-UfghJ}A z;KQO@rUPw8X4jkFC{Di=en?OkAgsTviP^FCo9i)ubO1KbDkE+^{;CI~1J_XzYcyXe z%{~3HgB_qc8V+2iC@&!%urNCLZC>f1S+Q|)(~E0=zI%wl z@$8@o*-KCmk96xOV?$)6pP)a*pS$;W?&Rb`XuxooA@+gdyU*7bVv4Sx^`pQ`t4s`} z#jgFi+aN6lf!-}S}0x}Jz7sK76{$IO4(wHg3uX1$gs1d zl}m|Gtu{euzi@u*joinmqhBDU_?lok|A^A#3#j+=(=CT{yDBhJlZ6!s022B~jkpx{sM@Yhor$0XT-M1m6 z@-Jx`9wQ85d?P_ZxJVmLw$92n6NQxh;1aZCmQJ2*p;OZNz(xz^Mm()6Jm>SyY%Hz3 z=IbnkfO!XC=7ie$SFd7!RV*RA7#IiSr51VK5ZvKV8MHhP^reCEd!{UoM_c!DLgmiC z&fsQh{m&yec6<(Zcx(1_?hv{Gmv}$aaENu2VHl_O9lj=wM)o`ZCVCT2fUZ>=dHp2N zWbQ+YIGFTD?n|r|Od(al)ET+Eq8eZdj;JHdme@QX6uMRUOaDm=0YqyM(D@D`kp=it zneD&INTh%WPOgrS&2nLCCr01&{P*QW0ih!vTd$%DjdPMU1D;_9^iO%vQeJIy(sj0m zuMN=Abt6s{ssPH5IsxTv&Dxm9RY{}c&AU)NOmyQah9(?XkGs7=5o`d4BM;$wZw4Ch zDi;Pc9aCqm-RF`D{Bk{~xaSP0N7AjlkQiV})cO_Z3fYj6r0)SLk!=bp+JP%lnb|-a ztt4=c{5fQBT+d*T5*W=O`aNDBp(~NJ4-oj**BJm+JqIk$&A~Rqp{F*^NyZG93^miB z=5-{U`oQLC>-n*x3BxIZX2RHvB~x861)yz}hmVEKzuyIz z7$RaMUs$#?8}=55U>58r_1qGKRgJqk3j~~=a%h1dWVDs6) z^f!aXb>n%pSF@S8V>=r*G_&}tu5ofA5)HdLV8=+@JvZo@&sLx+pqDqRGIZ0eqz59+ z?s?@vHSvxXJ{{)l@iu!V4I2zxXSwy;KcDl3tP3zw-UDIf#I7I17l0A2FyT?Qa`wHG z_Rl7NXe#)%Uq)_8Kpykb%)jC)3J;|i`l(LfTn#4W0(I;q8m9XG${|7l81Okk+Pf{# z`(z;0#_Bq_3bGx7pnN}Uoo)BK(f|$a6p(?M3iN0z_B;TZj%sdKj+h11PHk+l5wx6$*NXMk2$YNBJJr zgFq?D19bz$lJ1ATnp1fX6l`w8cX0EKD-Rb2x@FhYsW&^PUV(zy3NGX0!SE_Xea;^+ z>k5aq2A&1TH=9T7gdT3ov9hCo4jDv>3Ji?SGg5mIr z=ktfQA?er=&{O9*cj2h|ug7q+3>LN)S`}~<-UE}Y`6ZguHfQ23Q9R> zn2tz%5JjpXhj?^~ec||Z`n4aH0VB#{!5OF>P%VcFeRIWa@Hlk0Zrk^#94V;B0J1s< z;w4>(1abU6Xn#g{arDo&phA@(5q<1U46|OM_pW{B1Nbav5Y^R+4=UdHz4w8y^yVgN zO*1M6fZ2`g#~t8C-BiRrn+rumbW0hJE0?R6;TNxyQ@9r|{#qpLRkQAZV|H84S84Bl z>9q>UXXW)@3!gY8f?dAOu#!!~Nc|}9YW3sOZ{81i|5%?j7UO!|Tedr9s`-s;Ov#(J zzWXKbPB?OZnt1rGqRB2f+6FS$mXq}_A{zJJ#yD@5g}i`Kyp&cY$yPF9?2GhQ;z~f7M<-_j~Ld^=tir&Lq!G=%F@2%sc|d z1E-+jX^hIV7e{|?|2`pXDA)+_+a)EE-J3;WH2vAZdE@L~nR_$DQsP~WDF{B7Ve+^c ziIViMmv8#@KHskx2ff*tpFZA5K-r^oB)aK%l;ek%_G$MpC8(539vI5ce=6cqF8wG) zX)fWV>y+iVM5EmpI@``JZa{CfENP`F=y~UDKY{V_ocp8Q4e5R8spZ=ixP16<8M zq@`zG*)i7oNSM^~`-8x%VA+wsACi~cK3coyRJC;e(MRd;|2&kcUDG!MZ!+48K=0`I zC~QV+l)J7;Iz#d3eI)Mq@2`2y$^sy*eSpp^7;3bq@Lu@4W4xV~w(q8(&y<~tg^|dL zD95}*E49~vV73LUJ)6f$um3qIY?uiQ3}3E+?yxjahsVBr-4gKoZ4|)Fc4thO+|LVN z6pAMDOTT6Ozqg?MVrq2P8R&GUv}bUG-tAMCfBpTh3w7)~Ye3&z0j+%YGB=Nj{W%-d zN_KF^z(Yf+yB&ZfryyQD1!%vajdcCG+0STbiGdzQ%i{dcZ>IpSUZwPS;`J(J7i&=W zN_uI46A5b^?@KkV;hx6=Ed6y>sQ0mqhdV$aDi~T70#XRtHSuEKKfWSJo0oIkTG$q^ zMiEd-TF==4@(?;Vfj{y&bN zqvM=YI;V3g?LsKB?wi}rNW&h9gd4fJZ+i>L=&qv~&wTFh6a-1A$~+H16f}j{k1cY5V;6g6r9K_c&HI9XFdSxs z7+@(#q4y_Hyk{p;m5X?8H0S)vGU;s2hc)m0jOBRP2W*>|qiKXo?^Vt2$07fHe2fmmT`z6DUiz&1S=-{^5TG0=*cxX-Y8@R-Ifju{_wGeul`Zl z{s>!{T~4nICvwu=?^o-?nzgNo5nmY))qaZz7%_}4?{>NZCYwqq5VXy6v5%>ypy<;v zn4RA_!K7Yj3Jecs2fUko_p<7IAZSBajv;d2*8#wXaUEP#|;F z*HGN}O3&guVLQ+isv!1cD{$j0=^oRmn%%H-+DxTP?bq4i4oF;9s+oyrO5Lw?>z`Jl zgYq|^-018EV)5SEAjh{iWGjJ#WzzBu|3m2@49N(rp%e9<%K~7o>7QE~_uOy=L$|v} zsZenj$6GwrxJ}ra2wOlOC;?EY3@u&kKQIoceeGhb`4`j)1d_;ST?cZ;2t5&wWs_tp*lt1qEUC+^9f5c$WeED*z35KrLn! zL<{-~xGqrBwGVs`ezJScviV7VdcRG=)iF2Se3M*CMY)>b=bof%*R9`%V@6=C3aWAR z0buziSiYM&9E-vT!Gp? z0ng-*-iv;0#xS3XSbcRKNr z2ZuYMjxQ8_9m#(mcP*3l-WWyZj~nHv@!;`162{5N(+WU;xuUgXCgH|LS*H{sg0BOU>@Bcu&G`RWIvF``jE+NR z*LuybSXrY=aq$# zB(T1K`k;u{-UwX&Z8XohdfH-rqgg}a!3J1QxIT_glZok7@T{8aIufW@q50Z zun&$Sb@#3ojTLfP1DKlv&m=Ku4J)7NMGxgbK_|r*|PCnve z`K-&IT1$^XVTw%c9hNIgF}n63)abEmH%qF3LRqBJrI~96i@jUhN>Y^nOTah+D2K(M zCUr0D1926sxKCi)+=x+h-PJtkI0LeQ*;wvItV)s~;Y9r3i5M*X3vvEV08wIxe4cGv ziMHj(S_U4k48@QNTUZK#d2Unv!AT_$jwC0B4QOH=S~+Xk&cEcEYx5ifu9R2D^f=AsM)QTG+~ELqQ=QTPAy7@#r-^|KU-NH&W!@9Tk6 zhA2@~lb}KpO(j@`SV#Z(YsY3pD_+0nV1D%m1~$!lP0lZrQ_;nv~)`qFKNk%HxA88*jNOvK&Ep(b37@HhUJ>@y36YE|W;wdp( z^1=i3%{G(9)XY9dUS~;XXtiiD$eBFiKIS&YsX$K2f~xn1@z9PWZJUqv z{iXH0liXwbI;R)i*QIOBNIFY;M*F2sD)iu!iO}`Qi55_2{OkMuCT9mlS}eG@1AI@W z>+hCYe6!&WSV|^}$K5w<{{}sNlVo|&VO>-K+dgaq-S!NqOssVhvqYJ%XbLj1pVaak zKvnwf7sz|gF5hS$sa!0(Rs10QEK={-I^mKvNu zI~bd&->_42Ms}}Mc93{o&%hti_Elyh2cqif(>^}d@VanH(<|TpP=?Q&^b|T{O|SZt zbs7Cp6`j*E^`@Hjo}aRA7KIGS$HsT2f&nvHOl6=k6)C%h*(9y};y;j6LJ56Vuz}s#uCx9x3?V3Va zN@F^ae`W6c_wE|%X8x3I8=uqvIZ1o&3CuJ4QyS-#ghr%=ru${hnKt6K6!4Q~Knx-G zFJbze_kzUJ?z=Ls!~^JkKzmN;LSUYejR{|X+l8sj~S-IWBSdlM&d;|=|1O|Zw?VBt7Vuii0MPw%3w9yTobB9M@+3XtS_m{I^LbLgC z=KDH!PyUsC0-6*FsxA5=%uiP+2jM8+D-8QTz%q)1;8!F9_JTgJdq1U&K3(E4X!D=c zovFP*b?2)J zq#Oo`uWk6b{Q`N=kb&~YqA8Y!%2clK080!IfqU)u!TI^iSVFJ6i0^h3f9OClMY+O$W zwknvMZ*8)xm@IIZU(hh9n1os|ku`etJzK@$+ zTlZ9BeRY2wWBY)No7W@vV0t<(@AZ>C0~F;NcV5peIn{m+EO$Buvr>Y`d;W0!MyfBX zT)Tb;0U|vH^GcVj)-y>SM0n5j)E7yZ-ax%TX9LlRTj)H%pp)q-E$Oj12P|7QIQn0? zkJ8_eUo~|)&P#zz>dD)5QdFL@x7;w`ot31yEf>gC1$&)c7pZtl)9ZqLtNm-ni$_|v zU-2~aFRe>I2CpW_GQuYr&JYvXb zIjLI#lxHh=jqa-}GC#YxLTNwK;_qJ~UAz&oxAiqD^403^k8&aHa-V9LPFdQah;pZ) z@G?N@oooL1+ZXm^J^~)8UW7`6yHu;~FKYZJp-1gl!|!|jLb#|5BVe$ZFrZBvOw<@G z^CJ=+=*6b5_ZF(6FEH174KsI*aQF?F-x0|K4~}=RMjj3pW|T|mnoxs1nZKWo&r${o z|AO2PpyqlNsKJx@Zvz#3SM>JQ54$zMNQON6mf<<L?mG=f**_!cSL5K9D=*6Eq2ALDL}utkbW^6~50~v;9Qs4h_TMhdc9p~R z0YY14$GeM-!2jF;wYyG1v=nS2msuUYj;&?PTUrI#B)0)Vb1W(Gn~&&#EKUIPvH~-et^V(yF6gwvX^82tgNTbN z*h6(2VoorEvbNtrB;w$OVoO&EC)8oGNf3ZRU&I`s(R1w7xi3bAAHXBQmI?%vyJVqj zymEubhl}}8V%rD=*}sTKX3^)QN7uQ5u+sp=!Y!ZW{=Hl5$T|+j_TK`{oewsv{Ah+h ze+gKO5ekK9I+ks@um>D3g#2iI){y^U4&ZwL#x7IPMYwgcIdZ#QhbF|)iZtTkdhq_%D^VEyJxOa7$qM_p*f)r%ck>!ju?71UzdiB;!#Z( zs=-%4L34&~4d!I)$|rh1fU>Xna+ie)Q5ixiB5y8%QC*<-wn@}gh-`VOR-3Xy?^8CK z0%9vgiV3RY<{*ZR=TG*4x1wJ-`;Vp}Mt`mEE>ZrHPc7+{u(QY>Bwie-aqCx*@TY@) zBz?eTd|*M`1u}S0++dPD75=kzBb=rphBd?oBm$O)KG;L^8Xy4S^j~@yEjq&rj{fj% zQXXy*km@!8iG5oiCQGY#`Ccvh`o%vOu-aidPT#2tJ`X%X`XTAl4q1W6!uN4t zKij`aqu1j6Pt!YyK7x-a|!w)cr7+5LyXfbMhYr=4TH7i**BN zZ6YD`62?*qG-p_|<1;R3Q2{Kfz&x|vbM7%yFCZt(u*a;44&XP;B|*#SgKA;A`3_pnK#iv5Fd;RxsMvv`Ow+$Qd1SuPH|z5IMW z{Z9xuI9T?7sI~DBZ~}nMAIYrgg?RS+1wJh+00UKEfuSQ*hXmWVO-RuurRWoW3p^y9nD;b4YQ_lalYQu55%@2{T#i|2*vmw<}<>+niEW{zWMLMEY>+4W=<~g%XIo; zq5Z)+na0pTJcO%d@$^eO2&#lF(;qfSGdhN2cyY1hHwTnRx(!kI!$D-P5Y)(7DCtAyGvyAi|pHQvIzuVsG=Okqt~|I;~TXSD^| z`vf)>%WS)$&hczzO`A)~z-Y(_ai{)Pd0j%hFfSJn5B$=E?H@nIA#=6YQp{6_;Tl1 zN=GLJK`XtGq;U=8k4ngad$M8{11lwff=E$~{Qk(z5R&D#YQhO#jlGDU1wj%-!4TBS z2wNe?2k8Hnn0kT4ctqG$Zl3_nrXVIrI`EHgVqOrSA1^i>`Wy@JJuRWSvVTCdUA*C= zONbha@bvG{-thA7Er@LqiG}eKeRpfMP=$Ds5kKY~*zeK~d{4sPpgqoQ?!zg_Ok8}s zU+OBWpGe&6TKd2tC(F+70x%F|l@N2EaU*A5WQFSOrf!xA6L+?b zzr7B&syw+$mW(nZMCeB-@i^qi2AYL>qf-PvcqqkstP|{{mNp=fO_u|K%EAuTXbvmJ<-@garExAk0-@ z&(OIjnEDy26kfkF^1lvI$Vi09KM>c*yDFhPp4^U@t^e3%D^0Y2#{S)s!&bF@i0}`> z0}oho3?A&Hlb=2ll!LLs@8Rb&NtIs1I0tCI4KbRphwYnqp;pK5DKQ=BBF0$Lmn`=4 zA%58H5czB{0tKa4P3d8o|3%ygkf6R7k#TIk6U89B8xKxO@P(>_S0ZVZl>P${5K(mw zPM+P97nv4gjN21fuz^OjA6e1xQgdo6Ts#5jZ&$|4FgV`1lL2NuG z!x6a=iTx7dTA`wU|8~rO&u)m=vOs*?NJ7g8KafOl%+`m#7v#QwgLLg$SO(9lsO*Zk zO+i!FbYM(fp%!=5ndzwzdsXQIi-g#=Rwsj*6_d@{ zWze~=J_(V3NI2iA9COVE$#rau?CL*TlwYXo8}lLJp$|lXYe+yCm|`^*l$*9-Y&aLZ zJyVF+8Js-Fv;9{$ya|8LC%}Ssf)p^`3-o~}A<>Kh9X^LH$jqO>H@yRhI#;_aH9qdh zx*wzT3I$SApv&uW(nUmAJJ|i}2?!n9-wqEP^g4*8X9CiwH;>+tRzJR%{(G?dO3JVy zuu~C-KExZ?R6w-YR@kGS@B8s7)FkriWZyMqq7U@h$ocx3D zjUfIQLd^Rh9mpSSd!g!0*^IC^jm*ZkUtHX6cYvY?z~7U0^|dq{Gbl>j~@`41^KuwJArVd}VYZI73eLxpX zGu^r>LK4p;^T+ya|E#zcLcRqPAbQ&YRj+Pd&zrlJjd7Fx&pjD|8|4%$5TgvFp{c2l zHg3acc^#eXF1sVo!-e^xAnYox3rsPvY4J}dErIO==*&pm3gd8VI$r|iGaa{}BCIC^ z46?8rheCtLs`w1Zkq{ar0;$Ma3bl2s4t+m%6rt&V z=_6T!4Ulc+0{N`hbZ)V%=98cbP+!2|zyFmxkZ`C0onwx3RM=)%7C0__V6{j5nSh_H zx%8UNE)YlC3keKvh&l7u8U0Wc56Tk%eFf~&2x@3-m1`#~jw$@V*bXv@(pKxx2znsQ zVDa3SS%?{$>8ap0;Y2F>LDnF*xzV%SRj_|V;o@_9!$2pWK;Xcn95NqbuzL@gzS~#x zMM&|VUfB0#qn!D9qx2et9z7WREw{_51Ib(69w^lHPyWwL-M`Z1u-k%AS_PqsTmc%o zWkZ;!GWbs<-WqJRhTd$dfBY1Y`uE#H8b>AQHn0XZrqiezuW%{`QhysD&=OG~8cqr! z+4J6U2|pJA&r4tZvlBcy;CcT6)9+cWxv@773|5s%z-I8VL>R8=P<(wJ-pd^9|5J;zAXq z=d23o=0LyLf;iM+5AfVCKquYBa8Ean7d*wmwE+Yu|iWGLTeuCq>vR+&h)A7lg>w>K7jKH3pRs zKlOMH;^psv*Yz6_i1P#FpI0H~S*YIS3pFOTtWrpeUK%l(Xo_AYfMa9_xC9S;l{6I5 z1cAn$Qs4?s4?$O2rMU4L(uM5ewF99p&5|j|JYS^^?RALOFmYLGkp)Rd;11GQowW7= z@)o2(Bky?v0rz(xx{|#`=sLz(`Z;WYG*>~D3?qb}C0*O?jLo`}ap%U$%1>dCNymri^_2h=<| zm3?hhLIk*2Z!R&5frsWjB#3^yI*3h5`=3FO@vnpFVC$+tNRWyoH6fCf+_@G1o|*t1 zNKO6_8sI)O4IK5LIBbjpd8C}K0;^Db56~fG72;R;RV3%%&H-$0Mx1m&c8muaOER^R zv-QLg;q`Ua+P9DJ^9${gQDA?B5-rJ%YAU>UPiUGZWir=Sy6tlp>aqZQj&@D37<5~f zq=}SXXY}XVkvKb;1MJ4~JQ&_1iXkOnYC^J$tJntO?75+u;d{s<`QNsKe?`Xf`$&+Q zGy!&&DhS&OUJ?oOkgi<{2rI2$Dhu<7sa?u%2Qh0zWHi+k*%zTGq*S%4Kx<6r({UkZ z-rY%)X3^r&o)Ojk-C7U9e>|z`#!GWQ5Z;|sKMzV+;J3+YpKkA_rhGPQ0fTU2V-P4TpCxtv?v1`UFtq+8H-tV=mFcI7OPJY0l z3TkOSFDYtTAbD!_pVQdoKeX7TIWc~}b78Tx6e1>Rywyv)H8U(dc0*0v(_5N%Kr-l- zb*mE>H@QH>juF%@iViD}%hcvlKW_%orgnfCq~%7tBuoYue>Axx*Xl5y8g?}QT%%d~e8IbGc>v<3;}Y`a@%G6%K>$t{x|#s&Jk)66fW% z%kS=-Qh!uY=R8rbMZ!SaJ-9{lF0sPnZC-4q)S8O4fuUhQ6^Pz?ed18CPrZgm0K0H5 z;~MVZ;AecgZFb~(gIJT)6pMoMWc;v95Bq%iEe(T0EzQ_EVw+>>;&umnmUaLFNnO=!M_h ze<-y&tzS|L@1}{L3Xs~Vpeb-gkna1)^o8nF1YZZ?_-(~%7A*ncD{m6Ic zd~kmsnHOTT-mSTJ?x%F|Go>6-mu61p&FQ-K6Vd+5E$Q0~f3gM7|1!MiCnDmPThzkM zpus3w&Hgmd=Cd-O_KpscfF#QbSKvFtNy>Ob9WVgrN$RFJMsP8NVYP-|36H(S|Jktb{$-!A=YbX*F)B_E$uXw z#LUM3V*ySZ(Mz7n^47*aVs5tx7{ z4=LKc2mqy4{$^oOhPXTT1JjAXZM3%nc5fdn-7#w#stwd&s#IovcKD|ugctyso~KKH zD9NQCHgRlMA+0-Oo^pR@Oj+Xi$49eLS3H$ng-UFndO#p(mbu_vNnbGP;JmqH`)=bH83P^vxQqHf~U=?d0>O@zUw=+Qd_1CJHfRt7_@E` z5ILm(`XYM`sr%9F9o6*F%y6g{$?~qwV4I$}vk%zZtu!v~37>O(NGorA**|3ru}JLJDSyM@z5z`6U;`vSc@)22dAr?JP3OciVVCFYa+`_v7_-A=o$7e77z z>YnnjXX^D?DKR{Q{%H1L|Mvw}d61(xST!BbtPy5nZe$YvDR0M?f5%Jmm=j9~tba?H z_8hP}@Ul3$ax`n|I4puG=g)${SBM|@?Xw9TkNs8s5^AKLk2yW{xY*a<+5_?B1F~6( z%6$x%$7Zko1JZ*9V9!4Ki-O5%!Ko)@lV(Qapstc#N3s8rP#*CoC2JPk<77F1o9ce( zmBF$%w>`6OKixsE!V=fouv58iM;$GD(i%yKw3-yi~{m#*>`NH?DETBywJ z%JuO-ndSrKS&o7x)3+?U|B2+e(LK6n#-?nZNddNKyPF+LT;!>}1sdlkZr|XM#+iod z;D&o=+zPmoVUCz~=n;06->V2_hCN0yZqLN3?bH*z)%Y@x4xAE3=TWnR*nD)qa`I=V= zw=+Mk(Vf2@md9NCZc2TpBZl#{e8MeP+aBLgaL!F8}!#3U#7~=zA_rvE!FT~v$d;JJJ-ZZafhr) zPDlIKCFjTj8%7@*)my%c>741la8x^=UCRK{aC^(@|GN8fHt_qCcj0-@LQdpDeDd#FsFPFKDE78cg`c* zvSBZST>^PIWzzR~WGODn?UUCl!v!lz#x06}DH;XQ+3Sfp`arBiG0@J!@26=XpV3hu}z5wnRJ~j znTkf6xn8FC{L>fhwvQn>o>?VhwA@qKr=Q7Vv+UpE%n*0I3!$(3XnC87VQV>RHG*Ws zIZp>sPAI&&E!?wAi;dENGHE$=?fFMLYO~6Y57D|dNbV^bYvu}PG3#xrH;GLm+-Z2Q zH0uYY9XDH?JSa3V=t8@gpk0tn&WSxb9M$ll6%I^^J}P^No~|lS@-2KZsXubnyN~-!=_^qo`P%L zbTf%EyQ6SiKiYKM_qGlB*SO9RWwVGt<#neuO*$#13;IG94Kd3_M`qLXG7C?L3`Q1A zW-jx%(k_}yuqH3Q9Y~ao)|ViKN=jsre|4ytOX!=}6n*ut43@t;P14PI#`gzkOl4^N zM=tx3j0e%boJxU|L9?#Rqa)&X4Y+BxM-NWTy?vG9Q1vQDHQnWdfojF_x$*5hx`cYa zruDL%Q-Qg}_xtH@dhdSVEF`Yd-Z&s`*&xJF5O38o)PlEOj6apG`mWy-ugy%ikmFi{I zC-T(pI@80tiL_urT7oBI=nnqoTmaQ zZSt`J6RMKoUT%$V?%-J`&R9f46hXf$wIS@`>@%|i0rP(7n>SPE{~Ef z@98i;lki%89dp9@#y(YvP@dQj$!JQu{6M3n)VSvZwlHCfFnSyH_sKx4*b=LdU;PTyye_$ z$u)7XeYiH$$uuJ4AAil$6eBfXRyNm*k9l&3-{!3KY0lPe;=Yn&KRtA+)Y^TEh(cXK z+j;ffP5Ngg^w0H;(yVA>?suwaK))9@rzvC|OZa5}Yy9HEyGOFm2(?dwLZTEEV-vQA z?GUsMstt4m9xE9odZ1Q4$K_e3zlmIR-r3g;`>*+RbU3`D^qo;+r^atiNf{FeWME6! z9_(HceUi?~>krgeBl8;bDlkeX(#Y)cDs}o_4F{ z zryd%c-P0-F*KYlCpY9f3tHHVR?!UV|JgB2XXZ6v6uX5n>*yIGaN^kOe z>#;5!Z;<1W=N-*64c7|`ek9bSf0b`nroqhZlw87{<6(x$TK;3&|D>c?J&o~bDADWS zJ{TNgq?pB*kN~@D=yu$^2ejjPdMMS+gGZ})-1pJ@2Oj1v z3mG>h zdNlW-F1e%gEJN1zpuf93$6vW+y_^_&u07GdGb<|&A<9BoYGKst~SwL0gWLt`#H;+Gs%;;7CWb2ok`NqFsi5N(T!zUXZ1vG{U91T`hc2|iW&l~o`j|SgvNYqTdmqyT4R}fA3}B?gSJZHn_pVm*?2_al~CG>VDrAtE>ka>&Bh~PgioE zXo1j9Wyg^hyiUU%;tb>jtL_W4S-6v7XHDOitT7r}^lDqL@W;3?N4VaB9?G-Eepr** zUxH;cE;AiuWB6CIGiAXyzWMoedIyntq}h2U zLntriZ7XEk8Z8#ai%o)C?(5}^ZK{dZ94IN^AAXtc{})nzg2tfPUze^|#0zXbqP8=r zG(kIB{Xt6ab7{u$+Y|wXEE&dj^&+NEttT3a267uLipE;ZXD%GQmbE>pQAwdjlKAg% ztId^*GhJLdiw3V!Qxw$yJa8rf#BS{y^Vhb>Ugl|E*0qNCsq#j0+lUISE`;VAU)cEk zlv$^+(v^;Q+>fcvXS)mXifAzFd_3~M+adf!%?PYIg2$47p0@Gj zDQ*TK6FTd zXlZ6_Z|KZr?qFm6%elQyWP1m=yH{*4elPc4vbB+ov6;0AoRSAF-Q9|0XzJQ9X+jy-$f}a5%i-vSjInk8Yw#lxH(bnK?O{7X*BWd*fWB zthLem_zOo4Utqf;B1l_uL@(f2h>q|dp2ZQ^m2!tpxA+zi1|6lw7bM3MI?yH*1ERGT zk4>{c?lOlZdPS5^xr<-d-93Lg+;h3@eNm!Os-mgeh}cCB2RD>MXX@t zm@e02{zC83f}s*xN}tS|s=1yrhfl|vnnusnpbbTYu^)pl37^LI&%%uSqkYgTxOYSf z3z3v4d~3|lHOwTtxh2=SpmM~=)7&ByZ9py_8^I3bm*%sL{l;{}3!QyUkV`m1%BwU;d$-U5`p~jSKh-V{_S)3 znwr8Bg+G=e_i)0`3u!{=k&q|am_RYnZ$6G5;$z#c+M!1RGSY7c$yN5BmYZm3W5LSk zw#o6ScYVb}m%75b`=-xyj1IaBa%L@f-<*9!n(0CxGk*MV+-1o4J+;!sg@qNpL_ikRsO zJfY@DgaUh1phA@XVIh}+w&rVxQ;Ecc%k^Y)1F7P$f+{nO!^FK$h3>H&CsO#)H{jo( zUE>7GJn;PWiv8$CA@uEd%Ba5X!2Z!sO)5)^x(@qY4tuJ(bxJwd9I_h6o+a8>rzi0W zD~6v>JAV~JHEezfDR%n~7JEx@#lGvqTZO*3*oVG_+-zFoR`~uMdSYB~tN%sz3G^`> z9=#+Kut@3@s?8cMA3Txs(exmZ(w;LjJR{wicsSAowp==I#6fhlMVs;y9m@kwmB*nc zq%Lv0mj~NR+4ynxi9z*v^r0R)>XK76z1Re>`8u$-Xd3WC@7r`aYauA0x;YaIZU{0g!aiFYcFSr zfiJlMy&^n1U)1Z1HebPlfRpH%kzZ=!@X|aldP2aESg>%swEI=!X!7Bu?(8$@^MbF@f6{QC1yQnd*MROu4xtDB}y~Wn;WE1Hq-+QXnkr~@eRSphpk_fC+^=sW|BXqv-M$BR&=&2B8 zVC;5KduZgabwY@918X>e`AWmtXV>u9LW;2NGcBuq7QLiKEA_QEK7vz`;Xf8ACufS` zzL9}Q%eaX|K~M23bTB0JuV^LqD*V}Y?0kCC&xkv=@_amcYGkj1{NgZ(!HHZj=OkP~ zui%ZZ$e{=Prsu=%MY26Z+lydxOdi3=9^vhk1xfS<{_*Wop{hDw>F6_pg3qEFdaQ-6 zM>+QrDZ!$1IuDxrRK*M3?|6_7@!1K*7`%5R?hQoS65O5<9Uozb_hQPQ;YABAg|O8V zl;gz?aB1jE$mqP|gdJ^apW28<^-u+644%tQXR@gdzP{h-zAw1F{5Hn~+Ei4OG+R8{ z7nzH8j6pkD8oh;g&P~lr7ku?8LW>iHOPqkMFTV`hkf2|Fzq3`*%2wooc{&CX6e7uT zUm%Cy{Y0E~tT2iv zI5OI`V3ytovC8+AV}ohp4cR^VuI?=?UO5-6^N7~Mxx-T}_lHWX@1>w^@fgLzlZ$(t zy;GC5`^V58NEjU8g9am;U1IgJ)ZSa~w~*lCkFU0K720RDpXi8zA%-%-tms38k3b!-BP$MToVRvmHTGXZZDm@Rogdr`qM&BX$1;xcw^sV*qmUU@S9Gs zH`>>z``NY)Iw>2V9SF8ASsz&tLSI5Iv@%E&^X~T1*F3-m(}jr%Z(KhS!80R1MtMX!8vFzM6~wHf6gXElP_vPFNkPeWGl+ot}qW& zRwKE}rzD4PNYoodq(0@$AFx-nYW0a*J>0obw# z;QU@>E1keD@}q;ok#*I*i6myXi%+-(Bl^%={1)gB>FCTO7jAi_Bh2K!n6Qsi2k*Zc zGl+8?ev?0=lVx41rFghVqFTI={ptt84Hi{aJ3(pgeJbZg`!Cst5$$87;)$f=qzJSp z!4qX(=mCLHjQ*01UDMspU=S_x@Z%ewb|xEVG+_aF>;7DqyR5Qsv5jEEHS1McsA3Y{q z*9q-3@4*PQz{`I0BJM8!?Lf|X>}mD7Lt$o@P_Bd{7ic&K5GnZUxM;MYuwaC$>!I?3 zI8-3O6Z-MP(>X6R^+WMo`xkT`D7B)ku=yevu@;GnmY|(tlneU`K{p-|CqdgL=O&>g zHWiry^b!2?14Kum<>g=^`Y?{9A&C-H9PWJ96xPCNz;PBmBIt|uWDMjKCT2-I6u&gD zwdi2c>f#-H8D(%d{@JtW)Z<8(FlI)G^Veu}10ctSB}%lnwj%BoK*uGB{uK#$x@17y zJNgnmGftaKcbB7>#);oWAs$CQeJfqv@a&>%V|nq-J3~h^YFFg;mwXaL`&K=r8;7zF z-(bhahITNUPm~_c7?ejIG$g`1HC155&aREN$n>(+9|M?qmSXur#;WM=O08n z!Sm;!7X@q3A9SQRLAG1igG34j#H3jCHXd(b&~;L^ny(kVEz;~S@cJm`BQN>e&1~OkZ?I#ik z67k%NXGIQRc#kd~j=%Gjl=X;A?68R^?N(G;^h5Nm!V=hz#~ix?h8Kj4!u~4x(}v2i zwnnrm&aV1e{=6rh8FHOxq}881VB>m%VisPA@72@f7wE$T7M8F~Ngpn|{+i?H1)*CP znAzbI?qLQPnb>T+YtdG?B7*`BI zC_Lz0Lj4m3_I_;Qh11J;l>~~Z0#t%vzYNyB-)-g&HlMreRS+3^cv)@L{ggeOVuW_VZ|`?;{yACA;NdlC4-3b`FX|loh@^NHg&1Ss zxm0uvIKp-94)4Vn!?G0g5g~hA7CJWm1N+cljMWH>a=z9xzmN5b{uO-KiSlDSg-ev= zE5LmGb)qWzfS?+WJ_@pUBO!l}G@`X)LdT*edP~&h9nn!~fjJxP0e1(zh`$tbk7#d- zUgBq4RJB8|l8N@Tl`ke0GJ8KpFA8#@t?z0cxK4SZy#v|><-IxO>J!4Fs056b8P0@a*ZDcvv6qNunp`pIDK7ISr2ulE zFAxsDSEXPjqe`UY4ZKP_{=)rmyPA_~o`=mTza+mfKP#@AL)buUZVCw!9WfqG%FU({ ziRg8HZ)PPM^;z=;thfA6)%EZ1S~o|ukD7H5i9i)|4ws^D!V|`^;qVKb=a9ZsmQN(| zp>hU}oUQJT!J_D;kr|{PJnmw~M991&ANGnMT!LN&dMIRp-WEA2=d114O|)*YE$%P5 z_da><#j`{K^c!)cpwfUKyn+^omhs!z+3X&yiz4Sy@$P(A(d69Ac^LJ_Vb&i!?#LZq ztj)-H0Ef%z;yzvUln{*)1RNfvTsWaccJ>uYf(=oWNIZuTu1RU$`t0-yTP>3+<{ zz(05UG0p1n4dz9fD>?=|0Tm{2{7ctg9%kbclE***FCycabcB1JXnpQBPqz{--4N|m z0PB#)I9^-qM6&$}CA_=fCsA#i%tzQDC^CP8=GqtMD0X?2VLrj0(hfe#4cO z!CksR8c~-CsU+)8y52Mhu~qD&v0+dq5Q5Qmfg94x57N{>qAwFJS|W{IXO?;MMDWph z^A`plN-Op;oE#-IFA zP-w+CKg^H<)*`U_E-S|Ug^5Jd%0d~5MlG~gywPz%AG8hO(?gGvbh&9ov>X06<2;ve z8Y!Yi{pel%`SK1sBFR1AMnAT|xLYVn<8UEpdyv>xFpLl-db2O}_>q1^QTDNufp^2M zJds%;J&mocL}L52>%<&+03%vPk}sndLOae?=7gTBW%FvrMxv7(SA3qMiZ3pa`HiHw z7B7pexf6^3l;!oQgVyH+2=(=7Gr@ph2cor5L8-R6N19ptbqSRPi*namPujr!BMxr! zOWxAx$nXQDJ$b^(*&15UbIqpEOC;KD!WF)@9+Y(8DJ%G{Rv&JAg~iL%>)`8Po`;bh z_aBL0{xmuNzzltcJRaSO87=q&s6@wsYLdT0RJBbgVW^6@S333>^D(tSd8{S!7cW{{ zcf(5rBeNtGF#~jxaY95#HV;!ye`%qoFDB< zAoQ6h(bXLJxi~bBO9D9oO)HJPi-o-MjsToaoKo!yuOsU_adQqMtGd9&})J0dS2yR-u{P= z42BoZBCT+g0ggj2L@s%l5{aA|=yUwF2FKsF&fJYXe*`;!g#HKzo$s_`dv9`K4$sE9 z&okkmdEzaX%D8}w#Jz8cdyjE-qpc`T7hg0YM>ruub1ZSMe*1as(~Vj4AEK-S$A4Lu z+w+)h9@<`@w8DN|`6C90q>SvNr!OZ>;B_QVqIYp#7)%qM^())v8@bGD-(E=Y+@F6DHUsN>f?Nsz`}V<>C(i76H4sVms`!-@R^xU($nDZ=RU=iRc48SUeS+2uHk(#x-DFN;voA$5;xK*PVh7(Jki%5 zw#<5lt8_sx;fz3fR2D8NWWAao9YrKQy8M8+*Bc!If4bVEJcB_At+#sjG5j{3$i!Q^ zUawg;T0ZL|_l|Q#^1v0cL}5=nAFmZT$BZ81aZ8WS2Ba3mYNzv{IEE8ALFGb05if|g zAiR|3L?I9_oT)N}vG-I>JQKl~d9EGhx!1t~YVBXbW78L3GRq=4)7os^xD`wu~39A=;ytgsx)E z4i=!lM)Q|0$tv-Bq9?-WU&HQesu>t*COnTi*1;XEq`*2N>Gkvh$E-?uyr?hEE&HJj zwdE~W_EnU-;>k5hmu0Jn_DQzba*15a*7n1?uNy>ual-9K-eCvm$KgfL6AYJx+@lGT zkF_dQP7@vbbi>i-2!%H%pE}6-%wI+O!Qtfvq;BAuIV;5upih#F3o70ah6)l*!Fc>i z{!K9^sEAnI>~)Xh4}EkAg;czed!4;x9V(x#E{4LLnRd!HhVxEZ^ZC~vCrQW9>y#eOJHEP4 zMXp)aPIjHCxa}!;lg9BTGR;5ag0x891NR6Hg^!mpGLI+BdFG3Hi4#=s9iDT=p0-=4 zi79%sl%|EAl7rKcUpzmGKsfSqhnN@Wx8d+B)V9k{k1tf$X$ftP8|~^H4s)M&xs9F* zwG#@ht`R9g9m=d()P#?;s&S(AT*Z00^Ij2(1y(%m*+gO+CN1D%u@2C;8Kt2olyXef z5PV)@qu4{Vh2gmx-8E|oWAh?0XtiafdBvi literal 0 HcmV?d00001 diff --git a/docs/developers/code/projects/media/more-menu.png b/docs/developers/code/projects/media/more-menu.png new file mode 100644 index 0000000000000000000000000000000000000000..d81acd24e27604950140d12fc2a8cce081eca5bd GIT binary patch literal 157577 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