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 8216a73b7..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,40 +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; -} - void InputUtils::onQgsLogMessageReceived( const QString &message, const QString &tag, Qgis::MessageLevel level ) { QString levelStr; @@ -503,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 ) @@ -512,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; } @@ -526,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; } } @@ -539,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; } } @@ -552,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; @@ -573,11 +475,6 @@ QString InputUtils::renameWithDateTime( const QString &srcPath, const QDateTime return QString(); } -QString InputUtils::downloadInProgressFilePath( const QString &projectDir ) -{ - return projectDir + "/.mergin/.project.downloading"; -} - void InputUtils::showNotification( const QString &message ) { emit showNotificationRequested( message ); @@ -594,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 6744a65f6..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"). @@ -182,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/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/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/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/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/localprojectsmanager.cpp b/app/localprojectsmanager.cpp deleted file mode 100644 index a9beebb93..000000000 --- a/app/localprojectsmanager.cpp +++ /dev/null @@ -1,343 +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 "localprojectsmanager.h" - -#include "merginapi.h" -#include "merginprojectmetadata.h" -#include "inpututils.h" - -#include -#include - -LocalProjectsManager::LocalProjectsManager( const QString &dataDir ) - : mDataDir( dataDir ) -{ - reloadProjectDir(); -} - -void LocalProjectsManager::reloadProjectDir() -{ - mProjects.clear(); - QStringList entryList = QDir( mDataDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); - for ( QString folderName : entryList ) - { - LocalProjectInfo info; - info.projectDir = mDataDir + "/" + folderName; - info.qgisProjectFilePath = findQgisProjectFile( info.projectDir, info.qgisProjectError ); - - MerginProjectMetadata metadata = MerginProjectMetadata::fromCachedJson( info.projectDir + "/" + MerginApi::sMetadataFile ); - if ( metadata.isValid() ) - { - info.projectName = metadata.name; - info.projectNamespace = metadata.projectNamespace; - info.localVersion = metadata.version; - } - else - { - info.projectName = folderName; - } - - mProjects << info; - } - qDebug() << "LocalProjectsManager: found" << mProjects.size() << "projects"; -} - -LocalProjectInfo LocalProjectsManager::projectFromDirectory( const QString &projectDir ) const -{ - for ( const LocalProjectInfo &info : mProjects ) - { - if ( info.projectDir == projectDir ) - return info; - } - return LocalProjectInfo(); -} - -LocalProjectInfo LocalProjectsManager::projectFromProjectFilePath( const QString &projectFilePath ) const -{ - for ( const LocalProjectInfo &info : mProjects ) - { - if ( info.qgisProjectFilePath == projectFilePath ) - return info; - } - return LocalProjectInfo(); -} - -LocalProjectInfo LocalProjectsManager::projectFromMerginName( const QString &projectFullName ) const -{ - for ( const LocalProjectInfo &info : mProjects ) - { - if ( MerginApi::getFullProjectName( info.projectNamespace, info.projectName ) == projectFullName ) - return info; - } - return LocalProjectInfo(); -} - -LocalProjectInfo 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 ) -{ - LocalProjectInfo project; - project.projectDir = projectDir; - project.qgisProjectFilePath = findQgisProjectFile( projectDir, project.qgisProjectError ); - 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::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::deleteProjectDirectory( const QString &projectDir ) -{ - for ( int i = 0; i < mProjects.count(); ++i ) - { - if ( mProjects[i].projectDir == projectDir ) - { - Q_ASSERT( !projectDir.isEmpty() && projectDir != "/" ); - QDir( projectDir ).removeRecursively(); - mProjects.removeAt( i ); - return; - } - } -} - -void LocalProjectsManager::updateMerginLocalVersion( 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] ); - 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::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; - return; - } - } -} - -QString LocalProjectsManager::findQgisProjectFile( const QString &projectDir, QString &err ) -{ - if ( QFile::exists( InputUtils::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) - - err = tr( "Download failed, remove and retry" ); - return QString(); - } - - QList foundProjectFiles; - QDirIterator it( projectDir, QStringList() << QStringLiteral( "*.qgs" ) << QStringLiteral( "*.qgz" ), QDir::Files, QDirIterator::Subdirectories ); - - while ( it.hasNext() ) - { - it.next(); - foundProjectFiles << it.filePath(); - } - - if ( foundProjectFiles.count() == 1 ) - { - return foundProjectFiles.first(); - } - else if ( foundProjectFiles.count() > 1 ) - { - // error: multiple project files found - err = tr( "Found multiple QGIS project files" ); - } - else if ( foundProjectFiles.count() < 1 ) - { - // no projects - err = tr( "Failed to find a QGIS project file" ); - } - - return QString(); -} - - -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 LocalProjectsManager::currentProjectStatus( const LocalProjectInfo &project ) -{ - // There was no sync yet - if ( project.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.projectDir + "/" + MerginApi::sMetadataFile; - QDateTime lastModified = _getLastModifiedFileDateTime( project.projectDir ); - QDateTime lastSync = QFileInfo( metadataFilePath ).lastModified(); - MerginProjectMetadata meta = MerginProjectMetadata::fromCachedJson( metadataFilePath ); - int filesCount = _getProjectFilesCount( project.projectDir ); - if ( lastSync < lastModified || meta.files.count() != filesCount ) - { - return ProjectStatus::Modified; - } - - // Version is lower than latest one, last sync also before updated - if ( project.localVersion < project.serverVersion ) - { - return ProjectStatus::OutOfDate; - } - - return ProjectStatus::UpToDate; -} - -void LocalProjectsManager::updateProjectStatus( LocalProjectInfo &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 deleted file mode 100644 index 9c1e9678a..000000000 --- a/app/localprojectsmanager.h +++ /dev/null @@ -1,136 +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 LOCALPROJECTSMANAGER_H -#define LOCALPROJECTSMANAGER_H - -#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; - - // 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 ); - - QString dataDir() const { return mDataDir; } - - QList projects() const { return mProjects; } - - void reloadProjectDir(); - - LocalProjectInfo projectFromDirectory( const QString &projectDir ) const; - LocalProjectInfo projectFromProjectFilePath( const QString &projectDir ) const; - - LocalProjectInfo projectFromMerginName( const QString &projectFullName ) const; - LocalProjectInfo projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const; - - 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 ); - - //! 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) - void removeProject( const QString &projectDir ); - - //! Resets mergin related info for given project. - void resetMerginInfo( const QString &projectNamespace, const QString &projectName ); - - //! Recursively removes project's directory (only when it exists in the list) - 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 ); - - //! 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 updateMerginNamespace( 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 ); - - signals: - void projectMetadataChanged( const QString &projectDir ); - void localMerginProjectAdded( const QString &projectDir ); - void localProjectAdded( const QString &projectDir ); - void localProjectRemoved( const QString &projectDir ); - - private: - void updateProjectStatus( LocalProjectInfo &project ); - //! 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; -}; - - -#endif // LOCALPROJECTSMANAGER_H diff --git a/app/main.cpp b/app/main.cpp index a5a928ffe..265f67be6 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -31,15 +31,14 @@ #include "androidutils.h" #include "ios/iosutils.h" #include "inpututils.h" +#include "coreutils.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" @@ -55,6 +54,10 @@ #include "codefilter.h" #include "inputexpressionfunctions.h" +#include "projectsmodel.h" +#include "projectsproxymodel.h" +#include "project.h" + #ifdef INPUT_TEST #include "test/testmerginapi.h" #include "test/testlinks.h" @@ -187,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 ) @@ -216,7 +219,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", "" ); @@ -231,12 +233,15 @@ 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" ); } #ifdef INPUT_TEST void initTestDeclarative() { - qRegisterMetaType( "MerginProjectList" ); + qRegisterMetaType( "MerginProjectsList" ); } #endif @@ -267,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" ); @@ -325,7 +330,7 @@ int main( int argc, char *argv[] ) } #endif - InputUtils::setLogFilename( projectDir + "/.logs" ); + CoreUtils::setLogFilename( projectDir + "/.logs" ); setEnvironmentQgisPrefixPath(); QString appBundleDir; @@ -358,20 +363,18 @@ 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 ); // 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 ); @@ -383,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 ); @@ -404,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 @@ -423,7 +422,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 ) @@ -475,18 +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( "__localProjectsManager", &localProjectsManager ); #ifdef MOBILE_OS engine.rootContext()->setContextProperty( "__appwindowvisibility", QWindow::Maximized ); @@ -563,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/merginprojectmodel.cpp b/app/merginprojectmodel.cpp deleted file mode 100644 index 75e6648a4..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 ); - - 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 ) - { - 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 1668b57d4..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; - void setFilterCreator( int filterCreator ); - - int filterWriter() const; - void setFilterWriter( int filterWriter ); - - 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 ); - - 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 index a1ea49ef3..bc010c6bd 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -1,10 +1,4 @@ /*************************************************************************** - 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 * @@ -14,192 +8,581 @@ ***************************************************************************/ #include "projectsmodel.h" - -#include -#include -#include -#include - +#include "localprojectsmanager.h" #include "inpututils.h" -#include "merginapi.h" +#include "merginuserauth.h" +#include "coreutils.h" -ProjectModel::ProjectModel( LocalProjectsManager &localProjects, QObject *parent ) - : QAbstractListModel( parent ) - , mLocalProjects( localProjects ) +ProjectsModel::ProjectsModel( QObject *parent ) : QAbstractListModel( parent ) { - findProjectFiles(); - - QObject::connect( &mLocalProjects, &LocalProjectsManager::localProjectAdded, this, &ProjectModel::addLocalProject ); } -ProjectModel::~ProjectModel() {} +void ProjectsModel::initializeProjectsModel() +{ + if ( !mBackend || !mLocalProjectsManager || mModelType == EmptyProjectsModel ) // Model is not set up properly yet + return; + + 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 ); + QObject::connect( mBackend, &MerginApi::authChanged, this, &ProjectsModel::onAuthChanged ); -void ProjectModel::findProjectFiles() + if ( mModelType == ProjectModelTypes::LocalProjectsModel ) + { + QObject::connect( mBackend, &MerginApi::listProjectsByNameFinished, this, &ProjectsModel::onListProjectsByNameFinished ); + loadLocalProjects(); + } + else if ( mModelType != ProjectModelTypes::RecentProjectsModel ) + { + QObject::connect( mBackend, &MerginApi::listProjectsFinished, this, &ProjectsModel::onListProjectsFinished ); + } + else + { + // Implement RecentProjectsModel type + } + + 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::data( const QModelIndex &index, int role ) const { - 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 ) + 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->projectFullName() ); + case ProjectId: return QVariant( project->projectId() ); + case ProjectIsLocal: return QVariant( project->isLocal() ); + case ProjectIsMergin: return QVariant( project->isMergin() ); + 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: { - projectFile.info = QString( created.toString() ); + 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(); } - else + case ProjectDescription: { - projectFile.info = project.qgisProjectError; + 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 ); + } + + // This should not happen + CoreUtils::log( "Project error", "Found project that is not downloaded nor remote" ); + return QVariant(); + } + 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 ); + else if ( role == ProjectRemoteError ) return QVariant( project->mergin->remoteError ); + return QVariant(); } - mProjectFiles << projectFile; } +} - std::sort( mProjectFiles.begin(), mProjectFiles.end() ); - endResetModel(); +QModelIndex ProjectsModel::index( int row, int col, const QModelIndex &parent ) const +{ + Q_UNUSED( col ) + Q_UNUSED( parent ) + return createIndex( row, 0, nullptr ); } +QHash ProjectsModel::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(); + 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; +} -QVariant ProjectModel::data( const QModelIndex &index, int role ) const +int ProjectsModel::rowCount( const QModelIndex & ) const { - int row = index.row(); - if ( row < 0 || row >= mProjectFiles.count() ) - return QVariant( "" ); + return mProjects.count(); +} - const ProjectFile &projectFile = mProjectFiles.at( row ); +void ProjectsModel::listProjects( const QString &searchExpression, int page ) +{ + if ( mModelType == LocalProjectsModel ) + { + listProjectsByName(); + return; + } - switch ( role ) + mLastRequestId = mBackend->listProjects( searchExpression, modelTypeToFlag(), "", page ); + + if ( !mLastRequestId.isEmpty() ) + emit setModelIsLoading( true ); +} + +void ProjectsModel::listProjectsByName() +{ + if ( mModelType != LocalProjectsModel ) { - 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; } - return QVariant(); + mLastRequestId = mBackend->listProjectsByName( projectNames() ); + + if ( !mLastRequestId.isEmpty() ) + emit setModelIsLoading( true ); } -QHash ProjectModel::roleNames() const +bool ProjectsModel::hasMoreProjects() 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; + return ( mProjects.size() < mServerProjectsCount ); } -QModelIndex ProjectModel::index( int row, int column, const QModelIndex &parent ) const +void ProjectsModel::fetchAnotherPage( const QString &searchExpression ) { - Q_UNUSED( column ); - Q_UNUSED( parent ); - return createIndex( row, 0, nullptr ); + listProjects( searchExpression, mPaginatedPage + 1 ); +} + +void ProjectsModel::onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ) +{ + if ( mLastRequestId != requestId ) + { + return; + } + + if ( page == 1 ) + { + // if we are populating first page, reset model and throw away previous projects + beginResetModel(); + mergeProjects( merginProjects, pendingProjects, false ); + 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 ); + endInsertRows(); + } + + mServerProjectsCount = projectsCount; + mPaginatedPage = page; + emit hasMoreProjectsChanged(); + + emit setModelIsLoading( false ); +} + +void ProjectsModel::onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ) +{ + if ( mLastRequestId != requestId ) + { + return; + } + + beginResetModel(); + mergeProjects( merginProjects, pendingProjects ); + endResetModel(); + + emit setModelIsLoading( false ); } -int ProjectModel::rowAccordingPath( QString path ) const +void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious ) { - int i = 0; - for ( ProjectFile prj : mProjectFiles ) + const LocalProjectsList localProjects = mLocalProjectsManager->projects(); + + if ( !keepPrevious ) + mProjects.clear(); + + if ( mModelType == ProjectModelTypes::LocalProjectsModel ) + { + // 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() ); + project->local = std::unique_ptr( localProject.clone() ); + + 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 ( res != merginProjects.end() ) + { + project->mergin = std::unique_ptr( res->clone() ); + + if ( pendingProjects.contains( project->mergin->id() ) ) + { + 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 ); + } + 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() ); + project->mergin->projectName = project->local->projectName; + project->mergin->projectNamespace = project->local->projectNamespace; + project->mergin->status = ProjectStatus::projectStatus( project ); + } + + mProjects << project; + } + } + else if ( mModelType != ProjectModelTypes::RecentProjectsModel ) { - if ( prj.path == path ) + // Keep all remote projects and ignore all non mergin projects from local projects + for ( const auto &remoteEntry : merginProjects ) { - return i; + std::shared_ptr project = std::shared_ptr( new Project() ); + project->mergin = std::unique_ptr( remoteEntry.clone() ); + + if ( pendingProjects.contains( project->mergin->id() ) ) + { + TransactionStatus projectTransaction = pendingProjects.value( project->mergin->id() ); + project->mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; + project->mergin->pending = true; + } + + 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 ( res != localProjects.end() ) + { + project->local = std::unique_ptr( res->clone() ); + } + project->mergin->status = ProjectStatus::projectStatus( project ); + + mProjects << project; } - i++; } - return -1; } -void ProjectModel::deleteProject( int row ) +void ProjectsModel::syncProject( const QString &projectId ) { - if ( row < 0 || row >= mProjectFiles.length() ) + std::shared_ptr project = projectFromId( projectId ); + + if ( project == nullptr || !project->isMergin() || project->mergin->pending ) { - InputUtils::log( "Deleting local project error", QStringLiteral( "Unable to delete local project, index out of bounds" ) ); return; } - ProjectFile project = mProjectFiles.at( row ); + if ( project->mergin->status == ProjectStatus::NoVersion || project->mergin->status == ProjectStatus::OutOfDate ) + { + bool useAuth = !mBackend->userAuth()->hasAuthData() && mModelType == ProjectModelTypes::PublicProjectsModel; + mBackend->updateProject( project->mergin->projectNamespace, project->mergin->projectName, useAuth ); + } + else if ( project->mergin->status == ProjectStatus::Modified ) + { + mBackend->uploadProject( project->mergin->projectNamespace, project->mergin->projectName ); + } +} - mLocalProjects.deleteProjectDirectory( mLocalProjects.dataDir() + "/" + project.folderName ); +void ProjectsModel::stopProjectSync( const QString &projectId ) +{ + std::shared_ptr project = projectFromId( projectId ); - beginResetModel(); - mProjectFiles.removeAt( row ); - endResetModel(); + if ( project == nullptr || !project->isMergin() || !project->mergin->pending ) + { + return; + } + + if ( project->mergin->status == ProjectStatus::NoVersion || project->mergin->status == ProjectStatus::OutOfDate ) + { + mBackend->updateCancel( project->mergin->id() ); + } + else if ( project->mergin->status == ProjectStatus::Modified ) + { + mBackend->uploadCancel( project->mergin->id() ); + } +} + +void ProjectsModel::removeLocalProject( const QString &projectId ) +{ + mLocalProjectsManager->removeLocalProject( projectId ); } -int ProjectModel::rowCount( const QModelIndex &parent ) const +void ProjectsModel::migrateProject( const QString &projectId ) { - Q_UNUSED( parent ); - return mProjectFiles.count(); + // if it is indeed a local project + std::shared_ptr project = projectFromId( projectId ); + + if ( project->isLocal() ) + mBackend->migrateProjectToMergin( project->local->projectName ); } -QString ProjectModel::dataDir() const +void ProjectsModel::onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully, int newVersion ) { - return mLocalProjects.dataDir(); + Q_UNUSED( projectDir ) + + std::shared_ptr project = projectFromId( projectFullName ); + if ( !project || !project->isMergin() || !successfully ) + return; + + 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 ); } -QString ProjectModel::searchExpression() const +void ProjectsModel::onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ) { - return mSearchExpression; + 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 ); } -void ProjectModel::setSearchExpression( const QString &searchExpression ) +void ProjectsModel::onProjectAdded( const LocalProject &project ) { - if ( searchExpression != mSearchExpression ) + // Check if such project is already in project list + std::shared_ptr proj = projectFromId( project.id() ); + if ( proj ) { - mSearchExpression = searchExpression; - // Hack to model changed signal - beginResetModel(); - endResetModel(); + // add local information ~ project downloaded + proj->local = std::unique_ptr( project.clone() ); + if ( proj->isMergin() ) + proj->mergin->status = ProjectStatus::projectStatus( proj ); + + 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::make_shared(); + newProject->local = std::unique_ptr( project.clone() ); + + int insertIndex = mProjects.size(); + + beginInsertRows( QModelIndex(), insertIndex, insertIndex ); + mProjects << newProject; + endInsertRows(); + } +} + +void ProjectsModel::onAboutToRemoveProject( const LocalProject project ) +{ + std::shared_ptr proj = projectFromId( project.id() ); + + if ( proj ) + { + if ( mModelType == LocalProjectsModel ) + { + int removeIndex = mProjects.indexOf( proj ); + + beginRemoveRows( QModelIndex(), removeIndex, removeIndex ); + mProjects.removeOne( proj ); + endRemoveRows(); + } + else + { + // just remove local part + proj->local.reset(); + + if ( proj->isMergin() ) + proj->mergin->status = ProjectStatus::projectStatus( proj ); + + QModelIndex ix = index( mProjects.indexOf( proj ) ); + emit dataChanged( ix, ix ); + } + } +} + +void ProjectsModel::onProjectDataChanged( const LocalProject &project ) +{ + std::shared_ptr proj = projectFromId( project.id() ); + + if ( proj ) + { + proj->local = std::unique_ptr( project.clone() ); + if ( proj->isMergin() ) + proj->mergin->status = ProjectStatus::projectStatus( proj ); + + QModelIndex editIndex = index( mProjects.indexOf( proj ) ); + + emit dataChanged( editIndex, editIndex ); + } +} + +void ProjectsModel::onProjectDetachedFromMergin( const QString &projectFullName ) +{ + std::shared_ptr proj = projectFromId( projectFullName ); + + if ( proj ) + { + proj->mergin.reset(); + 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. } } -bool ProjectModel::containsProject( const QString &projectNamespace, const QString &projectName ) +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. + listProjectsByName(); +} + +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 ) + return; + + mBackend = merginApi; + initializeProjectsModel(); +} + +void ProjectsModel::setLocalProjectsManager( LocalProjectsManager *localProjectsManager ) +{ + if ( !localProjectsManager || mLocalProjectsManager == localProjectsManager ) + return; + + mLocalProjectsManager = localProjectsManager; + initializeProjectsModel(); +} + +void ProjectsModel::setModelType( ProjectsModel::ProjectModelTypes modelType ) { - return mLocalProjects.hasMerginProject( projectNamespace, projectName ); + if ( mModelType == modelType ) + return; + + mModelType = modelType; + initializeProjectsModel(); +} + +QString ProjectsModel::modelTypeToFlag() const +{ + switch ( mModelType ) + { + case CreatedProjectsModel: + return QStringLiteral( "created" ); + case SharedProjectsModel: + return QStringLiteral( "shared" ); + default: + return QStringLiteral( "" ); + } } -void ProjectModel::syncedProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully ) +QStringList ProjectsModel::projectNames() const { + QStringList projectNames; + const LocalProjectsList projects = mLocalProjectsManager->projects(); - // Do basic validity check - if ( successfully ) + for ( const auto &proj : projects ) { - QString errMsg; - mLocalProjects.findQgisProjectFile( projectDir, errMsg ); - mLocalProjects.updateProjectErrors( projectDir, errMsg ); + if ( !proj.projectName.isEmpty() && !proj.projectNamespace.isEmpty() ) + projectNames << proj.id(); } - reloadProjectFiles( projectDir, projectFullName, successfully ); + return projectNames; } -void ProjectModel::addLocalProject( const QString &projectDir ) +void ProjectsModel::loadLocalProjects() { - Q_UNUSED( projectDir ); - mLocalProjects.reloadProjectDir(); - findProjectFiles(); + if ( mModelType == LocalProjectsModel ) + { + beginResetModel(); + mergeProjects( MerginProjectsList(), Transactions() ); // Fills model with local projects + endResetModel(); + } } -void ProjectModel::reloadProjectFiles( QString projectFolder, QString projectName, bool successful ) +bool ProjectsModel::containsProject( QString projectId ) const { - if ( !successful ) return; + std::shared_ptr proj = projectFromId( projectId ); + return proj != nullptr; +} - if ( projectFolder.isEmpty() ) return; +std::shared_ptr ProjectsModel::projectFromId( QString projectId ) const +{ + for ( const std::shared_ptr &it : mProjects ) + { + if ( it->isMergin() && it->mergin->id() == projectId ) + return it; + else if ( it->isLocal() && it->local->id() == projectId ) + return it; + } + return nullptr; +} - Q_UNUSED( projectName ); - findProjectFiles(); +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 166319e92..7444344f3 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -1,10 +1,4 @@ /*************************************************************************** - 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 * @@ -13,113 +7,185 @@ * * ***************************************************************************/ - #ifndef PROJECTSMODEL_H #define PROJECTSMODEL_H #include -#include -#include +#include + +#include "project.h" +#include "merginapi.h" 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) +/** + * \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 ProjectModel : public QAbstractListModel +class ProjectsModel : 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 + ProjectName = Qt::UserRole + 1, ProjectNamespace, - FolderName, - Path, - ProjectInfo, - Size, - IsValid, - PassesFilter + ProjectFullName, + ProjectId, + ProjectDirectory, + ProjectDescription, + ProjectPending, + ProjectIsMergin, + ProjectIsLocal, + ProjectFilePath, + ProjectIsValid, + ProjectSyncStatus, + ProjectSyncProgress, + ProjectRemoteError }; - Q_ENUMS( Roles ) + Q_ENUM( Roles ) + + /** + * \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 + { + EmptyProjectsModel = 0, // default, holding no projects ~ invalid model + LocalProjectsModel, + CreatedProjectsModel, + SharedProjectsModel, + PublicProjectsModel, + RecentProjectsModel + }; + Q_ENUM( ProjectModelTypes ) + + 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 ) + Q_PROPERTY( LocalProjectsManager *localProjectsManager READ localProjectsManager WRITE setLocalProjectsManager ) + Q_PROPERTY( ProjectModelTypes modelType READ modelType WRITE setModelType ) - explicit ProjectModel( LocalProjectsManager &localProjects, QObject *parent = nullptr ); - ~ProjectModel() override; + //! 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 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; + //! lists projects, either fetch more or get first, search expression + Q_INVOKABLE void listProjects( const QString &searchExpression = QString(), int page = 1 ); + + //! lists projects via listProjectsByName API, used in LocalProjectsModel + Q_INVOKABLE void listProjectsByName(); + + //! Syncs specified project - upload or update + Q_INVOKABLE void syncProject( const QString &projectId ); + + //! Stops running project upload or update + Q_INVOKABLE void stopProjectSync( const QString &projectId ); + + //! Forwards call to LocalProjectsManager to remove local project + Q_INVOKABLE void removeLocalProject( const QString &projectId ); - QString searchExpression() const; - void setSearchExpression( const QString &searchExpression ); + //! Migrates local project to mergin + Q_INVOKABLE void migrateProject( const QString &projectId ); - // Test function - bool containsProject( const QString &projectNamespace, const QString &projectName ); + //! Calls listProjects with incremented page + Q_INVOKABLE void fetchAnotherPage( const QString &searchExpression ); + + //! Merges local and remote projects based on the model type + void mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious = false ); + + ProjectsModel::ProjectModelTypes modelType() const; + + MerginApi *merginApi() const { return mBackend; } + + LocalProjectsManager *localProjectsManager() const { return mLocalProjectsManager; } + + bool hasMoreProjects() const; + + bool containsProject( QString projectId ) const; + + std::shared_ptr projectFromId( QString projectId ) const; + + bool isLoading() const; + + void setModelIsLoading( bool state ); public slots: - void syncedProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully ); - void addLocalProject( const QString &projectDir ); - void findProjectFiles(); + // 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, int newVersion ); + 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 ); + void onAboutToRemoveProject( const LocalProject project ); + void onProjectDataChanged( const LocalProject &project ); + + void setMerginApi( MerginApi *merginApi ); + void setLocalProjectsManager( LocalProjectsManager *localProjectsManager ); + void setModelType( ProjectModelTypes modelType ); + + signals: + void modelInitialized(); + void hasMoreProjectsChanged(); + + void isLoadingChanged( bool isLoading ); private: - void reloadProjectFiles( QString projectFolder, QString projectName, bool successful ); + QString modelTypeToFlag() const; + QStringList projectNames() const; + void loadLocalProjects(); + void initializeProjectsModel(); - 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; - 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; + MerginApi *mBackend = nullptr; + LocalProjectsManager *mLocalProjectsManager = nullptr; + QList> mProjects; + + ProjectModelTypes mModelType = EmptyProjectsModel; + + //! For pagination + int mServerProjectsCount = -1; + int mPaginatedPage = 1; + + //! For processing only my requests + QString mLastRequestId; + bool mModelIsLoading; }; #endif // PROJECTSMODEL_H diff --git a/app/projectsproxymodel.cpp b/app/projectsproxymodel.cpp new file mode 100644 index 000000000..a7d59dba8 --- /dev/null +++ b/app/projectsproxymodel.cpp @@ -0,0 +1,100 @@ +/*************************************************************************** + * * + * 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.h" + +ProjectsProxyModel::ProjectsProxyModel( QObject *parent ) : QSortFilterProxyModel( parent ) +{ +} + +void ProjectsProxyModel::initialize() +{ + setSourceModel( mModel ); + mModelType = mModel->modelType(); + + if ( mModelType == ProjectsModel::CreatedProjectsModel ) + setFilterRole( ProjectsModel::ProjectName ); + else + setFilterRole( ProjectsModel::ProjectFullName ); + + setFilterCaseSensitivity( Qt::CaseInsensitive ); + + sort( 0, Qt::AscendingOrder ); +} + +QString ProjectsProxyModel::searchExpression() const +{ + return mSearchExpression; +} + +ProjectsModel *ProjectsProxyModel::projectSourceModel() const +{ + return mModel; +} + +void ProjectsProxyModel::setSearchExpression( QString searchExpression ) +{ + if ( mSearchExpression == searchExpression ) + return; + + mSearchExpression = searchExpression; + setFilterFixedString( mSearchExpression ); + emit searchExpressionChanged( mSearchExpression ); +} + +void ProjectsProxyModel::setProjectSourceModel( ProjectsModel *sourceModel ) +{ + if ( mModel == sourceModel ) + return; + + mModel = sourceModel; + QObject::connect( mModel, &ProjectsModel::modelInitialized, this, &ProjectsProxyModel::initialize ); +} + +bool ProjectsProxyModel::lessThan( const QModelIndex &left, const QModelIndex &right ) const +{ + if ( mModelType == ProjectsModel::LocalProjectsModel ) + { + 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), + * then mergin projects (sorted first by namespace, then project name) + */ + + if ( !lProjectIsMergin && !rProjectIsMergin ) + { + QString lProjectFullName = mModel->data( left, ProjectsModel::ProjectFullName ).toString(); + QString rProjectFullName = mModel->data( right, ProjectsModel::ProjectFullName ).toString(); + + return lProjectFullName.compare( rProjectFullName, Qt::CaseInsensitive ) < 0; + } + if ( !lProjectIsMergin && rProjectIsMergin ) + { + return true; + } + if ( lProjectIsMergin && !rProjectIsMergin ) + { + return false; + } + } + + // 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; +} diff --git a/app/projectsproxymodel.h b/app/projectsproxymodel.h new file mode 100644 index 000000000..28befb1c2 --- /dev/null +++ b/app/projectsproxymodel.h @@ -0,0 +1,56 @@ +/*************************************************************************** + * * + * 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_H +#define PROJECTSPROXYMODEL_H + +#include +#include + +#include "projectsmodel.h" + +/** + * \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 +{ + Q_OBJECT + + Q_PROPERTY( QString searchExpression READ searchExpression WRITE setSearchExpression NOTIFY searchExpressionChanged ) + Q_PROPERTY( ProjectsModel *projectSourceModel READ projectSourceModel WRITE setProjectSourceModel ) + + public: + explicit ProjectsProxyModel( QObject *parent = nullptr ); + ~ProjectsProxyModel() override {}; + + QString searchExpression() const; + ProjectsModel *projectSourceModel() const; + + public slots: + void setSearchExpression( QString searchExpression ); + void setProjectSourceModel( ProjectsModel *sourceModel ); + + signals: + void searchExpressionChanged( QString SearchExpression ); + + protected: + bool lessThan( const QModelIndex &left, const QModelIndex &right ) const override; + + private: + void initialize(); + + 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; +}; + +#endif // PROJECTSPROXYMODEL_H 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/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..89e3465f4 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" @@ -47,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 @@ -85,6 +87,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 6c1167743..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 { - 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..5e38d00d2 --- /dev/null +++ b/app/qml/ProjectListPage.qml @@ -0,0 +1,61 @@ +/*************************************************************************** + * * + * 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 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.text ) + } + + SearchBar { + id: searchBar + + anchors { + top: parent.top + left: parent.left + right: parent.right + bottom: projectlist.top + } + + allowTimer: true + } + + ProjectList { + id: projectlist + + projectModelType: root.projectModelType + activeProjectId: root.activeProjectId + searchText: searchBar.text + + 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..4b8f7bf07 --- /dev/null +++ b/app/qml/ProjectPanel.qml @@ -0,0 +1,553 @@ +/*************************************************************************** + * * + * 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: "" + + signal openProjectRequested( string projectId, string projectPath ) + signal resetView() // resets view to state as when panel is opened + + 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( keepSearchFilter = false ) { + + stackView.pending = true + switch( pageContent.state ) { + case "local": + localProjectsPage.refreshProjectsList( keepSearchFilter ) + break + case "created": + createdProjectsPage.refreshProjectsList( keepSearchFilter ) + break + case "shared": + sharedProjectsPage.refreshProjectsList( keepSearchFilter ) + break + case "public": + publicProjectsPage.refreshProjectsList( keepSearchFilter ) + 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: InputStyle.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 ) + } + 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: { + __merginApi.pingMergin() + refreshProjectList() + pageFooter.setActiveButton( pageContent.state ) + } + + Connections { + target: root + onVisibleChanged: { + if ( root.visible ) { // projectsPanel opened + pageContent.state = "local" + } + else { + pageContent.state = "" + } + } + + onResetView: { + if ( pageContent.state === "created" || pageContent.state === "shared" ) + pageContent.state = "local" + } + } + + 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 + + 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 + + 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" + } + } + + // Other components + + Connections { + target: __projectWizard + onProjectCreated: { + if (stackView.currentItem.objectName === "projectWizard") { + stackView.popOnePageOrClose() + } + } + } + + Connections { + target: __merginApi + onListProjectsFinished: stackView.pending = false + 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() ) { + stackView.popOnePageOrClose() + projectsPage.refreshProjectList() + root.forceActiveFocus() + } + } + onAuthFailed: stackView.pending = false + onRegistrationFailed: stackView.pending = false + onRegistrationSucceeded: stackView.pending = false + } + } + } + + 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 ( !__merginApi.userAuth.hasAuthData() ) { + root.resetView() + } + } + } + } + + 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() + root.resetView() + } + 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/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/ProjectDelegateItem.qml b/app/qml/components/ProjectDelegateItem.qml new file mode 100644 index 000000000..d4f62219f --- /dev/null +++ b/app/qml/components/ProjectDelegateItem.qml @@ -0,0 +1,362 @@ +/*************************************************************************** + * * + * 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 projectDisplayName + 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(',') + + // clear previous items + while( contextMenu.count > 0 ) + contextMenu.takeItem( 0 ); + + items.forEach( item => { contextMenu.addItem( + menuItemComponent.createObject( contextMenu, itemsMap[item] ) ) + }) + contextMenu.height = items.length * root.menuItemHeight + } + + 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( projectDisplayName ) + 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 + 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: { + fillMoreMenu() + 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 } + } + } + + //! 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..c49798a20 --- /dev/null +++ b/app/qml/components/ProjectList.qml @@ -0,0 +1,348 @@ +/*************************************************************************** + * * + * 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 QtQuick.Layouts 1.12 +import lc 1.0 +import "../" +import "." + +Item { + id: root + + property int projectModelType: ProjectsModel.EmptyProjectsModel + property string activeProjectId: "" + property string searchText: "" + + signal openProjectRequested( string projectId, string projectFilePath ) + signal showLocalChangesRequested( string projectId ) + signal activeProjectDeleted() + + onSearchTextChanged: { + if ( projectModelType === ProjectsModel.PublicProjectsModel ) { + controllerModel.listProjects( searchText ) + } + else viewModel.searchExpression = searchText + } + + function refreshProjectList() { + controllerModel.listProjects( searchText ) + } + + 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 + visible: !storagePermissionText.visible + + // 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.projectItemHeight + + projectDisplayName: root.projectModelType === ProjectsModel.CreatedProjectsModel ? model.ProjectName : 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: { + if ( model.ProjectIsLocal ) + root.openProjectRequested( projectId, model.ProjectFilePath ) + else if ( !model.ProjectIsLocal && model.ProjectIsMergin ) { + downloadProjectDialog.relatedProjectId = model.ProjectId + downloadProjectDialog.open() + } + } + onSyncRequested: { + if ( __inputUtils.hasStoragePermission() ) { + controllerModel.syncProject( projectId ) + } + else if ( __inputUtils.acquireStoragePermission() ) { + restartAppDialog.open() + } + } + 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") + visible: listview.count > 0 + + onClicked: { + if ( __inputUtils.hasStoragePermission() ) { + stackView.push(projectWizardComp) + } + else if ( __inputUtils.acquireStoragePermission() ) { + restartAppDialog.open() + } + } + } + } + + RichTextBlock { + id: storagePermissionText + + anchors.fill: parent + + 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() && !reloadList.visible + } + + Item { + id: noLocalProjectsMessageContainer + + visible: listview.count === 0 && !storagePermissionText.visible && projectModelType === ProjectsModel.LocalProjectsModel && root.searchText === "" + + anchors.fill: parent + + 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 { + id: noMerginProjectsTexts + + anchors.fill: parent + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + 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 + 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: { + 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: { + // filters suppose to not change + controllerModel.listProjects( root.searchText ) + } + 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 + + 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: 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: { + if ( __inputUtils.hasStoragePermission() ) { + controllerModel.syncProject( relatedProjectId ) + } + else if ( __inputUtils.acquireStoragePermission() ) { + restartAppDialog.open() + } + } + } + + 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/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/main.qml b/app/qml/main.qml index 888250e98..f69a635bd 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -245,21 +245,22 @@ 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 + + if ( __localProjectsManager.projectIsValid( path ) && __loader.load( path ) ) { + projectPanel.activeProjectPath = path + projectPanel.activeProjectId = __localProjectsManager.projectId( path ) + __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 +442,7 @@ ApplicationWindow { gpsIndicatorColor: getGpsIndicatorColor() - onOpenProjectClicked: openProjectPanel.openPanel() + onOpenProjectClicked: projectPanel.openPanel() onOpenMapThemesClicked: mapThemesPanel.visible = true onMyLocationClicked: { mapCanvas.mapSettings.setCenter(positionKit.projectedPosition) @@ -582,26 +583,25 @@ 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: { + __appSettings.defaultProject = projectPath + __appSettings.activeProject = projectPath + __loader.load( projectPath ) } } @@ -680,17 +680,11 @@ 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) - //! 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..ac33f5ab0 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,8 @@ components/SettingsSwitch.qml components/SimpleTextWithIcon.qml components/Symbol.qml - CodeReader.qml - CodeReaderHandler.qml - CodeReaderOverlay.qml - Banner.qml + components/ProjectDelegateItem.qml + components/ProjectList.qml + components/RichTextBlock.qml diff --git a/app/sources.pri b/app/sources.pri index 7b74245a9..2d3f255c6 100644 --- a/app/sources.pri +++ b/app/sources.pri @@ -4,27 +4,15 @@ activelayer.cpp \ fieldsmodel.cpp \ layersmodel.cpp \ layersproxymodel.cpp \ -localprojectsmanager.cpp \ main.cpp \ -merginprojectmetadata.cpp \ -projectsmodel.cpp \ projectwizard.cpp \ loader.cpp \ digitizingcontroller.cpp \ mapthemesmodel.cpp \ appsettings.cpp \ -merginapi.cpp \ -merginapistatus.cpp \ -merginsubscriptionstatus.cpp \ -merginsubscriptiontype.cpp \ -merginprojectmodel.cpp \ -merginprojectstatusmodel.cpp \ -merginuserauth.cpp \ -merginuserinfo.cpp \ androidutils.cpp \ inputexpressionfunctions.cpp \ inpututils.cpp \ -geodiffutils.cpp \ positiondirection.cpp \ purchasing.cpp \ variablesmanager.cpp \ @@ -33,14 +21,8 @@ ios/iosutils.cpp \ inputprojutils.cpp \ codefilter.cpp \ qrdecoder.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 -} +projectsmodel.cpp \ +projectsproxymodel.cpp HEADERS += \ inputhelp.h \ @@ -48,26 +30,14 @@ activelayer.h \ fieldsmodel.h \ layersmodel.h \ layersproxymodel.h \ -localprojectsmanager.h \ -merginprojectmetadata.h \ -projectsmodel.h \ projectwizard.h \ loader.h \ digitizingcontroller.h \ mapthemesmodel.h \ appsettings.h \ -merginapi.h \ -merginapistatus.h \ -merginsubscriptionstatus.h \ -merginsubscriptiontype.h \ -merginprojectmodel.h \ -merginprojectstatusmodel.h \ -merginuserauth.h \ -merginuserinfo.h \ androidutils.h \ inputexpressionfunctions.h \ inpututils.h \ -geodiffutils.h \ positiondirection.h \ purchasing.h \ variablesmanager.h \ @@ -76,6 +46,8 @@ ios/iosutils.h \ inputprojutils.h \ codefilter.h \ qrdecoder.h \ +projectsmodel.h \ +projectsproxymodel.h \ contains(DEFINES, INPUT_TEST) { diff --git a/app/test/testmerginapi.cpp b/app/test/testmerginapi.cpp index 928cf5903..e5554fc84 100644 --- a/app/test/testmerginapi.cpp +++ b/app/test/testmerginapi.cpp @@ -1,12 +1,12 @@ #include #include -#include #define STR1(x) #x #define STR(x) STR1(x) #include "testmerginapi.h" #include "inpututils.h" +#include "coreutils.h" #include "geodiffutils.h" #include "testutils.h" #include "merginuserauth.h" @@ -16,23 +16,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 +127,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 +140,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,23 +171,27 @@ 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().projectFromMerginName( 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 - QVERIFY( !QFile::exists( InputUtils::downloadInProgressFilePath( mTestDataPath + "/" + TEST_PROJECT_NAME ) ) ); + QVERIFY( !QFile::exists( CoreUtils::downloadInProgressFilePath( mTestDataPath + "/" + TEST_PROJECT_NAME ) ) ); } void TestMerginApi::testDownloadProjectSpecChars() @@ -253,7 +269,7 @@ void TestMerginApi::createRemoteProject( MerginApi *api, const QString &projectN QCOMPARE( info.size(), 0 ); QVERIFY( dir.isEmpty() ); - api->localProjectsManager().removeProject( projectDir ); + api->localProjectsManager().removeLocalProject( MerginApi::getFullProjectName( projectNamespace, projectName ) ); QCOMPARE( QFileInfo( projectDir ).size(), 0 ); QVERIFY( QDir( projectDir ).isEmpty() ); @@ -311,7 +327,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 +337,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 +369,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 +388,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 +398,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 +425,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 +433,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 +452,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 +477,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 +499,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 +581,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 +599,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 +639,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 +657,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 +698,7 @@ void TestMerginApi::testPushModifiedFile() QString projectName = "testPushModifiedFile"; createRemoteProject( mApiExtra, mUsername, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + refreshProjectsModel( ProjectsModel::CreatedProjectsModel ); downloadRemoteProject( mApi, mUsername, projectName ); @@ -673,19 +714,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 +738,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 +755,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 +789,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 +808,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 +1019,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 +1039,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" ) ); @@ -1347,7 +1401,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 +1447,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 +1500,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 ); @@ -1476,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"; @@ -1491,15 +1545,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 +1587,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().removeProject( 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 +1651,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 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/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/core/localprojectsmanager.cpp b/core/localprojectsmanager.cpp new file mode 100644 index 000000000..58c9dcc87 --- /dev/null +++ b/core/localprojectsmanager.cpp @@ -0,0 +1,216 @@ +/*************************************************************************** + * * + * 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 "localprojectsmanager.h" + +#include "merginapi.h" +#include "merginprojectmetadata.h" +#include "coreutils.h" + +#include +#include + +LocalProjectsManager::LocalProjectsManager( const QString &dataDir ) + : mDataDir( dataDir ) +{ + reloadDataDir(); +} + +void LocalProjectsManager::reloadDataDir() +{ + mProjects.clear(); + QStringList entryList = QDir( mDataDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); + for ( const QString &folderName : entryList ) + { + LocalProject info; + info.projectDir = mDataDir + "/" + folderName; + info.qgisProjectFilePath = findQgisProjectFile( info.projectDir, info.projectError ); + + MerginProjectMetadata metadata = MerginProjectMetadata::fromCachedJson( info.projectDir + "/" + MerginApi::sMetadataFile ); + if ( metadata.isValid() ) + { + info.projectName = metadata.name; + info.projectNamespace = metadata.projectNamespace; + info.localVersion = metadata.version; + } + else + { + info.projectName = folderName; + } + + mProjects << info; + } + + qDebug() << "Found " << mProjects.size() << " local projects in " << mDataDir; + emit dataDirReloaded(); +} + +LocalProject LocalProjectsManager::projectFromDirectory( const QString &projectDir ) const +{ + for ( const LocalProject &info : mProjects ) + { + if ( info.projectDir == projectDir ) + return info; + } + return LocalProject(); +} + +LocalProject LocalProjectsManager::projectFromProjectFilePath( const QString &projectFilePath ) const +{ + for ( const LocalProject &info : mProjects ) + { + if ( info.qgisProjectFilePath == projectFilePath ) + return info; + } + return LocalProject(); +} + +LocalProject LocalProjectsManager::projectFromMerginName( const QString &projectFullName ) const +{ + for ( const LocalProject &info : mProjects ) + { + if ( info.id() == projectFullName ) + return info; + } + return LocalProject(); +} + +LocalProject LocalProjectsManager::projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const +{ + return projectFromMerginName( MerginApi::getFullProjectName( projectNamespace, projectName ) ); +} + +void LocalProjectsManager::addLocalProject( const QString &projectDir, const QString &projectName ) +{ + addProject( projectDir, QString(), projectName ); +} + +void LocalProjectsManager::addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ) +{ + addProject( projectDir, projectNamespace, projectName ); +} + +void LocalProjectsManager::removeLocalProject( const QString &projectId ) +{ + for ( int i = 0; i < mProjects.count(); ++i ) + { + if ( mProjects[i].id() == projectId ) + { + emit aboutToRemoveLocalProject( mProjects[i] ); + + CoreUtils::removeDir( mProjects[i].projectDir ); + mProjects.removeAt( i ); + + return; + } + } +} + +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::updateLocalVersion( const QString &projectDir, int version ) +{ + for ( int i = 0; i < mProjects.count(); ++i ) + { + if ( mProjects[i].projectDir == projectDir ) + { + mProjects[i].localVersion = version; + + emit localProjectDataChanged( mProjects[i] ); + return; + } + } + Q_ASSERT( false ); // should not happen +} + +void LocalProjectsManager::updateNamespace( const QString &projectDir, const QString &projectNamespace ) +{ + for ( int i = 0; i < mProjects.count(); ++i ) + { + if ( mProjects[i].projectDir == projectDir ) + { + mProjects[i].projectNamespace = projectNamespace; + + emit localProjectDataChanged( mProjects[i] ); + return; + } + } +} + +QString LocalProjectsManager::findQgisProjectFile( const QString &projectDir, QString &err ) +{ + 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) + + err = tr( "Download failed, remove and retry" ); + return QString(); + } + + QList foundProjectFiles; + QDirIterator it( projectDir, QStringList() << QStringLiteral( "*.qgs" ) << QStringLiteral( "*.qgz" ), QDir::Files, QDirIterator::Subdirectories ); + + while ( it.hasNext() ) + { + it.next(); + foundProjectFiles << it.filePath(); + } + + if ( foundProjectFiles.count() == 1 ) + { + return foundProjectFiles.first(); + } + else if ( foundProjectFiles.count() > 1 ) + { + // error: multiple project files found + err = tr( "Found multiple QGIS project files" ); + } + else if ( foundProjectFiles.count() < 1 ) + { + // no projects + err = tr( "Failed to find a QGIS project file" ); + } + + return QString(); +} + +void LocalProjectsManager::addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ) +{ + LocalProject project; + project.projectDir = projectDir; + project.qgisProjectFilePath = findQgisProjectFile( projectDir, project.projectError ); + project.projectName = projectName; + project.projectNamespace = projectNamespace; + + mProjects << project; + emit localProjectAdded( project ); +} diff --git a/core/localprojectsmanager.h b/core/localprojectsmanager.h new file mode 100644 index 000000000..9ff18bd7a --- /dev/null +++ b/core/localprojectsmanager.h @@ -0,0 +1,73 @@ +/*************************************************************************** + * * + * 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 LOCALPROJECTSMANAGER_H +#define LOCALPROJECTSMANAGER_H + +#include +#include + +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; } + + LocalProject projectFromDirectory( const QString &projectDir ) const; + LocalProject projectFromProjectFilePath( const QString &projectFilePath ) 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 ); + + //! Adds entry for downloaded project + void addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); + + //! Should forget about that project (it has been removed already) + Q_INVOKABLE void removeLocalProject( const QString &projectId ); + + Q_INVOKABLE bool projectIsValid( const QString &path ) const; + + 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 ); + + //! 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 ); + + signals: + void projectMetadataChanged( const QString &projectDir ); + void localMerginProjectAdded( const QString &projectDir ); + void localProjectAdded( const LocalProject &project ); + void aboutToRemoveLocalProject( const LocalProject project ); + void localProjectDataChanged( const LocalProject &project ); + void dataDirReloaded(); + + private: + void addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); + + QString mDataDir; //!< directory with all local projects + LocalProjectsList mProjects; +}; + + +#endif // LOCALPROJECTSMANAGER_H diff --git a/app/merginapi.cpp b/core/merginapi.cpp similarity index 78% rename from app/merginapi.cpp rename to core/merginapi.cpp index eb1622bc1..59f5ec883 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 @@ -67,12 +66,13 @@ 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; + emit listProjectsFailed(); + return QString(); } QUrlQuery query; @@ -100,11 +100,41 @@ void MerginApi::listProjects( const QString &searchExpression, const QString &fl QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() ); request.setUrl( url ); + QString requestId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); + QNetworkReply *reply = mManager.get( request ); - InputUtils::log( "list projects", QStringLiteral( "Requesting: " ) + url.toString() ); - connect( reply, &QNetworkReply::finished, this, &MerginApi::listProjectsReplyFinished ); + CoreUtils::log( "list projects", QStringLiteral( "Requesting: " ) + url.toString() ); + connect( reply, &QNetworkReply::finished, this, [this, requestId]() {this->listProjectsReplyFinished( requestId );} ); + + return requestId; } +QString MerginApi::listProjectsByName( const QStringList &projectNames ) +{ + // 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 = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); + + QNetworkReply *reply = mManager.post( request, body.toJson() ); + CoreUtils::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 ) ); @@ -144,8 +174,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 ) @@ -160,7 +190,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() ) ); @@ -236,7 +266,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; @@ -251,7 +281,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(); @@ -270,7 +300,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; @@ -328,7 +358,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 ) @@ -351,7 +381,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 ) @@ -364,25 +394,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 @@ -391,7 +421,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 ); @@ -413,7 +443,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 ) @@ -421,20 +451,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 @@ -463,19 +493,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() ); @@ -487,7 +517,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!" ) ); } } @@ -495,12 +525,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 ) ); @@ -513,7 +543,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!" ) ); } } @@ -526,7 +556,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" ); @@ -543,7 +575,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, @@ -611,7 +643,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( ) @@ -627,7 +659,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 ); } @@ -680,7 +712,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 ) @@ -698,7 +730,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() @@ -720,18 +752,19 @@ 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; 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 &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() ) @@ -745,7 +778,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" ) ); } @@ -761,7 +794,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 ); @@ -769,7 +802,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" ) ); } @@ -783,7 +816,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() ) @@ -794,11 +827,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" ); } @@ -806,7 +842,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 ) @@ -836,7 +872,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 ); @@ -847,7 +883,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 ) @@ -880,7 +916,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() ) { @@ -892,7 +928,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 ); @@ -906,7 +942,7 @@ QNetworkReply *MerginApi::getProjectInfo( const QString &projectFullName, bool w } int sinceVersion = -1; - LocalProjectInfo projectInfo = getLocalProject( projectFullName ); + LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); if ( projectInfo.isValid() ) { // let's also fetch the recent history of diffable files @@ -1031,7 +1067,7 @@ QString MerginApi::extractServerErrorMsg( const QByteArray &data ) } -LocalProjectInfo MerginApi::getLocalProject( const QString &projectFullName ) +LocalProject MerginApi::getLocalProject( const QString &projectFullName ) { return mLocalProjects.projectFromMerginName( projectFullName ); } @@ -1054,7 +1090,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 ); } @@ -1084,13 +1120,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 ); @@ -1103,20 +1139,21 @@ 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 projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + if ( projectInfo.isValid() ) { - QDir merginProjectDir( projectInfo.projectDir + "/.mergin" ); - merginProjectDir.removeRecursively(); + CoreUtils::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(); + emit projectDetached( projectFullName ); } QString MerginApi::apiRoot() const @@ -1153,11 +1190,6 @@ QString MerginApi::merginUserName() const return userAuth()->username(); } -MerginProjectList MerginApi::projects() -{ - return mRemoteProjects; -} - QList MerginApi::getLocalProjectFiles( const QString &projectPath ) { QList merginFiles; @@ -1178,13 +1210,14 @@ QList MerginApi::getLocalProjectFiles( const QString &projectPath ) return merginFiles; } -void MerginApi::listProjectsReplyFinished() +void MerginApi::listProjectsReplyFinished( QString requestId ) { QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); int projectCount = -1; int requestedPage = 1; + MerginProjectsList projectList; if ( r->error() == QNetworkReply::NoError ) { @@ -1193,50 +1226,63 @@ 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 ); - } - else - { - mRemoteProjects.clear(); - } - // for any local projects we can update the latest server version - for ( MerginProjectListEntry project : mRemoteProjects ) + if ( doc.isObject() ) { - QString fullProjectName = getFullProjectName( project.projectNamespace, project.projectName ); - LocalProjectInfo localProject = mLocalProjects.projectFromMerginName( fullProjectName ); - if ( localProject.isValid() ) - { - mLocalProjects.updateMerginServerVersion( localProject.projectDir, project.version ); - } + projectCount = doc.object().value( "count" ).toInt(); + projectList = parseProjectsFromJson( doc ); } - InputUtils::log( "list projects", QStringLiteral( "Success - got %1 projects" ).arg( mRemoteProjects.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 ) ); - mRemoteProjects.clear(); + CoreUtils::log( "list projects", QStringLiteral( "FAILED - %1" ).arg( message ) ); + + emit listProjectsFailed(); + } + + r->deleteLater(); + + emit listProjectsFinished( projectList, mTransactionalStatus, projectCount, requestedPage, requestId ); +} + +void MerginApi::listProjectsByNameReplyFinished( QString requestId ) +{ + QNetworkReply *r = qobject_cast( sender() ); + Q_ASSERT( r ); + + MerginProjectsList projectList; + + if ( r->error() == QNetworkReply::NoError ) + { + QByteArray data = r->readAll(); + QJsonDocument json = QJsonDocument::fromJson( data ); + projectList = parseProjectsFromJson( json ); + 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" ) ); + CoreUtils::log( "list projects by name", QStringLiteral( "FAILED - %1" ).arg( message ) ); emit listProjectsFailed(); } r->deleteLater(); - emit listProjectsFinished( mRemoteProjects, mTransactionalStatus, projectCount, requestedPage ); + + emit listProjectsByNameFinished( projectList, mTransactionalStatus, 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 ); @@ -1244,7 +1290,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; } @@ -1254,7 +1300,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() ); @@ -1270,11 +1316,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 ); } } } @@ -1282,13 +1328,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; @@ -1309,21 +1355,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 ); } // @@ -1337,23 +1383,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 - LocalProjectInfo info = mLocalProjects.projectFromMerginName( projectFullName ); - QString newDest = InputUtils::findUniquePath( generateConflictFileName( dest, info.localVersion ), false ); + LocalProject info = mLocalProjects.projectFromMerginName( projectFullName ); + 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 ); } } @@ -1363,13 +1409,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 } @@ -1383,7 +1429,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 ) { @@ -1399,15 +1445,15 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) { // move local file to conflict file QString origPath = projectDir + "/" + finalizationItem.filePath; - LocalProjectInfo info = mLocalProjects.projectFromMerginName( projectFullName ); - QString newPath = InputUtils::findUniquePath( generateConflictFileName( origPath, info.localVersion ), false ); + LocalProject info = mLocalProjects.projectFromMerginName( projectFullName ); + 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; @@ -1421,7 +1467,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; @@ -1432,7 +1478,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 ); } } @@ -1440,7 +1486,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(); @@ -1452,8 +1498,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 ); } @@ -1492,7 +1538,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 ); @@ -1503,7 +1549,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; @@ -1519,7 +1565,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; @@ -1547,7 +1593,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; @@ -1579,7 +1625,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(); @@ -1603,7 +1649,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; @@ -1613,7 +1659,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; @@ -1627,7 +1673,7 @@ void MerginApi::startProjectUpdate( const QString &projectFullName, const QByteA Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); TransactionStatus &transaction = mTransactionalStatus[projectFullName]; - LocalProjectInfo projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); if ( projectInfo.isValid() ) { transaction.projectDir = projectInfo.projectDir; @@ -1642,16 +1688,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 @@ -1660,13 +1706,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 ) { @@ -1737,10 +1783,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 ); @@ -1798,26 +1844,25 @@ 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(); transaction.replyUploadProjectInfo = nullptr; - LocalProjectInfo projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject 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; + 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 - 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 ) ); + 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; @@ -1826,10 +1871,8 @@ 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() ); + CoreUtils::log( "push " + projectFullName, transaction.diff.dump() ); // TODO: make sure there are no remote files to add/update/remove nor conflicts @@ -1851,8 +1894,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 ); @@ -1861,7 +1907,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 ); @@ -1869,12 +1915,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 ) ); } } @@ -1920,8 +1966,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; @@ -1938,7 +1984,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; @@ -1962,7 +2008,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; @@ -1981,13 +2027,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; @@ -1996,16 +2043,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 ); @@ -2014,7 +2061,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; @@ -2032,13 +2079,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 ); @@ -2053,7 +2100,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() ) { @@ -2065,7 +2112,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" ) ); } @@ -2225,55 +2272,78 @@ ProjectDiff MerginApi::compareProjectFiles( const QList &oldServerFi return diff; } - -MerginProjectList MerginApi::parseProjectJsonArray( const QJsonArray &vArray ) +MerginProject MerginApi::parseProjectMetadata( const QJsonObject &proj ) { + MerginProject project; - MerginProjectList result; - for ( auto it = vArray.constBegin(); it != vArray.constEnd(); ++it ) + if ( proj.isEmpty() ) { - QJsonObject projectMap = it->toObject(); - MerginProjectListEntry project; + return project; + } - project.projectName = projectMap.value( QStringLiteral( "name" ) ).toString(); - project.projectNamespace = projectMap.value( QStringLiteral( "namespace" ) ).toString(); + if ( proj.contains( QStringLiteral( "error" ) ) ) + { + // 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; + } - 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.serverVersion = 0; + } + else if ( versionStr.startsWith( "v" ) ) // cut off 'v' part from v123 + { + versionStr = versionStr.mid( 1 ); + project.serverVersion = 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 ) + +MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc ) { - MerginProjectList result; + if ( !doc.isObject() ) + return MerginProjectsList(); - QJsonDocument doc = QJsonDocument::fromJson( data ); - if ( doc.isArray() ) + QJsonObject object = doc.object(); + MerginProjectsList result; + + 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 ) + { + MerginProject 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; } @@ -2292,7 +2362,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; @@ -2350,24 +2420,24 @@ 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.updateMerginServerVersion( transaction.projectDir, transaction.version ); + 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; 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 ) { - 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 ); @@ -2375,7 +2445,7 @@ void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSucc } else { - emit syncProjectFinished( projectDir, projectFullName, syncSuccessful ); + emit syncProjectFinished( projectDir, projectFullName, syncSuccessful, newVersion ); if ( syncSuccessful ) { @@ -2389,7 +2459,6 @@ void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSucc } } } - } bool MerginApi::writeData( const QByteArray &data, const QString &path ) @@ -2419,7 +2488,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 ) ); } } } @@ -2466,5 +2535,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 92% rename from app/merginapi.h rename to core/merginapi.h index f86fad215..5c7ba6105 100644 --- a/app/merginapi.h +++ b/core/merginapi.h @@ -27,6 +27,7 @@ #include "merginsubscriptionstatus.h" #include "merginprojectmetadata.h" #include "localprojectsmanager.h" +#include "project.h" class MerginUserAuth; class MerginUserInfo; @@ -165,20 +166,6 @@ struct TransactionStatus ProjectDiff diff; }; - -struct MerginProjectListEntry -{ - 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 - -}; - -typedef QList MerginProjectList; - typedef QHash Transactions; Q_DECLARE_METATYPE( Transactions ); @@ -218,14 +205,24 @@ 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 QString listProjects( const QString &searchExpression = QStringLiteral(), + 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 QStringList of project full names (namespace/name) + * \returns unique id of a sent request */ - Q_INVOKABLE void listProjects( const QString &searchExpression = QStringLiteral(), - const QString &flag = QStringLiteral(), const QString &filterTag = QStringLiteral(), const int page = 1 ); + 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, * 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. @@ -237,7 +234,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. @@ -309,9 +306,6 @@ class MerginApi: public QObject */ Q_INVOKABLE void detachProjectFromMergin( const QString &projectNamespace, const QString &projectName ); - - LocalProjectInfo getLocalProject( const QString &projectFullName ); - static const int MERGIN_API_VERSION_MAJOR = 2020; static const int MERGIN_API_VERSION_MINOR = 4; static const QString sMetadataFile; @@ -348,6 +342,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) /** @@ -363,21 +359,12 @@ 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; void setApiRoot( const QString &apiRoot ); - QString merginUserName() const; - - //! Disk usage of current logged in user in Mergin instance in Bytes - int diskUsage() const; - - //! Total storage limit of current logged in user in Mergin instance in Bytes - int storageLimit() const; + QString merginUserName() const; // TODO: replace (can be replaced with userInfo->username) MerginApiStatus::VersionStatus apiVersionStatus() const; void setApiVersionStatus( const MerginApiStatus::VersionStatus &apiVersionStatus ); @@ -389,12 +376,22 @@ 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(); - void listProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page ); + void listProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ); void listProjectsFailed(); - void syncProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); + void listProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ); + 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. @@ -421,10 +418,12 @@ 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 ); + void projectAttachedToMergin( const QString &projectFullName ); private slots: - void listProjectsReplyFinished(); + void listProjectsReplyFinished( QString requestId ); + void listProjectsByNameReplyFinished( QString requestId ); // Pull slots void updateInfoReplyFinished(); @@ -446,8 +445,8 @@ class MerginApi: public QObject void pingMerginReplyFinished(); private: - MerginProjectList parseListProjectsMetadata( const QByteArray &data ); - MerginProjectList parseProjectJsonArray( const QJsonArray &vArray ); + MerginProject 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 ); @@ -488,14 +487,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. @@ -544,7 +535,6 @@ class MerginApi: public QObject QNetworkAccessManager mManager; QString mApiRoot; LocalProjectsManager &mLocalProjects; - MerginProjectList mRemoteProjects; QString mDataDir; // dir with all projects MerginUserInfo *mUserInfo; //owned by this (qml grouped-properties) 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 94% rename from app/merginprojectstatusmodel.cpp rename to core/merginprojectstatusmodel.cpp index 3c1d81742..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; @@ -125,7 +125,7 @@ void MerginProjectStatusModel::infoProjectUpdated( const ProjectDiff &projectDif bool MerginProjectStatusModel::loadProjectInfo( const QString &projectFullName ) { - LocalProjectInfo projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); if ( !projectInfo.projectDir.isEmpty() ) { ProjectDiff diff = MerginApi::localProjectChanges( projectInfo.projectDir ); 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/core/project.cpp b/core/project.cpp new file mode 100644 index 000000000..5dac5c524 --- /dev/null +++ b/core/project.cpp @@ -0,0 +1,87 @@ +/*************************************************************************** + * * + * 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.h" +#include "merginapi.h" +#include "coreutils.h" + +QString LocalProject::id() const +{ + if ( !projectName.isEmpty() && !projectNamespace.isEmpty() ) + return MerginApi::getFullProjectName( projectNamespace, projectName ); + + QDir dir( projectDir ); + 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 + 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 = CoreUtils::getLastModifiedFileDateTime( project->local->projectDir ); + QDateTime lastSync = QFileInfo( metadataFilePath ).lastModified(); + MerginProjectMetadata meta = MerginProjectMetadata::fromCachedJson( metadataFilePath ); + int filesCount = CoreUtils::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/core/project.h b/core/project.h new file mode 100644 index 000000000..ddaf13ea6 --- /dev/null +++ b/core/project.h @@ -0,0 +1,183 @@ +/*************************************************************************** + * * + * 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_H +#define PROJECT_H + +#include +#include +#include +#include + +struct Project; + +namespace ProjectStatus +{ + Q_NAMESPACE + enum Status + { + 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) + // Maybe orphaned state in future + }; + 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() {}; + ~LocalProject() {}; + + QString projectName; + QString projectNamespace; + + QString id() const; //! projectFullName for time being + + QString projectDir; + QString projectError; // Error that leads to project not being able to open in app + + QString qgisProjectFilePath; + + int localVersion = -1; + + bool isValid() { return !projectDir.isEmpty(); } + + LocalProject *clone() const; + + bool operator ==( const LocalProject &other ) + { + return ( this->id() == other.id() ); + } + + bool operator !=( const LocalProject &other ) + { + return !( *this == other ); + } +}; + +/** + * \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() {}; + ~MerginProject() {}; + + QString projectName; + QString projectNamespace; + + QString id() const; //! projectFullName for time being + + QDateTime serverUpdated; // available latest version of project files on server + int serverVersion; + + ProjectStatus::Status status = ProjectStatus::NoVersion; + bool pending = false; + qreal progress = 0; + + 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() ); + } + + bool operator !=( const MerginProject &other ) + { + return !( *this == other ); + } +}; + +/** + * \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() {}; + ~Project() {}; + + std::unique_ptr mergin; + std::unique_ptr local; + + bool isMergin() const { return mergin != nullptr; } + bool isLocal() const { return local != nullptr; } + + QString projectName() + { + if ( isMergin() ) return mergin->projectName; + else if ( isLocal() ) return local->projectName; + return QString(); + } + + QString projectNamespace() + { + if ( isMergin() ) return mergin->projectNamespace; + else if ( isLocal() ) return local->projectNamespace; + return QString(); + } + + QString projectId() + { + if ( isMergin() ) return mergin->id(); + else if ( isLocal() ) return local->id(); + return QString(); + } + + QString projectFullName() + { + return projectId(); + } + + bool operator ==( const Project &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 &other ) + { + return !( *this == other ); + } +}; + +typedef QList MerginProjectsList; +typedef QList LocalProjectsList; +Q_DECLARE_METATYPE( MerginProjectsList ) + +#endif // PROJECT_H 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 000000000..48d9e1469 Binary files /dev/null and b/docs/developers/code/projects/media/class-structure.png differ 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 000000000..d81acd24e Binary files /dev/null and b/docs/developers/code/projects/media/more-menu.png differ diff --git a/docs/developers/code/projects/project-models.md b/docs/developers/code/projects/project-models.md new file mode 100644 index 000000000..fa8ceb02c --- /dev/null +++ b/docs/developers/code/projects/project-models.md @@ -0,0 +1,8 @@ +## Project handling in app + +![](media/class-structure.png) + + +Fields in more menu: + +![](media/more-menu.png) 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 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