From adaa4d2f1e26119e4f04b8bc0458651f1ee9e686 Mon Sep 17 00:00:00 2001 From: vsklencar Date: Thu, 3 Dec 2020 09:34:06 +0100 Subject: [PATCH 1/6] Paging of project list --- app/localprojectsmanager.h | 3 +- app/main.cpp | 2 +- app/merginapi.cpp | 171 +++++++++++++++++++++----- app/merginapi.h | 14 ++- app/merginprojectmodel.cpp | 46 ++++++- app/merginprojectmodel.h | 18 ++- app/qml/InputStyle.qml | 1 + app/qml/MerginProjectPanel.qml | 24 +++- app/qml/ProjectDelegateItem.qml | 16 +++ app/qml/components/DelegateButton.qml | 40 ++++++ app/qml/qml.qrc | 2 + 11 files changed, 295 insertions(+), 42 deletions(-) create mode 100644 app/qml/components/DelegateButton.qml diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index 55f603620..ae7f5f3a8 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -18,7 +18,8 @@ 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) + Modified, //!< there are some local modifications in the project that need to be pushed (note: also server may have newer version) + Invalid }; Q_ENUMS( ProjectStatus ) diff --git a/app/main.cpp b/app/main.cpp index cb7727d48..6c91096c9 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -401,7 +401,7 @@ int main( int argc, char *argv[] ) 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::listProjectsFinished, &mpm, &MerginProjectModel::resetProjects ); + 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 ); diff --git a/app/merginapi.cpp b/app/merginapi.cpp index a6d8a53ec..84f57a430 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -102,6 +102,50 @@ void MerginApi::listProjects( const QString &searchExpression, connect( reply, &QNetworkReply::finished, this, &MerginApi::listProjectsReplyFinished ); } +void MerginApi::fetchProjectList( const QString &searchExpression, const QString &flag, const QString &filterTag, const int page ) +{ + bool authorize = !flag.isEmpty(); + if ( ( authorize && !validateAuthAndContinute() ) || mApiVersionStatus != MerginApiStatus::OK ) + { + return; + } + + QUrlQuery query; + if ( !filterTag.isEmpty() ) + { + query.addQueryItem( "tags", filterTag ); + } + if ( !searchExpression.isEmpty() ) + { + query.addQueryItem( "q", searchExpression ); + } + if ( !flag.isEmpty() ) + { + query.addQueryItem( "flag", flag ); + } + // Required query parameters + query.addQueryItem( "page", QString::number( page ) ); // TODO + query.addQueryItem( "per_page", QString::number( PROJECT_PER_PAGE ) ); + + QUrl url( mApiRoot + QStringLiteral( "/v1/project/paginated" ) ); + url.setQuery( query ); + + // Even if the authorization is not required, it can be include to fetch more results + QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() ); + request.setUrl( url ); + + QNetworkReply *reply = mManager.get( request ); + InputUtils::log( "list projects", QStringLiteral( "Requesting: " ) + url.toString() ); + connect( reply, &QNetworkReply::finished, this, &MerginApi::listProjectsPaginatedReplyFinished ); +} + +void MerginApi::listProjectsPaginated( const QString &searchExpression, + const QString &flag, const QString &filterTag ) +{ + // Always fetch first page + fetchProjectList( searchExpression, flag, filterTag, 1 ); +} + void MerginApi::downloadNextItem( const QString &projectFullName ) { @@ -1198,6 +1242,63 @@ void MerginApi::listProjectsReplyFinished() emit listProjectsFinished( mRemoteProjects, mTransactionalStatus ); } +void MerginApi::listProjectsPaginatedReplyFinished() +{ + QNetworkReply *r = qobject_cast( sender() ); + Q_ASSERT( r ); + + int projectCount = -1; + int requestedPage = 1; + + if ( r->error() == QNetworkReply::NoError ) + { + QUrlQuery query( r->request().url().query() ); + requestedPage = query.queryItemValue( "page" ).toInt(); + + 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 ); + } + else + { + mRemoteProjects.clear(); + } + + //mRemoteProjects = parseListProjectsMetadata( data ); + + // for any local projects we can update the latest server 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() ) ); + } + 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 ) ); + mRemoteProjects.clear(); + + emit listProjectsFailed(); + } + + r->deleteLater(); + emit listProjectsFinished( mRemoteProjects, mTransactionalStatus, projectCount, requestedPage == 1 ); +} + void MerginApi::finalizeProjectUpdateCopy( const QString &projectFullName, const QString &projectDir, const QString &tempDir, const QString &filePath, const QList &items ) { @@ -2174,46 +2275,54 @@ ProjectDiff MerginApi::compareProjectFiles( const QList &oldServerFi } -MerginProjectList MerginApi::parseListProjectsMetadata( const QByteArray &data ) +MerginProjectList MerginApi::parseProjectJsonArray( const QJsonArray &vArray ) { - MerginProjectList result; - QJsonDocument doc = QJsonDocument::fromJson( data ); - if ( doc.isArray() ) + MerginProjectList result; + for ( auto it = vArray.constBegin(); it != vArray.constEnd(); ++it ) { - QJsonArray vArray = doc.array(); + QJsonObject projectMap = it->toObject(); + MerginProjectListEntry project; + + project.projectName = projectMap.value( QStringLiteral( "name" ) ).toString(); + project.projectNamespace = projectMap.value( QStringLiteral( "namespace" ) ).toString(); - for ( auto it = vArray.constBegin(); it != vArray.constEnd(); ++it ) + QString versionStr = projectMap.value( QStringLiteral( "version" ) ).toString(); + if ( versionStr.isEmpty() ) { - QJsonObject projectMap = it->toObject(); - MerginProjectListEntry project; + project.version = 0; + } + else if ( versionStr.startsWith( "v" ) ) // cut off 'v' part from v123 + { + versionStr = versionStr.mid( 1 ); + project.version = versionStr.toInt(); + } - project.projectName = projectMap.value( QStringLiteral( "name" ) ).toString(); - project.projectNamespace = projectMap.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 = 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(); - } + result << project; + } + return result; +} - 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; - } +MerginProjectList MerginApi::parseListProjectsMetadata( const QByteArray &data ) +{ + MerginProjectList result; - result << project; - } + QJsonDocument doc = QJsonDocument::fromJson( data ); + if ( doc.isArray() ) + { + QJsonArray vArray = doc.array(); + + result = parseProjectJsonArray( vArray ); } return result; } diff --git a/app/merginapi.h b/app/merginapi.h index 103779bbc..4ba1e5a61 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -216,11 +216,18 @@ class MerginApi: public QObject * Eventually emits listProjectsFinished on which ProjectPanel (qml component) updates content. * \param searchExpression Search filter on projects name. * \param flag If defined, it is used to filter out projects tagged as 'created' or 'shared' with a authorized user - * \param withFilter If true, applies "input" tag in request. + * \param filterTag .// TODO */ Q_INVOKABLE void listProjects( const QString &searchExpression = QStringLiteral(), const QString &flag = QStringLiteral(), const QString &filterTag = QStringLiteral() ); + Q_INVOKABLE void fetchProjectList( const QString &searchExpression = QStringLiteral(), + const QString &flag = QStringLiteral(), const QString &filterTag = QStringLiteral(), const int page = 1 ); + + Q_INVOKABLE void listProjectsPaginated( const QString &searchExpression = QStringLiteral(), + const QString &flag = QStringLiteral(), const QString &filterTag = QStringLiteral() ); + + /** * 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 @@ -375,7 +382,7 @@ class MerginApi: public QObject signals: void apiSupportsSubscriptionsChanged(); - void listProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects ); + void listProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int requestedProjectCount = -1, bool isFirstPage = true ); void listProjectsFailed(); void syncProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); /** @@ -408,6 +415,7 @@ class MerginApi: public QObject private slots: void listProjectsReplyFinished(); + void listProjectsPaginatedReplyFinished(); // Pull slots void updateInfoReplyFinished(); @@ -430,6 +438,7 @@ class MerginApi: public QObject private: MerginProjectList parseListProjectsMetadata( const QByteArray &data ); + MerginProjectList parseProjectJsonArray( const QJsonArray &vArray ); static QStringList generateChunkIdsForSize( qint64 fileSize ); QJsonArray prepareUploadChangesJSON( const QList &files ); static QString getApiKey( const QString &serverName ); @@ -556,6 +565,7 @@ class MerginApi: public QObject static const int CHUNK_SIZE = 65536; static const int UPLOAD_CHUNK_SIZE; + const int PROJECT_PER_PAGE = 5; // TODO 50 const QString TEMP_FOLDER = QStringLiteral( ".temp/" ); static QList itemsForFileChunks( const MerginFile &file, int version ); diff --git a/app/merginprojectmodel.cpp b/app/merginprojectmodel.cpp index ddf34a2e7..3124e71c3 100644 --- a/app/merginprojectmodel.cpp +++ b/app/merginprojectmodel.cpp @@ -18,6 +18,8 @@ MerginProjectModel::MerginProjectModel( LocalProjectsManager &localProjects, QOb QObject::connect( &mLocalProjects, &LocalProjectsManager::projectMetadataChanged, this, &MerginProjectModel::projectMetadataChanged ); QObject::connect( &mLocalProjects, &LocalProjectsManager::localProjectAdded, this, &MerginProjectModel::onLocalProjectAdded ); QObject::connect( &mLocalProjects, &LocalProjectsManager::localProjectRemoved, this, &MerginProjectModel::onLocalProjectRemoved ); + + mAdditionalItem->status = Invalid; } QVariant MerginProjectModel::data( const QModelIndex &index, int role ) const @@ -59,6 +61,8 @@ QVariant MerginProjectModel::data( const QModelIndex &index, int role ) const return QVariant( QStringLiteral( "noVersion" ) ); case ProjectStatus::Modified: return QVariant( QStringLiteral( "modified" ) ); + case ProjectStatus::Invalid: + return QVariant( QStringLiteral( "invalid" ) ); // TODO } break; } @@ -96,10 +100,21 @@ int MerginProjectModel::rowCount( const QModelIndex &parent ) const return mMerginProjects.count(); } -void MerginProjectModel::resetProjects( const MerginProjectList &merginProjects, QHash pendingProjects ) +void MerginProjectModel::updateModel( const MerginProjectList &merginProjects, QHash pendingProjects, int expectedProjectCount, bool isFirstPage ) { beginResetModel(); - mMerginProjects.clear(); + mMerginProjects.removeOne( mAdditionalItem ); + + if ( isFirstPage ) + { + mMerginProjects.clear(); + setLastPage( 1 ); + } + else + { + setLastPage( lastPage() + 1 ); + } + for ( MerginProjectListEntry entry : merginProjects ) { @@ -129,6 +144,11 @@ void MerginProjectModel::resetProjects( const MerginProjectList &merginProjects, mMerginProjects << project; } + if ( mMerginProjects.count() < expectedProjectCount ) + { + mMerginProjects << mAdditionalItem; + } + endResetModel(); } @@ -144,6 +164,28 @@ int MerginProjectModel::findProjectIndex( const QString &projectFullName ) return -1; } +void MerginProjectModel::setLastPage( int lastPage ) +{ + mLastPage = lastPage; + emit lastPageChanged(); +} + +int MerginProjectModel::lastPage() const +{ + return mLastPage; +} + +int MerginProjectModel::expectedProjectCount() const +{ + return mExpectedProjectCount; +} + +void MerginProjectModel::setExpectedProjectCount( int expectedProjectCount ) +{ + mExpectedProjectCount = expectedProjectCount; + emit expectedProjectCountChanged(); +} + QString MerginProjectModel::searchExpression() const { return mSearchExpression; diff --git a/app/merginprojectmodel.h b/app/merginprojectmodel.h index 5c70b0510..6177e89aa 100644 --- a/app/merginprojectmodel.h +++ b/app/merginprojectmodel.h @@ -43,6 +43,8 @@ class MerginProjectModel: public QAbstractListModel { Q_OBJECT Q_PROPERTY( QString searchExpression READ searchExpression WRITE setSearchExpression ) + Q_PROPERTY( int expectedProjectCount READ expectedProjectCount NOTIFY expectedProjectCountChanged ) + Q_PROPERTY( int lastPage READ lastPage NOTIFY lastPageChanged ) public: enum Roles @@ -69,7 +71,8 @@ class MerginProjectModel: public QAbstractListModel int rowCount( const QModelIndex &parent = QModelIndex() ) const override; //! Updates list of projects with synchronization progress if a project is pending - void resetProjects( const MerginProjectList &merginProjects, QHash pendingProjects ); + //! @param isFirstPage If true clears current model, othewise merginProjects will be appended. + void updateModel( const MerginProjectList &merginProjects, QHash pendingProjects, int expectedProjectCount, bool isFirstPage = true ); int filterCreator() const; void setFilterCreator( int filterCreator ); @@ -80,6 +83,16 @@ class MerginProjectModel: public QAbstractListModel QString searchExpression() const; void setSearchExpression( const QString &searchExpression ); + int expectedProjectCount() const; + void setExpectedProjectCount( int expectedProjectCount ); + + int lastPage() const; + void setLastPage( int lastPage ); + + signals: + void expectedProjectCountChanged(); + void lastPageChanged(); + public slots: void syncProjectStatusChanged( const QString &projectFullName, qreal progress ); @@ -96,6 +109,9 @@ class MerginProjectModel: public QAbstractListModel ProjectList mMerginProjects; LocalProjectsManager &mLocalProjects; QString mSearchExpression; + int mExpectedProjectCount; + int mLastPage; + std::shared_ptr mAdditionalItem = std::make_shared(); }; #endif // MERGINPROJECTMODEL_H diff --git a/app/qml/InputStyle.qml b/app/qml/InputStyle.qml index ca044547e..738f69d4f 100644 --- a/app/qml/InputStyle.qml +++ b/app/qml/InputStyle.qml @@ -49,6 +49,7 @@ QtObject { property real panelOpacity: 1 property real lowHighlightOpacity: 0.4 property real highHighlightOpacity: 0.8 + property real cornerRadius: 8 * QgsQuick.Utils.dp property real refWidth: 640 property real refHeight: 1136 diff --git a/app/qml/MerginProjectPanel.qml b/app/qml/MerginProjectPanel.qml index c1e1eb80e..ab1591bb4 100644 --- a/app/qml/MerginProjectPanel.qml +++ b/app/qml/MerginProjectPanel.qml @@ -413,6 +413,7 @@ Item { Component { id: delegateItem + ProjectDelegateItem { id: delegateItemContent cellWidth: projectsPanel.width @@ -493,6 +494,7 @@ Item { Component { id: delegateItemMergin + ProjectDelegateItem { cellWidth: projectsPanel.width cellHeight: projectsPanel.rowHeight @@ -504,6 +506,7 @@ Item { iconSize: projectsPanel.iconSize projectFullName: __merginApi.getFullProjectName(projectNamespace, projectName) progressValue: syncProgress + isAdditional: status === "invalid" onMenuClicked: { if (status === "upToDate") return @@ -526,6 +529,19 @@ Item { } } + onDelegateButtonClicked: { + var flag = "explore" + if (toolbar.highlighted == myProjectsBtn.text) { + flag = "shared" + } else if (toolbar.highlighted == sharedProjectsBtn.text) { + flag = "shared" + } else if (toolbar.highlighted == exploreBtn.text) { + flag = "explore" + } + + __merginApi.fetchProjectList("", flag, "", __merginProjectsModel.lastPage + 1) // TODO + } + } } @@ -593,7 +609,7 @@ Item { toolbar.highlighted = myProjectsBtn.text stackView.pending = true showMergin = true - __merginApi.listProjects("", "created") + __merginApi.listProjectsPaginated("", "created") } } } @@ -612,7 +628,7 @@ Item { toolbar.highlighted = sharedProjectsBtn.text stackView.pending = true showMergin = true - __merginApi.listProjects("", "shared") + __merginApi.listProjectsPaginated("", "shared") } } } @@ -631,7 +647,7 @@ Item { toolbar.highlighted = exploreBtn.text stackView.pending = true showMergin = true - __merginApi.listProjects( searchBar.text ) + __merginApi.listProjectsPaginated( searchBar.text ) } } } @@ -688,7 +704,7 @@ Item { onClicked: { stackView.pending = true // filters suppose to not change - __merginApi.listProjects( searchBar.text ) + __merginApi.listProjectsPaginated( searchBar.text ) reloadList.visible = false } background: Rectangle { diff --git a/app/qml/ProjectDelegateItem.qml b/app/qml/ProjectDelegateItem.qml index eaf9eed8a..cc59b824a 100644 --- a/app/qml/ProjectDelegateItem.qml +++ b/app/qml/ProjectDelegateItem.qml @@ -14,6 +14,7 @@ import QtGraphicalEffects 1.0 import lc 1.0 import QgsQuick 0.1 as QgsQuick import "." // import InputStyle singleton +import "./components" Rectangle { id: itemContainer @@ -32,9 +33,12 @@ Rectangle { 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 @@ -52,6 +56,7 @@ Rectangle { Item { width: parent.width height: parent.height + visible: !itemContainer.isAdditional RowLayout { id: row @@ -160,4 +165,15 @@ Rectangle { anchors.bottom: parent.bottom } } + + // Additional item + DelegateButton { + visible: itemContainer.isAdditional + width: itemContainer.width + height: itemContainer.height + text: qsTr("Fetch more") + + onClicked: itemContainer.delegateButtonClicked() + } + } diff --git a/app/qml/components/DelegateButton.qml b/app/qml/components/DelegateButton.qml new file mode 100644 index 000000000..12637b2ad --- /dev/null +++ b/app/qml/components/DelegateButton.qml @@ -0,0 +1,40 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import "./.." // import InputStyle singleton + +Item { + signal clicked() + + property string text + property real cornerRadius: InputStyle.cornerRadius + property var bgColor: InputStyle.highlightColor + property var textColor: "white" + + id: delegateButtonContainer + + Button { + id: delegateButton + text: delegateButtonContainer.text + height: delegateButtonContainer.height / 2 + width: delegateButtonContainer.height * 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: InputStyle.fontPixelSizeTitle + + background: Rectangle { + color: delegateButtonContainer.bgColor + radius: delegateButtonContainer.cornerRadius + } + + onClicked: delegateButtonContainer.clicked() + + contentItem: Text { + text: delegateButton.text + font: delegateButton.font + color: delegateButtonContainer.textColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + } +} diff --git a/app/qml/qml.qrc b/app/qml/qml.qrc index 9c2d8eab3..ffdee6f49 100644 --- a/app/qml/qml.qrc +++ b/app/qml/qml.qrc @@ -52,5 +52,7 @@ TextHyperlink.qml LogPanel.qml components/PasswordField.qml + components/SettingsSwitch.qml + components/DelegateButton.qml From 6d7c9dec5a3bd86d1610f005bd277fb3ac4b1030 Mon Sep 17 00:00:00 2001 From: vsklencar Date: Thu, 3 Dec 2020 11:38:46 +0100 Subject: [PATCH 2/6] Paging of project list --- app/merginapi.cpp | 89 ++-------------------------------- app/merginapi.h | 17 ++----- app/merginprojectmodel.cpp | 10 ++-- app/merginprojectmodel.h | 13 ++++- app/qml/MerginProjectPanel.qml | 19 ++++---- 5 files changed, 32 insertions(+), 116 deletions(-) diff --git a/app/merginapi.cpp b/app/merginapi.cpp index 84f57a430..b4dc9eaff 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -67,42 +67,7 @@ MerginUserInfo *MerginApi::userInfo() const return mUserInfo; } -void MerginApi::listProjects( const QString &searchExpression, - const QString &flag, const QString &filterTag ) -{ - - bool authorize = !flag.isEmpty(); - if ( ( authorize && !validateAuthAndContinute() ) || mApiVersionStatus != MerginApiStatus::OK ) - { - return; - } - - QUrlQuery query; - if ( !filterTag.isEmpty() ) - { - query.addQueryItem( "tags", filterTag ); - } - if ( !searchExpression.isEmpty() ) - { - query.addQueryItem( "q", searchExpression ); - } - if ( !flag.isEmpty() ) - { - query.addQueryItem( "flag", flag ); - } - QUrl url( mApiRoot + QStringLiteral( "/v1/project" ) ); - url.setQuery( query ); - - // Even if the authorization is not required, it can be include to fetch more results - QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() ); - request.setUrl( url ); - - QNetworkReply *reply = mManager.get( request ); - InputUtils::log( "list projects", QStringLiteral( "Requesting: " ) + url.toString() ); - connect( reply, &QNetworkReply::finished, this, &MerginApi::listProjectsReplyFinished ); -} - -void MerginApi::fetchProjectList( const QString &searchExpression, const QString &flag, const QString &filterTag, const int page ) +void MerginApi::listProjects( const QString &searchExpression, const QString &flag, const QString &filterTag, const int page ) { bool authorize = !flag.isEmpty(); if ( ( authorize && !validateAuthAndContinute() ) || mApiVersionStatus != MerginApiStatus::OK ) @@ -136,17 +101,9 @@ void MerginApi::fetchProjectList( const QString &searchExpression, const QString QNetworkReply *reply = mManager.get( request ); InputUtils::log( "list projects", QStringLiteral( "Requesting: " ) + url.toString() ); - connect( reply, &QNetworkReply::finished, this, &MerginApi::listProjectsPaginatedReplyFinished ); -} - -void MerginApi::listProjectsPaginated( const QString &searchExpression, - const QString &flag, const QString &filterTag ) -{ - // Always fetch first page - fetchProjectList( searchExpression, flag, filterTag, 1 ); + connect( reply, &QNetworkReply::finished, this, &MerginApi::listProjectsReplyFinished ); } - void MerginApi::downloadNextItem( const QString &projectFullName ) { Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); @@ -1209,44 +1166,6 @@ void MerginApi::listProjectsReplyFinished() QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); - if ( r->error() == QNetworkReply::NoError ) - { - QByteArray data = r->readAll(); - mRemoteProjects = parseListProjectsMetadata( data ); - - // for any local projects we can update the latest server 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() ) ); - } - 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 ) ); - mRemoteProjects.clear(); - - emit listProjectsFailed(); - } - - r->deleteLater(); - emit listProjectsFinished( mRemoteProjects, mTransactionalStatus ); -} - -void MerginApi::listProjectsPaginatedReplyFinished() -{ - QNetworkReply *r = qobject_cast( sender() ); - Q_ASSERT( r ); - int projectCount = -1; int requestedPage = 1; @@ -1269,8 +1188,6 @@ void MerginApi::listProjectsPaginatedReplyFinished() mRemoteProjects.clear(); } - //mRemoteProjects = parseListProjectsMetadata( data ); - // for any local projects we can update the latest server version for ( MerginProjectListEntry project : mRemoteProjects ) { @@ -1296,7 +1213,7 @@ void MerginApi::listProjectsPaginatedReplyFinished() } r->deleteLater(); - emit listProjectsFinished( mRemoteProjects, mTransactionalStatus, projectCount, requestedPage == 1 ); + emit listProjectsFinished( mRemoteProjects, mTransactionalStatus, projectCount, requestedPage ); } diff --git a/app/merginapi.h b/app/merginapi.h index 4ba1e5a61..db0ccd17a 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -216,17 +216,11 @@ class MerginApi: public QObject * Eventually emits listProjectsFinished on which ProjectPanel (qml component) updates content. * \param searchExpression Search filter on projects name. * \param flag If defined, it is used to filter out projects tagged as 'created' or 'shared' with a authorized user - * \param filterTag .// TODO + * \param filterTag Name of tag that fetched projects have to have.. + * \param page Requested page of projects */ Q_INVOKABLE void listProjects( const QString &searchExpression = QStringLiteral(), - const QString &flag = QStringLiteral(), const QString &filterTag = QStringLiteral() ); - - Q_INVOKABLE void fetchProjectList( const QString &searchExpression = QStringLiteral(), - const QString &flag = QStringLiteral(), const QString &filterTag = QStringLiteral(), const int page = 1 ); - - Q_INVOKABLE void listProjectsPaginated( const QString &searchExpression = QStringLiteral(), - const QString &flag = QStringLiteral(), const QString &filterTag = QStringLiteral() ); - + const QString &flag = QStringLiteral(), const QString &filterTag = QStringLiteral(), const int page = 1 ); /** * Sends non-blocking POST request to the server to download/update a project with a given name. On downloadProjectReplyFinished, @@ -382,7 +376,7 @@ class MerginApi: public QObject signals: void apiSupportsSubscriptionsChanged(); - void listProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int requestedProjectCount = -1, bool isFirstPage = true ); + void listProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page ); void listProjectsFailed(); void syncProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); /** @@ -415,7 +409,6 @@ class MerginApi: public QObject private slots: void listProjectsReplyFinished(); - void listProjectsPaginatedReplyFinished(); // Pull slots void updateInfoReplyFinished(); @@ -565,7 +558,7 @@ class MerginApi: public QObject static const int CHUNK_SIZE = 65536; static const int UPLOAD_CHUNK_SIZE; - const int PROJECT_PER_PAGE = 5; // TODO 50 + const int PROJECT_PER_PAGE = 50; const QString TEMP_FOLDER = QStringLiteral( ".temp/" ); static QList itemsForFileChunks( const MerginFile &file, int version ); diff --git a/app/merginprojectmodel.cpp b/app/merginprojectmodel.cpp index 3124e71c3..486407911 100644 --- a/app/merginprojectmodel.cpp +++ b/app/merginprojectmodel.cpp @@ -100,20 +100,16 @@ int MerginProjectModel::rowCount( const QModelIndex &parent ) const return mMerginProjects.count(); } -void MerginProjectModel::updateModel( const MerginProjectList &merginProjects, QHash pendingProjects, int expectedProjectCount, bool isFirstPage ) +void MerginProjectModel::updateModel( const MerginProjectList &merginProjects, QHash pendingProjects, int expectedProjectCount, int page ) { beginResetModel(); mMerginProjects.removeOne( mAdditionalItem ); - if ( isFirstPage ) + if ( page == 1 ) { mMerginProjects.clear(); - setLastPage( 1 ); - } - else - { - setLastPage( lastPage() + 1 ); } + setLastPage( page ); for ( MerginProjectListEntry entry : merginProjects ) diff --git a/app/merginprojectmodel.h b/app/merginprojectmodel.h index 6177e89aa..93fd33856 100644 --- a/app/merginprojectmodel.h +++ b/app/merginprojectmodel.h @@ -71,8 +71,16 @@ class MerginProjectModel: public QAbstractListModel int rowCount( const QModelIndex &parent = QModelIndex() ) const override; //! Updates list of projects with synchronization progress if a project is pending - //! @param isFirstPage If true clears current model, othewise merginProjects will be appended. - void updateModel( const MerginProjectList &merginProjects, QHash pendingProjects, int expectedProjectCount, bool isFirstPage = true ); + //! + /** + * 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 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; void setFilterCreator( int filterCreator ); @@ -111,6 +119,7 @@ class MerginProjectModel: public QAbstractListModel QString mSearchExpression; int mExpectedProjectCount; int mLastPage; + //! Special item as a placeholder for custom component with extended funtionality std::shared_ptr mAdditionalItem = std::make_shared(); }; diff --git a/app/qml/MerginProjectPanel.qml b/app/qml/MerginProjectPanel.qml index ab1591bb4..61ea7be1f 100644 --- a/app/qml/MerginProjectPanel.qml +++ b/app/qml/MerginProjectPanel.qml @@ -530,16 +530,17 @@ Item { } onDelegateButtonClicked: { - var flag = "explore" + var flag = "" + var searchText = "" if (toolbar.highlighted == myProjectsBtn.text) { - flag = "shared" + flag = "created" } else if (toolbar.highlighted == sharedProjectsBtn.text) { flag = "shared" } else if (toolbar.highlighted == exploreBtn.text) { - flag = "explore" - } + searchText = searchBar.text + } - __merginApi.fetchProjectList("", flag, "", __merginProjectsModel.lastPage + 1) // TODO + __merginApi.listProjects(searchText, flag, "", __merginProjectsModel.lastPage + 1) } } @@ -609,7 +610,7 @@ Item { toolbar.highlighted = myProjectsBtn.text stackView.pending = true showMergin = true - __merginApi.listProjectsPaginated("", "created") + __merginApi.listProjects("", "created") } } } @@ -628,7 +629,7 @@ Item { toolbar.highlighted = sharedProjectsBtn.text stackView.pending = true showMergin = true - __merginApi.listProjectsPaginated("", "shared") + __merginApi.listProjects("", "shared") } } } @@ -647,7 +648,7 @@ Item { toolbar.highlighted = exploreBtn.text stackView.pending = true showMergin = true - __merginApi.listProjectsPaginated( searchBar.text ) + __merginApi.listProjects( searchBar.text ) } } } @@ -704,7 +705,7 @@ Item { onClicked: { stackView.pending = true // filters suppose to not change - __merginApi.listProjectsPaginated( searchBar.text ) + __merginApi.listProjects( searchBar.text ) reloadList.visible = false } background: Rectangle { From 84dbfba82a10818000b00cc4c0e97fbb14dcc5c4 Mon Sep 17 00:00:00 2001 From: vsklencar Date: Thu, 3 Dec 2020 12:11:03 +0100 Subject: [PATCH 3/6] Paging of project list cleanups --- app/localprojectsmanager.h | 2 +- app/merginapi.cpp | 2 +- app/merginapi.h | 4 ++-- app/merginprojectmodel.cpp | 2 +- app/merginprojectmodel.h | 5 +---- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index ae7f5f3a8..abe1f0496 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -19,7 +19,7 @@ enum ProjectStatus 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) - Invalid + Invalid //!< only for mock projects, acts like a hook to enable extra functionality for models working with projects . }; Q_ENUMS( ProjectStatus ) diff --git a/app/merginapi.cpp b/app/merginapi.cpp index b4dc9eaff..777ad5fb6 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -89,7 +89,7 @@ void MerginApi::listProjects( const QString &searchExpression, const QString &fl query.addQueryItem( "flag", flag ); } // Required query parameters - query.addQueryItem( "page", QString::number( page ) ); // TODO + query.addQueryItem( "page", QString::number( page ) ); query.addQueryItem( "per_page", QString::number( PROJECT_PER_PAGE ) ); QUrl url( mApiRoot + QStringLiteral( "/v1/project/paginated" ) ); diff --git a/app/merginapi.h b/app/merginapi.h index db0ccd17a..c21e8ab9b 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -216,8 +216,8 @@ class MerginApi: public QObject * Eventually emits listProjectsFinished on which ProjectPanel (qml component) updates content. * \param searchExpression Search filter on projects name. * \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 + * \param filterTag Name of tag that fetched projects have to have. + * \param page Requested page of projects. */ Q_INVOKABLE void listProjects( const QString &searchExpression = QStringLiteral(), const QString &flag = QStringLiteral(), const QString &filterTag = QStringLiteral(), const int page = 1 ); diff --git a/app/merginprojectmodel.cpp b/app/merginprojectmodel.cpp index 486407911..eb800541e 100644 --- a/app/merginprojectmodel.cpp +++ b/app/merginprojectmodel.cpp @@ -62,7 +62,7 @@ QVariant MerginProjectModel::data( const QModelIndex &index, int role ) const case ProjectStatus::Modified: return QVariant( QStringLiteral( "modified" ) ); case ProjectStatus::Invalid: - return QVariant( QStringLiteral( "invalid" ) ); // TODO + return QVariant( QStringLiteral( "invalid" ) ); } break; } diff --git a/app/merginprojectmodel.h b/app/merginprojectmodel.h index 93fd33856..2bc15adef 100644 --- a/app/merginprojectmodel.h +++ b/app/merginprojectmodel.h @@ -70,11 +70,8 @@ class MerginProjectModel: public QAbstractListModel int rowCount( const QModelIndex &parent = QModelIndex() ) const override; - //! Updates list of projects with synchronization progress if a project is pending - //! /** - * 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. + * 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 From 4d728b877f9c019bc7e97d8e2e82a68d7b914f52 Mon Sep 17 00:00:00 2001 From: vsklencar Date: Mon, 4 Jan 2021 14:30:56 +0100 Subject: [PATCH 4/6] Project list pagination some improvements --- app/localprojectsmanager.h | 2 +- app/merginapi.cpp | 3 ++- app/merginprojectmodel.cpp | 6 +++--- app/qml/MerginProjectPanel.qml | 10 +++++++++- app/qml/qml.qrc | 1 - 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index abe1f0496..41d2e58cd 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -19,7 +19,7 @@ enum ProjectStatus 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) - Invalid //!< only for mock projects, acts like a hook to enable extra functionality for models working with projects . + NonProjectItem //!< only for mock projects, acts like a hook to enable extra functionality for models working with projects . }; Q_ENUMS( ProjectStatus ) diff --git a/app/merginapi.cpp b/app/merginapi.cpp index 777ad5fb6..b0c3c19fb 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -82,12 +82,13 @@ void MerginApi::listProjects( const QString &searchExpression, const QString &fl } if ( !searchExpression.isEmpty() ) { - query.addQueryItem( "q", searchExpression ); + query.addQueryItem( "name", searchExpression ); } if ( !flag.isEmpty() ) { query.addQueryItem( "flag", flag ); } + query.addQueryItem( "order_by", QStringLiteral( "name" ) ); // Required query parameters query.addQueryItem( "page", QString::number( page ) ); query.addQueryItem( "per_page", QString::number( PROJECT_PER_PAGE ) ); diff --git a/app/merginprojectmodel.cpp b/app/merginprojectmodel.cpp index eb800541e..27e5d9aa3 100644 --- a/app/merginprojectmodel.cpp +++ b/app/merginprojectmodel.cpp @@ -19,7 +19,7 @@ MerginProjectModel::MerginProjectModel( LocalProjectsManager &localProjects, QOb QObject::connect( &mLocalProjects, &LocalProjectsManager::localProjectAdded, this, &MerginProjectModel::onLocalProjectAdded ); QObject::connect( &mLocalProjects, &LocalProjectsManager::localProjectRemoved, this, &MerginProjectModel::onLocalProjectRemoved ); - mAdditionalItem->status = Invalid; + mAdditionalItem->status = NonProjectItem; } QVariant MerginProjectModel::data( const QModelIndex &index, int role ) const @@ -61,8 +61,8 @@ QVariant MerginProjectModel::data( const QModelIndex &index, int role ) const return QVariant( QStringLiteral( "noVersion" ) ); case ProjectStatus::Modified: return QVariant( QStringLiteral( "modified" ) ); - case ProjectStatus::Invalid: - return QVariant( QStringLiteral( "invalid" ) ); + case ProjectStatus::NonProjectItem: + return QVariant( QStringLiteral( "nonProjectItem" ) ); } break; } diff --git a/app/qml/MerginProjectPanel.qml b/app/qml/MerginProjectPanel.qml index 61ea7be1f..c99f48ba9 100644 --- a/app/qml/MerginProjectPanel.qml +++ b/app/qml/MerginProjectPanel.qml @@ -392,6 +392,12 @@ Item { contentWidth: grid.width clip: true + 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 @@ -506,7 +512,7 @@ Item { iconSize: projectsPanel.iconSize projectFullName: __merginApi.getFullProjectName(projectNamespace, projectName) progressValue: syncProgress - isAdditional: status === "invalid" + isAdditional: status === "nonProjectItem" onMenuClicked: { if (status === "upToDate") return @@ -540,6 +546,8 @@ Item { 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) } diff --git a/app/qml/qml.qrc b/app/qml/qml.qrc index ffdee6f49..1f14b018b 100644 --- a/app/qml/qml.qrc +++ b/app/qml/qml.qrc @@ -52,7 +52,6 @@ TextHyperlink.qml LogPanel.qml components/PasswordField.qml - components/SettingsSwitch.qml components/DelegateButton.qml From 2865eb920f3cc8fa21b387564769f5e12e8d0813 Mon Sep 17 00:00:00 2001 From: vsklencar Date: Tue, 5 Jan 2021 16:36:43 +0100 Subject: [PATCH 5/6] Project list pagination added timer for search --- app/qml/MerginProjectPanel.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/qml/MerginProjectPanel.qml b/app/qml/MerginProjectPanel.qml index c99f48ba9..c6a23b992 100644 --- a/app/qml/MerginProjectPanel.qml +++ b/app/qml/MerginProjectPanel.qml @@ -260,6 +260,7 @@ Item { SearchBar { id: searchBar y: header.height + allowTimer: true onSearchTextChanged: { if (toolbar.highlighted === homeBtn.text) { From 9c79abdbbadf48098936d60709eddeaacba65ea3 Mon Sep 17 00:00:00 2001 From: vsklencar Date: Wed, 6 Jan 2021 10:44:55 +0100 Subject: [PATCH 6/6] Project list pagination remove unused member mExpectedProjectCount --- app/merginprojectmodel.cpp | 11 ----------- app/merginprojectmodel.h | 6 ------ 2 files changed, 17 deletions(-) diff --git a/app/merginprojectmodel.cpp b/app/merginprojectmodel.cpp index 27e5d9aa3..e2031dc74 100644 --- a/app/merginprojectmodel.cpp +++ b/app/merginprojectmodel.cpp @@ -171,17 +171,6 @@ int MerginProjectModel::lastPage() const return mLastPage; } -int MerginProjectModel::expectedProjectCount() const -{ - return mExpectedProjectCount; -} - -void MerginProjectModel::setExpectedProjectCount( int expectedProjectCount ) -{ - mExpectedProjectCount = expectedProjectCount; - emit expectedProjectCountChanged(); -} - QString MerginProjectModel::searchExpression() const { return mSearchExpression; diff --git a/app/merginprojectmodel.h b/app/merginprojectmodel.h index 2bc15adef..1668b57d4 100644 --- a/app/merginprojectmodel.h +++ b/app/merginprojectmodel.h @@ -43,7 +43,6 @@ class MerginProjectModel: public QAbstractListModel { Q_OBJECT Q_PROPERTY( QString searchExpression READ searchExpression WRITE setSearchExpression ) - Q_PROPERTY( int expectedProjectCount READ expectedProjectCount NOTIFY expectedProjectCountChanged ) Q_PROPERTY( int lastPage READ lastPage NOTIFY lastPageChanged ) public: @@ -88,14 +87,10 @@ class MerginProjectModel: public QAbstractListModel QString searchExpression() const; void setSearchExpression( const QString &searchExpression ); - int expectedProjectCount() const; - void setExpectedProjectCount( int expectedProjectCount ); - int lastPage() const; void setLastPage( int lastPage ); signals: - void expectedProjectCountChanged(); void lastPageChanged(); public slots: @@ -114,7 +109,6 @@ class MerginProjectModel: public QAbstractListModel ProjectList mMerginProjects; LocalProjectsManager &mLocalProjects; QString mSearchExpression; - int mExpectedProjectCount; int mLastPage; //! Special item as a placeholder for custom component with extended funtionality std::shared_ptr mAdditionalItem = std::make_shared();