diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index 55f603620..41d2e58cd 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) + 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/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..b0c3c19fb 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -67,10 +67,8 @@ MerginUserInfo *MerginApi::userInfo() const return mUserInfo; } -void MerginApi::listProjects( const QString &searchExpression, - const QString &flag, const QString &filterTag ) +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 ) { @@ -84,13 +82,18 @@ void MerginApi::listProjects( const QString &searchExpression, } if ( !searchExpression.isEmpty() ) { - query.addQueryItem( "q", searchExpression ); + query.addQueryItem( "name", searchExpression ); } if ( !flag.isEmpty() ) { query.addQueryItem( "flag", flag ); } - QUrl url( mApiRoot + QStringLiteral( "/v1/project" ) ); + query.addQueryItem( "order_by", QStringLiteral( "name" ) ); + // Required query parameters + query.addQueryItem( "page", QString::number( page ) ); + 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 @@ -102,7 +105,6 @@ void MerginApi::listProjects( const QString &searchExpression, connect( reply, &QNetworkReply::finished, this, &MerginApi::listProjectsReplyFinished ); } - void MerginApi::downloadNextItem( const QString &projectFullName ) { Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); @@ -1165,10 +1167,27 @@ void MerginApi::listProjectsReplyFinished() 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(); - mRemoteProjects = parseListProjectsMetadata( data ); + 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(); + } // for any local projects we can update the latest server version for ( MerginProjectListEntry project : mRemoteProjects ) @@ -1195,7 +1214,7 @@ void MerginApi::listProjectsReplyFinished() } r->deleteLater(); - emit listProjectsFinished( mRemoteProjects, mTransactionalStatus ); + emit listProjectsFinished( mRemoteProjects, mTransactionalStatus, projectCount, requestedPage ); } @@ -2174,46 +2193,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; - for ( auto it = vArray.constBegin(); it != vArray.constEnd(); ++it ) + project.projectName = projectMap.value( QStringLiteral( "name" ) ).toString(); + project.projectNamespace = projectMap.value( QStringLiteral( "namespace" ) ).toString(); + + QString versionStr = projectMap.value( QStringLiteral( "version" ) ).toString(); + if ( versionStr.isEmpty() ) + { + project.version = 0; + } + else if ( versionStr.startsWith( "v" ) ) // cut off 'v' part from v123 { - QJsonObject projectMap = it->toObject(); - MerginProjectListEntry project; + 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..c21e8ab9b 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -216,10 +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 withFilter If true, applies "input" tag in request. + * \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 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, @@ -375,7 +376,7 @@ class MerginApi: public QObject signals: void apiSupportsSubscriptionsChanged(); - void listProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects ); + void listProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page ); void listProjectsFailed(); void syncProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); /** @@ -430,6 +431,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 +558,7 @@ class MerginApi: public QObject static const int CHUNK_SIZE = 65536; static const int UPLOAD_CHUNK_SIZE; + 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 ddf34a2e7..e2031dc74 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 = NonProjectItem; } 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::NonProjectItem: + return QVariant( QStringLiteral( "nonProjectItem" ) ); } break; } @@ -96,10 +100,17 @@ 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, int page ) { beginResetModel(); - mMerginProjects.clear(); + mMerginProjects.removeOne( mAdditionalItem ); + + if ( page == 1 ) + { + mMerginProjects.clear(); + } + setLastPage( page ); + for ( MerginProjectListEntry entry : merginProjects ) { @@ -129,6 +140,11 @@ void MerginProjectModel::resetProjects( const MerginProjectList &merginProjects, mMerginProjects << project; } + if ( mMerginProjects.count() < expectedProjectCount ) + { + mMerginProjects << mAdditionalItem; + } + endResetModel(); } @@ -144,6 +160,17 @@ int MerginProjectModel::findProjectIndex( const QString &projectFullName ) return -1; } +void MerginProjectModel::setLastPage( int lastPage ) +{ + mLastPage = lastPage; + emit lastPageChanged(); +} + +int MerginProjectModel::lastPage() const +{ + return mLastPage; +} + QString MerginProjectModel::searchExpression() const { return mSearchExpression; diff --git a/app/merginprojectmodel.h b/app/merginprojectmodel.h index 5c70b0510..1668b57d4 100644 --- a/app/merginprojectmodel.h +++ b/app/merginprojectmodel.h @@ -43,6 +43,7 @@ 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 @@ -68,8 +69,14 @@ 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 ); + /** + * 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; void setFilterCreator( int filterCreator ); @@ -80,6 +87,12 @@ class MerginProjectModel: public QAbstractListModel QString searchExpression() const; void setSearchExpression( const QString &searchExpression ); + int lastPage() const; + void setLastPage( int lastPage ); + + signals: + 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 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/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..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) { @@ -392,6 +393,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 @@ -413,6 +420,7 @@ Item { Component { id: delegateItem + ProjectDelegateItem { id: delegateItemContent cellWidth: projectsPanel.width @@ -493,6 +501,7 @@ Item { Component { id: delegateItemMergin + ProjectDelegateItem { cellWidth: projectsPanel.width cellHeight: projectsPanel.rowHeight @@ -504,6 +513,7 @@ Item { iconSize: projectsPanel.iconSize projectFullName: __merginApi.getFullProjectName(projectNamespace, projectName) progressValue: syncProgress + isAdditional: status === "nonProjectItem" onMenuClicked: { if (status === "upToDate") return @@ -526,6 +536,22 @@ Item { } } + 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) + } + } } 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..1f14b018b 100644 --- a/app/qml/qml.qrc +++ b/app/qml/qml.qrc @@ -52,5 +52,6 @@ TextHyperlink.qml LogPanel.qml components/PasswordField.qml + components/DelegateButton.qml