diff --git a/app/inpututils.cpp b/app/inpututils.cpp index 2dc33db99..cbe00cac5 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -443,6 +443,14 @@ QString InputUtils::createUniqueProjectDirectory( const QString &baseDataDir, co return projectDirPath; } +bool InputUtils::removeDir( const QString &dir ) +{ + if ( dir.isEmpty() || dir == "/" ) + return false; + + return QDir( dir ).removeRecursively(); +} + void InputUtils::onQgsLogMessageReceived( const QString &message, const QString &tag, Qgis::MessageLevel level ) { QString levelStr; @@ -528,6 +536,39 @@ QString InputUtils::renameWithDateTime( const QString &srcPath, const QDateTime return QString(); } +QDateTime InputUtils::getLastModifiedFileDateTime( const QString &path ) +{ + QDateTime lastModified; + QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); + while ( it.hasNext() ) + { + it.next(); + if ( !MerginApi::isInIgnore( it.fileInfo() ) ) + { + if ( it.fileInfo().lastModified() > lastModified ) + { + lastModified = it.fileInfo().lastModified(); + } + } + } + return lastModified.toUTC(); +} + +int InputUtils::getProjectFilesCount( const QString &path ) +{ + int count = 0; + QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); + while ( it.hasNext() ) + { + it.next(); + if ( !MerginApi::isInIgnore( it.fileInfo() ) ) + { + count++; + } + } + return count; +} + QString InputUtils::downloadInProgressFilePath( const QString &projectDir ) { return projectDir + "/.mergin/.project.downloading"; diff --git a/app/inpututils.h b/app/inpututils.h index 75dac7830..cbb5ab136 100644 --- a/app/inpututils.h +++ b/app/inpututils.h @@ -152,6 +152,12 @@ class InputUtils: public QObject //! 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 removeDir( const QString &projectDir ); + + static QDateTime getLastModifiedFileDateTime( const QString &path ); + + static int getProjectFilesCount( const QString &path ); + signals: Q_INVOKABLE void showNotificationRequested( const QString &message ); diff --git a/app/localprojectsmanager.cpp b/app/localprojectsmanager.cpp index 2a70fa1a9..258ada02e 100644 --- a/app/localprojectsmanager.cpp +++ b/app/localprojectsmanager.cpp @@ -19,18 +19,18 @@ LocalProjectsManager::LocalProjectsManager( const QString &dataDir ) : mDataDir( dataDir ) { - reloadProjectDir(); + reloadDataDir(); } -void LocalProjectsManager::reloadProjectDir() // TODO: maybe add function to reload one specific project +void LocalProjectsManager::reloadDataDir() { mProjects.clear(); QStringList entryList = QDir( mDataDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); - for ( QString folderName : entryList ) + for ( const QString &folderName : entryList ) { - LocalProjectInfo info; + LocalProject info; info.projectDir = mDataDir + "/" + folderName; - info.qgisProjectFilePath = findQgisProjectFile( info.projectDir, info.qgisProjectError ); + info.qgisProjectFilePath = findQgisProjectFile( info.projectDir, info.projectError ); MerginProjectMetadata metadata = MerginProjectMetadata::fromCachedJson( info.projectDir + "/" + MerginApi::sMetadataFile ); if ( metadata.isValid() ) @@ -47,183 +47,119 @@ void LocalProjectsManager::reloadProjectDir() // TODO: maybe add function to rel mProjects << info; } - qDebug() << "LocalProjectsManager: found" << mProjects.size() << "projects"; + qDebug() << "Found " << mProjects.size() << " local projects in " << mDataDir; + emit dataDirReloaded(); } -LocalProjectInfo LocalProjectsManager::projectFromDirectory( const QString &projectDir ) const +LocalProject LocalProjectsManager::projectFromDirectory( const QString &projectDir ) const { - for ( const LocalProjectInfo &info : mProjects ) + for ( const LocalProject &info : mProjects ) { if ( info.projectDir == projectDir ) return info; } - return LocalProjectInfo(); + return LocalProject(); } -LocalProjectInfo LocalProjectsManager::projectFromProjectFilePath( const QString &projectFilePath ) const +LocalProject LocalProjectsManager::projectFromProjectFilePath( const QString &projectFilePath ) const { - for ( const LocalProjectInfo &info : mProjects ) + for ( const LocalProject &info : mProjects ) { if ( info.qgisProjectFilePath == projectFilePath ) return info; } - return LocalProjectInfo(); + return LocalProject(); } -LocalProjectInfo LocalProjectsManager::projectFromMerginName( const QString &projectFullName ) const +LocalProject LocalProjectsManager::projectFromMerginName( const QString &projectFullName ) const { - for ( const LocalProjectInfo &info : mProjects ) + for ( const LocalProject &info : mProjects ) { - if ( MerginApi::getFullProjectName( info.projectNamespace, info.projectName ) == projectFullName ) + if ( info.id() == projectFullName ) return info; } - return LocalProjectInfo(); + return LocalProject(); } -LocalProjectInfo LocalProjectsManager::projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const +LocalProject LocalProjectsManager::projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const { return projectFromMerginName( MerginApi::getFullProjectName( projectNamespace, projectName ) ); } -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 ) +void LocalProjectsManager::addLocalProject( const QString &projectDir, const QString &projectName ) { - LocalProjectInfo project; - project.projectDir = projectDir; - project.qgisProjectFilePath = findQgisProjectFile( projectDir, project.qgisProjectError ); - project.projectNamespace = projectNamespace; - project.projectName = projectName; - - mProjects << project; + addProject( projectDir, QString(), projectName ); } void LocalProjectsManager::addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ) { addProject( projectDir, projectNamespace, projectName ); - // version info and status should be updated afterwards - emit localMerginProjectAdded( projectDir ); } -void LocalProjectsManager::addLocalProject( const QString &projectDir, const QString &projectName ) -{ - addProject( projectDir, QString(), projectName ); - emit localProjectAdded( projectDir ); -} - -void LocalProjectsManager::removeProject( const QString &projectDir ) +void LocalProjectsManager::removeLocalProject( const QString &projectId ) { for ( int i = 0; i < mProjects.count(); ++i ) { - if ( mProjects[i].projectDir == projectDir ) + if ( mProjects[i].id() == projectId ) { + emit aboutToRemoveLocalProject( mProjects[i] ); + + InputUtils::removeDir( mProjects[i].projectDir ); mProjects.removeAt( i ); - emit localProjectRemoved( projectDir ); + return; } } } -void LocalProjectsManager::resetMerginInfo( const QString &projectNamespace, const QString &projectName ) +bool LocalProjectsManager::projectIsValid( const QString &path ) const { for ( int i = 0; i < mProjects.count(); ++i ) { - if ( mProjects[i].projectNamespace == projectNamespace && mProjects[i].projectName == projectName ) + if ( mProjects[i].qgisProjectFilePath == path ) { - mProjects[i].localVersion = -1; - mProjects[i].serverVersion = -1; - mProjects[i].projectNamespace.clear(); - updateProjectStatus( mProjects[i] ); - emit projectMetadataChanged( mProjects[i].projectDir ); - return; + return mProjects[i].projectError.isEmpty(); } } + return false; } -void LocalProjectsManager::deleteProjectDirectory( const QString &projectDir ) +QString LocalProjectsManager::projectId( const QString &path ) const { for ( int i = 0; i < mProjects.count(); ++i ) { - if ( mProjects[i].projectDir == projectDir ) + if ( mProjects[i].qgisProjectFilePath == path ) { - Q_ASSERT( !projectDir.isEmpty() && projectDir != "/" ); - QDir( projectDir ).removeRecursively(); - mProjects.removeAt( i ); - return; + return mProjects[i].id(); } } + return QString(); } -void LocalProjectsManager::updateMerginLocalVersion( const QString &projectDir, int version ) +void LocalProjectsManager::updateLocalVersion( const QString &projectDir, int version ) { for ( int i = 0; i < mProjects.count(); ++i ) { if ( mProjects[i].projectDir == projectDir ) { mProjects[i].localVersion = version; - updateProjectStatus( mProjects[i] ); - return; - } - } - Q_ASSERT( false ); // should not happen -} -void LocalProjectsManager::updateMerginServerVersion( const QString &projectDir, int version ) -{ - for ( int i = 0; i < mProjects.count(); ++i ) - { - if ( mProjects[i].projectDir == projectDir ) - { - mProjects[i].serverVersion = version; - updateProjectStatus( mProjects[i] ); + emit localProjectDataChanged( mProjects[i] ); 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 ) +void LocalProjectsManager::updateNamespace( const QString &projectDir, const QString &projectNamespace ) { for ( int i = 0; i < mProjects.count(); ++i ) { if ( mProjects[i].projectDir == projectDir ) { - // Effects only local project list, no need to send projectMetadataChanged mProjects[i].projectNamespace = projectNamespace; + + emit localProjectDataChanged( mProjects[i] ); return; } } @@ -267,78 +203,14 @@ QString LocalProjectsManager::findQgisProjectFile( const QString &projectDir, QS 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 ) +void LocalProjectsManager::addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ) { - // 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; -} + LocalProject project; + project.projectDir = projectDir; + project.qgisProjectFilePath = findQgisProjectFile( projectDir, project.projectError ); + project.projectName = projectName; + project.projectNamespace = projectNamespace; -void LocalProjectsManager::updateProjectStatus( LocalProjectInfo &project ) -{ - ProjectStatus newStatus = currentProjectStatus( project ); - if ( newStatus != project.status ) - { - project.status = newStatus; - emit projectMetadataChanged( project.projectDir ); - } + mProjects << project; + emit localProjectAdded( project ); } diff --git a/app/localprojectsmanager.h b/app/localprojectsmanager.h index dfb84a674..9ff18bd7a 100644 --- a/app/localprojectsmanager.h +++ b/app/localprojectsmanager.h @@ -11,53 +11,7 @@ #define LOCALPROJECTSMANAGER_H #include -#include - -enum ProjectStatus -{ - NoVersion, //!< the project is not available locally - UpToDate, //!< both server and local copy are in sync with no extra modifications - OutOfDate, //!< server has newer version than what is available locally (but the project is not modified locally) - Modified, //!< there are some local modifications in the project that need to be pushed (note: also server may have newer version) - NonProjectItem //!< only for mock projects, acts like a hook to enable extra functionality for models working with projects . -}; -Q_ENUMS( ProjectStatus ) - - -//! Summary information about a local project -struct LocalProjectInfo -{ - bool isValid() const { return !projectDir.isEmpty(); } - - bool isShowable() const { return qgisProjectError.isEmpty(); } - - QString projectDir; //!< full path to the project directory - - QString qgisProjectFilePath; //!< path to the .qgs/.qgz file (or empty if not have exactly one such file) - - QString qgisProjectError; //!< If project is invalid, projectError carry more information why - // TODO: reset when project is synchronized - - // - // mergin-specific project info (may be empty) - // - - QString projectName; - QString projectNamespace; - - int localVersion = -1; //!< the project version that is currently available locally - int serverVersion = -1; //!< the project version most recently seen on server (may be -1 if no info from server is available) - - ProjectStatus status = NoVersion; - - bool operator ==( const LocalProjectInfo &other ) { return ( projectName == other.projectName && projectNamespace == other.projectNamespace );} - bool operator !=( const LocalProjectInfo &other ) { return !( *this == other ); } - - // Sync status (e.g. progress) is not kept here because if a project does not exist locally yet - // and it is only being downloaded for the first time, it's not in the list of local projects either - // and we would need to do some workarounds for that. -}; - +#include class LocalProjectsManager : public QObject { @@ -65,77 +19,54 @@ class LocalProjectsManager : public QObject public: explicit LocalProjectsManager( const QString &dataDir ); - QString dataDir() const { return mDataDir; } - - QList projects() const { return mProjects; } + //! Loads all projects from mDataDir, removes all old projects + void reloadDataDir(); - void reloadProjectDir(); + QString dataDir() const { return mDataDir; } - LocalProjectInfo projectFromDirectory( const QString &projectDir ) const; - LocalProjectInfo projectFromProjectFilePath( const QString &projectDir ) const; + LocalProjectsList projects() const { return mProjects; } - LocalProjectInfo projectFromMerginName( const QString &projectFullName ) const; - LocalProjectInfo projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const; + LocalProject projectFromDirectory( const QString &projectDir ) const; + LocalProject projectFromProjectFilePath( const QString &projectFilePath ) const; - bool hasMerginProject( const QString &projectFullName ) const; - bool hasMerginProject( const QString &projectNamespace, const QString &projectName ) const; + LocalProject projectFromMerginName( const QString &projectFullName ) const; + LocalProject projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const; - void updateProjectStatus( const QString &projectDir ); + //! Adds entry about newly created project + void addLocalProject( const QString &projectDir, const QString &projectName ); - //! Should add an entry about newly created Mergin project + //! Adds entry for downloaded project void addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); - //! Should add an entry about newly created local project - void addLocalProject( const QString &projectDir, const QString &projectName ); - //! Should forget about that project (it has been removed already) - void removeProject( const QString &projectDir ); + Q_INVOKABLE void removeLocalProject( const QString &projectId ); - //! Resets mergin related info for given project. - void resetMerginInfo( const QString &projectNamespace, const QString &projectName ); + Q_INVOKABLE bool projectIsValid( const QString &path ) const; - //! Recursively removes project's directory (only when it exists in the list) - void deleteProjectDirectory( const QString &projectDir ); - - // - // updates of mergin info - // + Q_INVOKABLE QString projectId( const QString &path ) const; //! after successful update/upload - both server and local version are the same - void 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 ); + void updateLocalVersion( const QString &projectDir, int version ); //! Updates proejct's namespace - void updateMerginNamespace( const QString &projectDir, const QString &projectNamespace ); + void updateNamespace( const QString &projectDir, const QString &projectNamespace ); //! Finds all QGIS project files and set the err variable if any occured. QString findQgisProjectFile( const QString &projectDir, QString &err ); - - static ProjectStatus currentProjectStatus( const LocalProjectInfo &project ); // TODO: local project manager should not have status - signals: void projectMetadataChanged( const QString &projectDir ); void localMerginProjectAdded( const QString &projectDir ); - void localProjectAdded( const QString &projectDir ); - void localProjectRemoved( const QString &projectDir ); + void localProjectAdded( const LocalProject &project ); + void aboutToRemoveLocalProject( const LocalProject project ); + void localProjectDataChanged( const LocalProject &project ); + void dataDirReloaded(); private: - void updateProjectStatus( LocalProjectInfo &project ); // TODO: local project manager should not have status - - //! Should add an entry about newly created project. Emits no signals void addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); - private: QString mDataDir; //!< directory with all local projects - QList mProjects; - - QList mLocalProjects_future; + LocalProjectsList mProjects; }; diff --git a/app/main.cpp b/app/main.cpp index ad203e0c0..056d5e3c6 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -32,14 +32,12 @@ #include "ios/iosutils.h" #include "inpututils.h" #include "positiondirection.h" -#include "projectsmodel.h" #include "mapthemesmodel.h" #include "digitizingcontroller.h" #include "merginapi.h" #include "merginapistatus.h" #include "merginsubscriptionstatus.h" #include "merginsubscriptiontype.h" -#include "merginprojectmodel.h" #include "merginprojectstatusmodel.h" #include "layersproxymodel.h" #include "layersmodel.h" @@ -54,6 +52,10 @@ #include "projectwizard.h" #include "codefilter.h" +#include "projectsmodel.h" +#include "projectsproxymodel.h" +#include "project.h" + #ifdef INPUT_TEST #include "test/testmerginapi.h" #include "test/testlinks.h" @@ -215,7 +217,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", "" ); @@ -230,6 +231,9 @@ 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 @@ -357,13 +361,11 @@ 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 ); @@ -380,11 +382,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 ); @@ -472,18 +470,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 ); diff --git a/app/merginapi.cpp b/app/merginapi.cpp index 265ffb020..1ed478e48 100644 --- a/app/merginapi.cpp +++ b/app/merginapi.cpp @@ -30,8 +30,7 @@ #include const QString MerginApi::sMetadataFile = QStringLiteral( "/.mergin/mergin.json" ); -//const QString MerginApi::sDefaultApiRoot = QStringLiteral( "https://public.cloudmergin.com/" ); -const QString MerginApi::sDefaultApiRoot = QStringLiteral( "https://dev.dev.cloudmergin.com/" ); +const QString MerginApi::sDefaultApiRoot = QStringLiteral( "https://public.cloudmergin.com/" ); const QSet MerginApi::sIgnoreExtensions = QSet() << "gpkg-shm" << "gpkg-wal" << "qgs~" << "qgz~" << "pyc" << "swap"; const QSet MerginApi::sIgnoreFiles = QSet() << "mergin.json" << ".DS_Store"; const int MerginApi::UPLOAD_CHUNK_SIZE = 10 * 1024 * 1024; // Should be the same as on Mergin server @@ -107,13 +106,11 @@ QString MerginApi::listProjects( const QString &searchExpression, const QString InputUtils::log( "list projects", QStringLiteral( "Requesting: " ) + url.toString() ); connect( reply, &QNetworkReply::finished, this, [this, requestId]() {this->listProjectsReplyFinished( requestId );} ); - qDebug() << "MerginAPI: ListProjects, returning requestId: " << requestId; return requestId; } QString MerginApi::listProjectsByName( const QStringList &projectNames ) { - setApiRoot( defaultApiRoot() ); // construct JSON body QJsonDocument body; QJsonObject projects; @@ -759,11 +756,12 @@ void MerginApi::createProjectFinished() extractProjectName( projectFullName, projectNamespace, projectName ); // Upload data if createProject has been called for a local project with empty namespace (case of migrating a project) - for ( const LocalProjectInfo &info : mLocalProjects.projects() ) + for ( const LocalProject &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() ) @@ -938,7 +936,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 @@ -1063,7 +1061,7 @@ QString MerginApi::extractServerErrorMsg( const QByteArray &data ) } -LocalProjectInfo MerginApi::getLocalProject( const QString &projectFullName ) +LocalProject MerginApi::getLocalProject( const QString &projectFullName ) { return mLocalProjects.projectFromMerginName( projectFullName ); } @@ -1135,20 +1133,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(); + InputUtils::removeDir( projectInfo.projectDir + "/.mergin" ); } - // Update localProjects (updating mMerginProjects can be omitted since it is updated on listing projects) - mLocalProjects.resetMerginInfo( projectNamespace, projectName ); - mLocalProjects.reloadProjectDir(); + + // Update localProject + mLocalProjects.updateNamespace( projectInfo.projectDir, "" ); + mLocalProjects.updateLocalVersion( projectInfo.projectDir, -1 ); emit notify( tr( "Project detached from Mergin" ) ); - emit projectDetached(); + emit projectDetached( projectFullName ); } QString MerginApi::apiRoot() const @@ -1185,11 +1184,6 @@ QString MerginApi::merginUserName() const return userAuth()->username(); } -MerginProjectList MerginApi::projects() -{ - return mRemoteProjects; -} - QList MerginApi::getLocalProjectFiles( const QString &projectPath ) { QList merginFiles; @@ -1217,6 +1211,7 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) int projectCount = -1; int requestedPage = 1; + MerginProjectsList projectList; if ( r->error() == QNetworkReply::NoError ) { @@ -1229,25 +1224,10 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) if ( doc.isObject() ) { projectCount = doc.object().value( "count" ).toInt(); - mRemoteProjects = parseProjectsFromJson( doc ); - } - else - { - mRemoteProjects.clear(); - } - - // for any local projects we can update the latest server version - for ( MerginProjectListEntry project : mRemoteProjects ) - { - QString fullProjectName = getFullProjectName( project.projectNamespace, project.projectName ); - LocalProjectInfo localProject = mLocalProjects.projectFromMerginName( fullProjectName ); - if ( localProject.isValid() ) - { - mLocalProjects.updateMerginServerVersion( localProject.projectDir, project.version ); - } + projectList = parseProjectsFromJson( doc ); } - InputUtils::log( "list projects", QStringLiteral( "Success - got %1 projects" ).arg( mRemoteProjects.count() ) ); + InputUtils::log( "list projects", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) ); } else { @@ -1255,14 +1235,13 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjects" ), r->errorString(), serverMsg ); emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjects" ) ); InputUtils::log( "list projects", QStringLiteral( "FAILED - %1" ).arg( message ) ); - mRemoteProjects.clear(); emit listProjectsFailed(); } r->deleteLater(); - emit listProjectsFinished( mRemoteProjects, mTransactionalStatus, projectCount, requestedPage, requestId ); + emit listProjectsFinished( projectList, mTransactionalStatus, projectCount, requestedPage, requestId ); } void MerginApi::listProjectsByNameReplyFinished( QString requestId ) @@ -1271,20 +1250,13 @@ void MerginApi::listProjectsByNameReplyFinished( QString requestId ) Q_ASSERT( r ); /* TODO: Detect orphaned project? Project that was considered Mergin but did not get info back */ - MerginProjectList projectList; + MerginProjectsList projectList; if ( r->error() == QNetworkReply::NoError ) { QByteArray data = r->readAll(); QJsonDocument json = QJsonDocument::fromJson( data ); - projectList = parseProjectsFromJson( json ); - - for ( MerginProjectListEntry project : qAsConst( projectList ) ) - { - qDebug() << "Project: " << project.projectName; - } - InputUtils::log( "list projects by name", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) ); } else @@ -1412,7 +1384,7 @@ void MerginApi::finalizeProjectUpdateApplyDiff( const QString &projectFullName, // not good... something went wrong in rebase - we need to save the local changes // let's put them into a conflict file and use the server version - LocalProjectInfo info = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject info = mLocalProjects.projectFromMerginName( projectFullName ); QString newDest = InputUtils::findUniquePath( generateConflictFileName( dest, info.localVersion ), false ); if ( !QFile::rename( dest, newDest ) ) { @@ -1466,7 +1438,7 @@ void MerginApi::finalizeProjectUpdate( const QString &projectFullName ) { // move local file to conflict file QString origPath = projectDir + "/" + finalizationItem.filePath; - LocalProjectInfo info = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject info = mLocalProjects.projectFromMerginName( projectFullName ); QString newPath = InputUtils::findUniquePath( generateConflictFileName( origPath, info.localVersion ), false ); if ( !QFile::rename( origPath, newPath ) ) { @@ -1694,7 +1666,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; @@ -1871,20 +1843,19 @@ void MerginApi::uploadInfoReplyFinished() 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 ) ); + .arg( projectInfo.localVersion ).arg( serverProject.version ) ); transaction.updateBeforeUpload = true; startProjectUpdate( projectFullName, data ); return; @@ -1893,8 +1864,6 @@ void MerginApi::uploadInfoReplyFinished() QList localFiles = getLocalProjectFiles( transaction.projectDir + "/" ); MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile ); - mLocalProjects.updateMerginServerVersion( transaction.projectDir, serverProject.version ); - transaction.diff = compareProjectFiles( oldServerProject.files, serverProject.files, localFiles, transaction.projectDir ); InputUtils::log( "push " + projectFullName, transaction.diff.dump() ); @@ -2292,19 +2261,19 @@ ProjectDiff MerginApi::compareProjectFiles( const QList &oldServerFi return diff; } -MerginProjectListEntry MerginApi::parseProjectMetadata( const QJsonObject &proj ) +MerginProject MerginApi::parseProjectMetadata( const QJsonObject &proj ) { - MerginProjectListEntry project; + MerginProject project; if ( proj.isEmpty() ) { return project; } + if ( proj.contains( QStringLiteral( "error" ) ) ) { - // TODO: handle project error (might be orphaned project) - - proj.value( QStringLiteral( "error" ) ).toInt( 0 ); // error code + // handle project error (user might be logged out / do not have write rights / project is on different server / project is orphaned) + project.remoteError = proj.value( QStringLiteral( "error" ) ).toInt( 0 ); // error code return project; } @@ -2314,12 +2283,12 @@ MerginProjectListEntry MerginApi::parseProjectMetadata( const QJsonObject &proj QString versionStr = proj.value( QStringLiteral( "version" ) ).toString(); if ( versionStr.isEmpty() ) { - project.version = 0; + project.serverVersion = 0; } else if ( versionStr.startsWith( "v" ) ) // cut off 'v' part from v123 { versionStr = versionStr.mid( 1 ); - project.version = versionStr.toInt(); + project.serverVersion = versionStr.toInt(); } QDateTime updated = QDateTime::fromString( proj.value( QStringLiteral( "updated" ) ).toString(), Qt::ISODateWithMs ).toUTC(); @@ -2335,13 +2304,13 @@ MerginProjectListEntry MerginApi::parseProjectMetadata( const QJsonObject &proj } -MerginProjectList MerginApi::parseProjectsFromJson( const QJsonDocument &doc ) +MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc ) { if ( !doc.isObject() ) - return MerginProjectList(); + return MerginProjectsList(); QJsonObject object = doc.object(); - MerginProjectList result; + MerginProjectsList result; if ( object.contains( "projects" ) && object.value( "projects" ).isArray() ) // listProjects API { @@ -2356,7 +2325,13 @@ MerginProjectList MerginApi::parseProjectsFromJson( const QJsonDocument &doc ) { for ( auto it = object.begin(); it != object.end(); ++it ) { - result << parseProjectMetadata( it->toObject() ); + 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; @@ -2434,8 +2409,7 @@ 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 ) ); } @@ -2473,7 +2447,6 @@ void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSucc } } } - } bool MerginApi::writeData( const QByteArray &data, const QString &path ) diff --git a/app/merginapi.h b/app/merginapi.h index 761920d86..338d22c0b 100644 --- a/app/merginapi.h +++ b/app/merginapi.h @@ -27,6 +27,7 @@ #include "merginsubscriptionstatus.h" #include "merginprojectmetadata.h" #include "localprojectsmanager.h" +#include "project.h" class MerginUserAuth; class MerginUserInfo; @@ -165,29 +166,6 @@ struct TransactionStatus ProjectDiff diff; }; - -struct MerginProjectListEntry // TODO: replace with RemoteProject from Project_future.h -{ - bool isValid() const { return !projectName.isEmpty() && !projectNamespace.isEmpty(); } - - QString projectName; - QString projectNamespace; - int version = -1; - QDateTime serverUpdated; // available latest version of project files on server - - bool operator ==( const MerginProjectListEntry &other ) - { - return ( this->projectName == other.projectName ) && ( this->projectNamespace == other.projectNamespace ); - } - - bool operator !=( const MerginProjectListEntry &other ) - { - return !( *this == other ); - } -}; - -typedef QList MerginProjectList; // TODO: replace with RemoteProject from Project_future.h - typedef QHash Transactions; Q_DECLARE_METATYPE( Transactions ); @@ -246,7 +224,7 @@ class MerginApi: public QObject /** * Sends non-blocking POST request to the server to download/update a project with a given name. On downloadProjectReplyFinished, * when a response is received, parses data-stream to files and rewrites local files with them. Extra files which don't match server - * files are removed. Eventually emits syncProjectFinished on which MerginProjectModel updates status of the project item. + * files are removed. Eventually emits syncProjectFinished on which ProjectModel updates status of the project item. * If update has been successful, updates metadata file of the project. * Emits also notify signal with a message for the GUI. * \param projectNamespace Project's namespace used in request. @@ -258,7 +236,7 @@ class MerginApi: public QObject /** * Sends non-blocking POST request to the server to upload changes in a project with a given name. * Firstly updateProject is triggered to fetch new changes. If it was successful, sends update post request with list of local changes - * and modified/newly added files in JSON. Eventually emits syncProjectFinished on which MerginProjectModel updates status of the project item. + * and modified/newly added files in JSON. Eventually emits syncProjectFinished on which ProjectModel updates status of the project item. * Emits also notify signal with a message for the GUI. * \param projectNamespace Project's namespace used in request. * \param projectName Project's name used in request. @@ -330,9 +308,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; @@ -369,6 +344,8 @@ class MerginApi: public QObject */ void deleteProject( const QString &projectNamespace, const QString &projectName ); + LocalProject getLocalProject( const QString &projectFullName ); + // Production and Test functions (therefore not private) /** @@ -384,9 +361,6 @@ class MerginApi: public QObject */ static ProjectDiff compareProjectFiles( const QList &oldServerFiles, const QList &newServerFiles, const QList &localFiles, const QString &projectDir ); - //! Returns the most recent list of projects fetched from the server - MerginProjectList projects(); - static QList getLocalProjectFiles( const QString &projectPath ); QString apiRoot() const; @@ -394,12 +368,6 @@ class MerginApi: public QObject QString merginUserName() const; // TODO: remove (use can be replaced with userInfo->username) - //! Disk usage of current logged in user in Mergin instance in Bytes - int diskUsage() const; // TODO: remove (no use) - - //! Total storage limit of current logged in user in Mergin instance in Bytes - int storageLimit() const; // TODO: remove (no use) - MerginApiStatus::VersionStatus apiVersionStatus() const; void setApiVersionStatus( const MerginApiStatus::VersionStatus &apiVersionStatus ); @@ -413,9 +381,9 @@ class MerginApi: public QObject signals: void apiSupportsSubscriptionsChanged(); - void listProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ); + void listProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ); void listProjectsFailed(); - void listProjectsByNameFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, QString requestId ); + void listProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ); void syncProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); /** * Emitted when sync starts/finishes or the progress changes - useful to give a clue in the GUI about the status. @@ -443,7 +411,8 @@ 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( QString requestId ); @@ -469,8 +438,8 @@ class MerginApi: public QObject void pingMerginReplyFinished(); private: - MerginProjectListEntry parseProjectMetadata( const QJsonObject &project ); - MerginProjectList parseProjectsFromJson( const QJsonDocument &object ); + 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 ); @@ -501,7 +470,7 @@ class MerginApi: public QObject void sendUploadCancelRequest( const QString &projectFullName, const QString &transactionUUID ); bool writeData( const QByteArray &data, const QString &path ); - void createPathIfNotExists( const QString &filePath ); + void createPathIfNotExists( const QString &filePath ); // TODO: make static and move to InputUtils static QByteArray getChecksum( const QString &filePath ); static QSet listFiles( const QString &projectPath ); @@ -567,7 +536,6 @@ class MerginApi: public QObject QNetworkAccessManager mManager; QString mApiRoot; LocalProjectsManager &mLocalProjects; - MerginProjectList mRemoteProjects; // TODO: remove (no use, only in tests - TBD) QString mDataDir; // dir with all projects MerginUserInfo *mUserInfo; //owned by this (qml grouped-properties) diff --git a/app/merginprojectmodel.cpp b/app/merginprojectmodel.cpp deleted file mode 100644 index d7109cb52..000000000 --- a/app/merginprojectmodel.cpp +++ /dev/null @@ -1,254 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -#include "merginprojectmodel.h" - -#include - -MerginProjectModel::MerginProjectModel( LocalProjectsManager &localProjects, QObject *parent ) - : QAbstractListModel( parent ) - , mLocalProjects( localProjects ) -{ - QObject::connect( &mLocalProjects, &LocalProjectsManager::projectMetadataChanged, this, &MerginProjectModel::projectMetadataChanged ); - QObject::connect( &mLocalProjects, &LocalProjectsManager::localMerginProjectAdded, this, &MerginProjectModel::onLocalProjectAdded ); - QObject::connect( &mLocalProjects, &LocalProjectsManager::localProjectRemoved, this, &MerginProjectModel::onLocalProjectRemoved ); - - mAdditionalItem->status = NonProjectItem; -} - -QVariant MerginProjectModel::data( const QModelIndex &index, int role ) const -{ - int row = index.row(); - if ( row < 0 || row >= mMerginProjects.count() ) - return QVariant(); - - const MerginProject *project = mMerginProjects.at( row ).get(); - - switch ( role ) - { - case ProjectName: - return QVariant( project->projectName ); - case ProjectNamespace: return QVariant( project->projectNamespace ); - case ProjectInfo: - { - // TODO: better project info - // TODO: clientUpdated currently not being set - if ( !project->clientUpdated.isValid() ) - { - return project->serverUpdated.toLocalTime().toString(); - } - else - { - return project->clientUpdated.toLocalTime().toString(); - } - } - - case Status: - { - switch ( project->status ) - { - case ProjectStatus::OutOfDate: - return QVariant( QStringLiteral( "outOfDate" ) ); - case ProjectStatus::UpToDate: - return QVariant( QStringLiteral( "upToDate" ) ); - case ProjectStatus::NoVersion: - return QVariant( QStringLiteral( "noVersion" ) ); - case ProjectStatus::Modified: - return QVariant( QStringLiteral( "modified" ) ); - case ProjectStatus::NonProjectItem: - return QVariant( QStringLiteral( "nonProjectItem" ) ); - } - break; - } - case Pending: return QVariant( project->pending ); - case PassesFilter: return mSearchExpression.isEmpty() || project->projectName.contains( mSearchExpression, Qt::CaseInsensitive ) - || project->projectNamespace.contains( mSearchExpression, Qt::CaseInsensitive ); - case SyncProgress: return QVariant( project->progress ); - } - - return QVariant(); -} - - -QHash MerginProjectModel::roleNames() const -{ - QHash roleNames = QAbstractListModel::roleNames(); - roleNames[ProjectName] = "projectName"; - roleNames[ProjectNamespace] = "projectNamespace"; - roleNames[ProjectInfo] = "projectInfo"; - roleNames[Status] = "status"; - roleNames[Pending] = "pendingProject"; - roleNames[PassesFilter] = "passesFilter"; - roleNames[SyncProgress] = "syncProgress"; - return roleNames; -} - -ProjectList MerginProjectModel::projects() -{ - return mMerginProjects; -} - -int MerginProjectModel::rowCount( const QModelIndex &parent ) const -{ - Q_UNUSED( parent ) - return mMerginProjects.count(); -} - -void MerginProjectModel::updateModel( const MerginProjectList &merginProjects, QHash pendingProjects, int expectedProjectCount, int page ) -{ - beginResetModel(); - mMerginProjects.removeOne( mAdditionalItem ); // TODO: remove - - if ( page == 1 ) - { - mMerginProjects.clear(); - } - setLastPage( page ); - - - for ( MerginProjectListEntry entry : merginProjects ) - { - QString fullProjectName = MerginApi::getFullProjectName( entry.projectNamespace, entry.projectName ); - std::shared_ptr project = std::make_shared(); - project->projectNamespace = entry.projectNamespace; - project->projectName = entry.projectName; - project->serverUpdated = entry.serverUpdated; - - // figure out info from local projects (projectDir etc) - LocalProjectInfo localProject = mLocalProjects.projectFromMerginName( entry.projectNamespace, entry.projectName ); - if ( localProject.isValid() ) - { - project->projectDir = localProject.projectDir; - project->status = localProject.status; - // TODO: what else to copy? - } - - if ( pendingProjects.contains( fullProjectName ) ) - { - - TransactionStatus projectTransaction = pendingProjects.value( fullProjectName ); - project->progress = projectTransaction.transferedSize / projectTransaction.totalSize; - project->pending = true; - } - - mMerginProjects << project; - } - - if ( mMerginProjects.count() < expectedProjectCount ) // TODO: remove - { - mMerginProjects << mAdditionalItem; - } - - endResetModel(); -} - -int MerginProjectModel::findProjectIndex( const QString &projectFullName ) -{ - int row = 0; - for ( std::shared_ptr project : mMerginProjects ) - { - if ( MerginApi::getFullProjectName( project->projectNamespace, project->projectName ) == projectFullName ) - return row; - row++; - } - return -1; -} - -void MerginProjectModel::setLastPage( int lastPage ) -{ - mLastPage = lastPage; - emit lastPageChanged(); -} - -int MerginProjectModel::lastPage() const -{ - return mLastPage; -} - -QString MerginProjectModel::searchExpression() const -{ - return mSearchExpression; -} - -void MerginProjectModel::setSearchExpression( const QString &searchExpression ) -{ - if ( searchExpression != mSearchExpression ) - { - mSearchExpression = searchExpression; - // Hack to model changed signal - beginResetModel(); - endResetModel(); - } -} - -void MerginProjectModel::syncProjectStatusChanged( const QString &projectFullName, qreal progress ) -{ - int row = findProjectIndex( projectFullName ); - if ( row == -1 ) - return; - - std::shared_ptr project = mMerginProjects[row]; - project->pending = progress >= 0; - project->progress = progress >= 0 ? progress : 0; - - QModelIndex ix = index( row ); - emit dataChanged( ix, ix ); -} - - -void MerginProjectModel::projectMetadataChanged( const QString &projectDir ) -{ - int row = 0; - for ( std::shared_ptr project : mMerginProjects ) - { - if ( project->projectDir == projectDir ) - { - LocalProjectInfo localProject = mLocalProjects.projectFromDirectory( projectDir ); - if ( !localProject.isValid() ) - return; - - // update cached information - project->status = localProject.status; - - QModelIndex ix = index( row ); - emit dataChanged( ix, ix ); - return; - } - row++; - } -} - -void MerginProjectModel::onLocalProjectAdded( const QString &projectDir ) -{ - LocalProjectInfo localProject = mLocalProjects.projectFromDirectory( projectDir ); - if ( !localProject.isValid() ) - return; - - QString projectFullName = MerginApi::getFullProjectName( localProject.projectNamespace, localProject.projectName ); - int i = findProjectIndex( projectFullName ); - if ( i == -1 ) - return; - - std::shared_ptr project = mMerginProjects[i]; - - // store project dir - project->projectDir = localProject.projectDir; - - // update metadata and emit dataChanged() signal - projectMetadataChanged( projectDir ); -} - -void MerginProjectModel::onLocalProjectRemoved( const QString &projectDir ) -{ - // TODO: implement - // (at this point this is not needed because after removal we need to switch tab - // and that will re-fetch the list of projects) - Q_UNUSED( projectDir ); -} - diff --git a/app/merginprojectmodel.h b/app/merginprojectmodel.h deleted file mode 100644 index e45eee127..000000000 --- a/app/merginprojectmodel.h +++ /dev/null @@ -1,117 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -#ifndef MERGINPROJECTMODEL_H -#define MERGINPROJECTMODEL_H - -#include -#include -#include -#include -#include "merginapi.h" - - -/** - * Basic information about a remote mergin project from the received list of projects. - * - * We add some local information for project is also available locally: - * - general local info: project dir, downloaded version number, project status - * - sync status: whether sync is active, progress indicator - */ -struct MerginProject -{ - QString projectName; - QString projectNamespace; - QString projectDir; // full path to the project directory - QDateTime clientUpdated; // client's version of project files - QDateTime serverUpdated; // available latest version of project files on server - bool pending = false; // if there is a pending request for downlaod/update a project - ProjectStatus status = NoVersion; - qreal progress = 0; // progress in case of pending download/upload (values [0..1]) -}; - -typedef QList> ProjectList; - - -class MerginProjectModel: public QAbstractListModel -{ - Q_OBJECT - Q_PROPERTY( QString searchExpression READ searchExpression WRITE setSearchExpression ) - Q_PROPERTY( int lastPage READ lastPage NOTIFY lastPageChanged ) - - public: - enum Roles - { - ProjectName = Qt::UserRole + 1, - ProjectNamespace, - Size, - ProjectInfo, - Status, - Pending, - PassesFilter, - SyncProgress - }; - Q_ENUMS( Roles ) - - explicit MerginProjectModel( LocalProjectsManager &localProjects, QObject *parent = nullptr ); - - Q_INVOKABLE QVariant data( const QModelIndex &index, int role ) const override; - - ProjectList projects(); - - QHash roleNames() const override; - - int rowCount( const QModelIndex &parent = QModelIndex() ) const override; - - /** - * Updates list of projects with synchronization progress if a project is pending. - * \param merginProjects List of mergin projects - * \param pendingProjects Projects in pending state - * \param expectedProjectCount Total number of projects - * \param page Int representing page. - */ - void updateModel( const MerginProjectList &merginProjects, QHash pendingProjects, int expectedProjectCount, int page ); - - int filterCreator() const; // TODO: remove, no use - void setFilterCreator( int filterCreator ); - - int filterWriter() const; // TODO: remove, no use - void setFilterWriter( int filterWriter ); - - QString searchExpression() const; // TODO: remove, search will be in proxy model - void setSearchExpression( const QString &searchExpression ); - - int lastPage() const; - void setLastPage( int lastPage ); - - signals: - void lastPageChanged(); - - public slots: - void syncProjectStatusChanged( const QString &projectFullName, qreal progress ); - - private slots: - - void projectMetadataChanged( const QString &projectDir ); - void onLocalProjectAdded( const QString &projectDir ); - void onLocalProjectRemoved( const QString &projectDir ); - - private: - - int findProjectIndex( const QString &projectFullName ); - - ProjectList mMerginProjects; - LocalProjectsManager &mLocalProjects; - QString mSearchExpression; - int mLastPage; - //! Special item as a placeholder for custom component with extended funtionality - std::shared_ptr mAdditionalItem = std::make_shared(); - -}; -#endif // MERGINPROJECTMODEL_H diff --git a/app/merginprojectstatusmodel.cpp b/app/merginprojectstatusmodel.cpp index 3c1d81742..ff7097835 100644 --- a/app/merginprojectstatusmodel.cpp +++ b/app/merginprojectstatusmodel.cpp @@ -125,7 +125,7 @@ void MerginProjectStatusModel::infoProjectUpdated( const ProjectDiff &projectDif bool MerginProjectStatusModel::loadProjectInfo( const QString &projectFullName ) { - LocalProjectInfo projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); if ( !projectInfo.projectDir.isEmpty() ) { ProjectDiff diff = MerginApi::localProjectChanges( projectInfo.projectDir ); diff --git a/app/models/projectsmodel_future.cpp b/app/models/projectsmodel_future.cpp deleted file mode 100644 index 0168f391b..000000000 --- a/app/models/projectsmodel_future.cpp +++ /dev/null @@ -1,277 +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 "projectsmodel_future.h" -#include "localprojectsmanager.h" -#include "inpututils.h" - -ProjectsModel_future::ProjectsModel_future( - MerginApi *merginApi, - ProjectModelTypes modelType, - LocalProjectsManager &localProjectsManager, - QObject *parent ) : - QAbstractListModel( parent ), - mBackend( merginApi ), - mLocalProjectsManager( localProjectsManager ), - mModelType( modelType ) -{ - QObject::connect( mBackend, &MerginApi::syncProjectStatusChanged, this, &ProjectsModel_future::onProjectSyncProgressChanged ); - QObject::connect( mBackend, &MerginApi::syncProjectFinished, this, &ProjectsModel_future::onProjectSyncFinished ); - - // TODO: connect to signals from LocalProjectsManager -// QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectAdded ) - - if ( mModelType == ProjectModelTypes::LocalProjectsModel ) - { - QObject::connect( mBackend, &MerginApi::listProjectsByNameFinished, this, &ProjectsModel_future::onListProjectsByNameFinished ); - } - else if ( mModelType != ProjectModelTypes::RecentProjectsModel ) - { - QObject::connect( mBackend, &MerginApi::listProjectsFinished, this, &ProjectsModel_future::onListProjectsFinished ); - } - else - { - // Implement RecentProjectsModel type - } -} - -QVariant ProjectsModel_future::data( const QModelIndex &, int ) const -{ - return QVariant( "Test data" ); -} - -QModelIndex ProjectsModel_future::index( int, int, const QModelIndex & ) const -{ - return QModelIndex(); -} - -QHash ProjectsModel_future::roleNames() const -{ - QHash roles; - roles[Roles::Project] = QStringLiteral( "project" ).toLatin1(); - - return roles; -} - -int ProjectsModel_future::rowCount( const QModelIndex & ) const -{ - return mProjects.count(); -} - -void ProjectsModel_future::listProjects() -{ - if ( mModelType == LocalProjectsModel ) - { - InputUtils::log( "Input", "Can not call listProjects API on LocalProjectsModel" ); - return; - } - - mLastRequestId = mBackend->listProjects( "", modelTypeToFlag(), "", 1 ); //TODO: pagination -} - -void ProjectsModel_future::listProjectsByName() -{ - if ( mModelType != LocalProjectsModel ) - { - InputUtils::log( "Input", "Can not call listProjectsByName API on not LocalProjectsModel" ); - return; - } - - mLastRequestId = mBackend->listProjectsByName( projectNames() ); -} - -void ProjectsModel_future::mergeProjects( const MerginProjectList &merginProjects, Transactions pendingProjects ) -{ - QList localProjects = mLocalProjectsManager.projects(); - - qDebug() << "PMR: mergeProjects(): # of local projects = " << localProjects.size() << " # of mergin projects = " << merginProjects.size(); - mProjects.clear(); - - if ( mModelType == ProjectModelTypes::LocalProjectsModel ) - { - // Keep all local projects and ignore all not downloaded remote projects - for ( auto &localProject : localProjects ) - { - std::shared_ptr project = std::shared_ptr( new Project_future() ); - std::unique_ptr local = std::unique_ptr( new LocalProject_future() ); - - local->projectName = localProject.projectName; - local->projectNamespace = localProject.projectNamespace; - local->projectDir = localProject.projectDir; - // TODO: later copy data by copy constructor - project->local = std::move( local ); - - MerginProjectListEntry remoteEntry; - remoteEntry.projectName = project->local->projectName; - remoteEntry.projectNamespace = project->local->projectNamespace; - - if ( merginProjects.contains( remoteEntry ) ) - { - int i = merginProjects.indexOf( remoteEntry ); - std::unique_ptr mergin = std::unique_ptr( new MerginProject_future() ); - mergin->projectName = merginProjects[i].projectName; - mergin->projectNamespace = merginProjects[i].projectNamespace; - // TODO: later copy data by copy constructor - // TODO: check for project errors (from ListByName API ~> not authorized / no rights / no version) - - if ( pendingProjects.contains( mergin->projectIdentifier() ) ) - { - TransactionStatus projectTransaction = pendingProjects.value( mergin->projectIdentifier() ); - mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; - mergin->pending = true; - } - - project->mergin = std::move( mergin ); - } - - mProjects << project; - } - } - else if ( mModelType != ProjectModelTypes::RecentProjectsModel ) - { - // Keep all remote projects and ignore all non mergin projects from local projects - for ( const auto &remoteEntry : merginProjects ) - { - std::shared_ptr project = std::shared_ptr( new Project_future() ); - std::unique_ptr mergin = std::unique_ptr( new MerginProject_future() ); - - mergin->projectName = remoteEntry.projectName; - mergin->projectNamespace = remoteEntry.projectNamespace; - // TODO: later copy data by copy constructor - - if ( pendingProjects.contains( mergin->projectIdentifier() ) ) - { - TransactionStatus projectTransaction = pendingProjects.value( mergin->projectIdentifier() ); - mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; - mergin->pending = true; - } - - project->mergin = std::move( mergin ); - - // find downloaded projects - LocalProjectInfo localProject; - localProject.projectName = project->mergin->projectName; - localProject.projectNamespace = project->mergin->projectNamespace; - - if ( localProjects.contains( localProject ) ) - { - int ix = localProjects.indexOf( localProject ); - project->local = std::unique_ptr( new LocalProject_future() ); - - project->local->projectName = localProjects[ix].projectName; - project->local->projectNamespace = localProjects[ix].projectNamespace; - // TODO: later copy data by copy constructor - } - - mProjects << project; - } - } -} - -void ProjectsModel_future::onListProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ) -{ - qDebug() << "PMR: onListProjectsFinished(): received response with requestId = " << requestId; - if ( mLastRequestId != requestId ) - { - qDebug() << "PMR: onListProjectsFinished(): ignoring request with id " << requestId; - return; - } - - Q_UNUSED( projectCount ); - Q_UNUSED( page ); - - qDebug() << "PMR: onListProjectsFinished(): project count = " << projectCount << " but mergin projects emited: " << merginProjects.size(); - - beginResetModel(); - mergeProjects( merginProjects, pendingProjects ); - printProjects(); - endResetModel(); -} - -void ProjectsModel_future::onListProjectsByNameFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, QString requestId ) -{ - qDebug() << "PMR: onListProjectsByNameFinished(): received response with requestId = " << requestId; - if ( mLastRequestId != requestId ) - { - qDebug() << "PMR: onListProjectsByNameFinished(): ignoring request with id " << requestId; - return; - } - - Q_UNUSED( merginProjects ); - Q_UNUSED( pendingProjects ); - Q_UNUSED( requestId ); - - beginResetModel(); - mergeProjects( merginProjects, pendingProjects ); - printProjects(); - endResetModel(); -} - -void ProjectsModel_future::onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully ) -{ - Q_UNUSED( projectDir ) - Q_UNUSED( projectFullName ) - Q_UNUSED( successfully ) - - qDebug() << "PMR: Project " << projectFullName << " finished sync"; -} - -void ProjectsModel_future::onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ) -{ - Q_UNUSED( projectFullName ) - Q_UNUSED( progress ) - - qDebug() << "PMR: Project " << projectFullName << " changed sync progress to " << progress; -} - -QString ProjectsModel_future::modelTypeToFlag() const -{ - switch ( mModelType ) - { - case MyProjectsModel: - return QStringLiteral( "created" ); - case SharedProjectsModel: - return QStringLiteral( "shared" ); - default: - return QStringLiteral( "" ); - } -} - -void ProjectsModel_future::printProjects() const // TODO: Helper function, remove after refactoring is done -{ - qDebug() << "Model " << this << " with type " << modelTypeToFlag() << " has projects: "; - for ( const auto &proj : mProjects ) - { - QString lcl = proj->isLocal() ? "local" : ""; - QString mrgn = proj->isMergin() ? "mergin" : ""; - - if ( proj->isLocal() ) - { - qDebug() << " - " << proj->local->projectNamespace << proj->local->projectName << lcl << mrgn; - } - else if ( proj->isMergin() ) - { - qDebug() << " - " << proj->mergin->projectNamespace << proj->mergin->projectName << lcl << mrgn; - } - } -} - -QStringList ProjectsModel_future::projectNames() const // TODO: use local projects instead -{ - QStringList projectNames; - QList projects = mLocalProjectsManager.projects(); - - for ( const auto &proj : projects ) - { - if ( !proj.projectName.isEmpty() && !proj.projectNamespace.isEmpty() ) - projectNames << MerginApi::getFullProjectName( proj.projectNamespace, proj.projectName ); - } - - return projectNames; -} diff --git a/app/models/projectsmodel_future.h b/app/models/projectsmodel_future.h deleted file mode 100644 index 066b6089b..000000000 --- a/app/models/projectsmodel_future.h +++ /dev/null @@ -1,92 +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 PROJECTSMODEL_FUTURE_H -#define PROJECTSMODEL_FUTURE_H - -#include -#include - -#include "project_future.h" -#include "merginapi.h" - -class LocalProjectsManager; - -/** - * \brief The ProjectModelTypes enum - */ -enum ProjectModelTypes -{ - LocalProjectsModel = 0, - MyProjectsModel, - SharedProjectsModel, - ExploreProjectsModel, - RecentProjectsModel -}; - -/** - * \brief The ProjectsModel_future class - */ -class ProjectsModel_future : public QAbstractListModel -{ - Q_OBJECT - - public: - - enum Roles - { - // TODO: rewrite to individual roles - Project = Qt::UserRole + 1 - }; - Q_ENUMS( Roles ) - - ProjectsModel_future( MerginApi *merginApi, ProjectModelTypes modelType, LocalProjectsManager &localProjectsManager, QObject *parent = nullptr ); - ~ProjectsModel_future() override {}; - - // Needed methods from QAbstractListModel - Q_INVOKABLE QVariant data( const QModelIndex &index, int role ) const override; - Q_INVOKABLE QModelIndex index( int row, int column = 0, const QModelIndex &parent = QModelIndex() ) const override; - QHash roleNames() const override; - int rowCount( const QModelIndex &parent = QModelIndex() ) const override; - - //! Called to list projects, either fetch more or get first - Q_INVOKABLE void listProjects(); - - //! Called to list projects, either fetch more or get first - Q_INVOKABLE void listProjectsByName(); - - //! Method detecting local project for remote projects - void mergeProjects( const MerginProjectList &merginProjects, Transactions pendingProjects ); - - public slots: - void onListProjectsFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, int projectCount, int page, QString requestId ); - void onListProjectsByNameFinished( const MerginProjectList &merginProjects, Transactions pendingProjects, QString requestId ); - void onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully = true ); - void onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ); - - private: - - QString modelTypeToFlag() const; - void printProjects() const; - QStringList projectNames() const; - - MerginApi *mBackend; - LocalProjectsManager &mLocalProjectsManager; - QList> mProjects; - - ProjectModelTypes mModelType; - - //! For pagination - int mPopulatedPage = -1; - - //! For processing only my requests - QString mLastRequestId; -}; - -#endif // PROJECTSMODEL_FUTURE_H diff --git a/app/models/projectsproxymodel_future.cpp b/app/models/projectsproxymodel_future.cpp deleted file mode 100644 index fcbab74b3..000000000 --- a/app/models/projectsproxymodel_future.cpp +++ /dev/null @@ -1,23 +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 "projectsproxymodel_future.h" - -ProjectsProxyModel_future::ProjectsProxyModel_future( ProjectModelTypes modelType, QObject *parent ) : - QSortFilterProxyModel( parent ), - mModelType( modelType ) -{ - -} - -bool ProjectsProxyModel_future::filterAcceptsRow( int, const QModelIndex & ) const -{ - // return true if it passes search filter - return true; -} diff --git a/app/models/projectsproxymodel_future.h b/app/models/projectsproxymodel_future.h deleted file mode 100644 index db02b5fae..000000000 --- a/app/models/projectsproxymodel_future.h +++ /dev/null @@ -1,36 +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 PROJECTSPROXYMODEL_FUTURE_H -#define PROJECTSPROXYMODEL_FUTURE_H - -#include -#include - -#include "projectsmodel_future.h" - -/** - * @brief The ProjectsProxyModel_future class - */ -class ProjectsProxyModel_future : public QSortFilterProxyModel -{ - Q_OBJECT - public: - explicit ProjectsProxyModel_future( ProjectModelTypes modelType, QObject *parent = nullptr ); - ~ProjectsProxyModel_future() override {}; - - protected: - bool filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent ) const override; -// bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; - - private: - ProjectModelTypes mModelType; -}; - -#endif // PROJECTSPROXYMODEL_FUTURE_H diff --git a/app/project.cpp b/app/project.cpp new file mode 100644 index 000000000..f95332d22 --- /dev/null +++ b/app/project.cpp @@ -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. * + * * + ***************************************************************************/ + +#include "project.h" +#include "merginapi.h" +#include "inpututils.h" + +QString LocalProject::id() const +{ + if ( !projectName.isEmpty() && !projectNamespace.isEmpty() ) + return MerginApi::getFullProjectName( projectNamespace, projectName ); + + QDir dir( projectDir ); + return dir.dirName(); +} + +QString MerginProject::id() const +{ + return MerginApi::getFullProjectName( projectNamespace, projectName ); +} + +ProjectStatus::Status ProjectStatus::projectStatus( const std::shared_ptr project ) +{ + if ( !project || !project->isMergin() || !project->isLocal() ) // This is not a Mergin project or not downloaded project + return ProjectStatus::NoVersion; + + // There was no sync yet + if ( project->local->localVersion < 0 ) + { + return ProjectStatus::NoVersion; + } + + // + // TODO: this check for local modifications should be revisited + // + + // Something has locally changed after last sync with server + QString metadataFilePath = project->local->projectDir + "/" + MerginApi::sMetadataFile; + QDateTime lastModified = InputUtils::getLastModifiedFileDateTime( project->local->projectDir ); + QDateTime lastSync = QFileInfo( metadataFilePath ).lastModified(); + MerginProjectMetadata meta = MerginProjectMetadata::fromCachedJson( metadataFilePath ); + int filesCount = InputUtils::getProjectFilesCount( project->local->projectDir ); + if ( lastSync < lastModified || meta.files.count() != filesCount ) + { + return ProjectStatus::Modified; + } + + // Version is lower than latest one, last sync also before updated + if ( project->local->localVersion < project->mergin->serverVersion ) + { + return ProjectStatus::OutOfDate; + } + + return ProjectStatus::UpToDate; +} diff --git a/app/project.h b/app/project.h new file mode 100644 index 000000000..f23d752b0 --- /dev/null +++ b/app/project.h @@ -0,0 +1,153 @@ +/*************************************************************************** + * * + * 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 +#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 ) + + Status projectStatus( const std::shared_ptr project ); +} + +struct LocalProject +{ + LocalProject() {}; // TODO: define copy constructor + ~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(); } + + bool operator ==( const LocalProject &other ) + { + return ( this->id() == other.id() ); + } + + bool operator !=( const LocalProject &other ) + { + return !( *this == other ); + } +}; + +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; + + // Maybe better use enum or int for error code + QString remoteError; // Error leading to project not being able to sync + + bool operator ==( const MerginProject &other ) + { + return ( this->id() == other.id() ); + } + + bool operator !=( const MerginProject &other ) + { + return !( *this == other ); + } +}; + +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(); + } + + 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; + +#endif // PROJECT_H diff --git a/app/project_future.cpp b/app/project_future.cpp deleted file mode 100644 index 8b6be5f46..000000000 --- a/app/project_future.cpp +++ /dev/null @@ -1,35 +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 "project_future.h" - -#include "merginapi.h" - -void LocalProject_future::copyValues( const LocalProject_future &other ) -{ - projectName = other.projectName; - projectNamespace = other.projectNamespace; - projectDir = other.projectDir; - projectError = other.projectError; - qgisProjectFilePath = other.qgisProjectFilePath; - localVersion = other.localVersion; -} - -QString MerginProject_future::projectIdentifier() -{ - return MerginApi::getFullProjectName( projectNamespace, projectName ); -} - -QString LocalProject_future::projectIdentifier() -{ - if ( !projectName.isEmpty() && !projectNamespace.isEmpty() ) - return MerginApi::getFullProjectName( projectNamespace, projectName ); - - return projectDir; -} diff --git a/app/project_future.h b/app/project_future.h deleted file mode 100644 index 6df937e90..000000000 --- a/app/project_future.h +++ /dev/null @@ -1,83 +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 PROJECT_FUTURE_H -#define PROJECT_FUTURE_H - -#include -#include -#include -#include - -enum ProjectStatus_future -{ - _NoVersion, //!< the project is not available locally - _UpToDate, //!< both server and local copy are in sync with no extra modifications - _OutOfDate, //!< server has newer version than what is available locally (but the project is not modified locally) - _Modified, //!< there are some local modifications in the project that need to be pushed (note: also server may have newer version) - // TODO: add orphaned state -}; -Q_ENUMS( ProjectStatus_future ) - -struct LocalProject_future -{ - LocalProject_future() {}; /*{ qDebug() << "Building LocalProject_future " << this; }*/ - ~LocalProject_future() {}; /*{ qDebug() << "Removing LocalProject_future " << this; }*/ - - QString projectName; - QString projectNamespace; - - QString projectIdentifier(); - - QString projectDir; - QString projectError; // Error that leads to project not being able to open in app - - QString qgisProjectFilePath; - - int localVersion = -1; - - void copyValues( const LocalProject_future &other ); -}; - -struct MerginProject_future -{ - MerginProject_future() {}; /*{ qDebug() << "Building MerginProject_future " << this; }*/ - ~MerginProject_future() {}; /*{ qDebug() << "Removing MerginProject_future " << this; }*/ - - QString projectName; - QString projectNamespace; - - QString projectIdentifier(); - - QDateTime serverUpdated; // available latest version of project files on server // TODO: maybe we do not need this at all - only as description - int serverVersion; - - ProjectStatus_future status = ProjectStatus_future::_NoVersion; - bool pending = false; - - qreal progress = 0; - QString remoteError; // Error leading to project not being able to sync -}; - -struct Project_future -{ - Project_future() {}; /*{ qDebug() << "Building Project_future " << this; }*/ - ~Project_future() {}; /*{ qDebug() << "Removing Project_future " << this; }*/ - - std::unique_ptr mergin; - std::unique_ptr local; - - bool isMergin() { return mergin != nullptr; } - bool isLocal() { return local != nullptr; } - - //! Attributes that should be there no matter the project type - QString projectDescription; // rather on the fly -}; - -#endif // PROJECT_FUTURE_H diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index bc2a2d17c..e65661915 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,656 @@ ***************************************************************************/ #include "projectsmodel.h" - -#include -#include -#include -#include - +#include "localprojectsmanager.h" #include "inpututils.h" -#include "merginapi.h" +#include "merginuserauth.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 ); + qDebug() << "PMR: Instantiated ProjectsModel! " << this << "MerginAPI: " << mBackend << "LPM:" << mLocalProjectsManager << "Type: " << mModelType; } -ProjectModel::~ProjectModel() {} +void ProjectsModel::initializeProjectsModel() +{ + if ( !mBackend || !mLocalProjectsManager || mModelType == EmptyProjectsModel ) // Model is not set up properly yet + return; + + qDebug() << "PMR: initializing projects model " << this; + + QObject::connect( mBackend, &MerginApi::syncProjectStatusChanged, this, &ProjectsModel::onProjectSyncProgressChanged ); + QObject::connect( mBackend, &MerginApi::syncProjectFinished, this, &ProjectsModel::onProjectSyncFinished ); + QObject::connect( mBackend, &MerginApi::projectDetached, this, &ProjectsModel::onProjectDetachedFromMergin ); + QObject::connect( mBackend, &MerginApi::projectAttachedToMergin, this, &ProjectsModel::onProjectAttachedToMergin ); + + 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 + } -void ProjectModel::findProjectFiles() + 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->projectId() ); + 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 ); + } + return QVariant(); // This should not happen + } + 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; } +int ProjectsModel::rowCount( const QModelIndex & ) const +{ + return mProjects.count(); +} -QVariant ProjectModel::data( const QModelIndex &index, int role ) const +void ProjectsModel::listProjects( const QString &searchExpression, int page ) { - int row = index.row(); - if ( row < 0 || row >= mProjectFiles.count() ) - return QVariant( "" ); + if ( mModelType == LocalProjectsModel ) + { + listProjectsByName(); + return; + } - const ProjectFile &projectFile = mProjectFiles.at( row ); + mLastRequestId = mBackend->listProjects( searchExpression, modelTypeToFlag(), "", page ); +} - switch ( role ) +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() ); } -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; + if ( mProjects.size() < mServerProjectsCount ) + return true; + + return false; } -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 ); } -int ProjectModel::rowAccordingPath( QString path ) const +QVariant ProjectsModel::dataFrom( int fromRole, QVariant fromValue, int desiredRole ) const { - int i = 0; - for ( ProjectFile prj : mProjectFiles ) + switch ( fromRole ) { - if ( prj.path == path ) + case ProjectId: { - return i; + std::shared_ptr project = projectFromId( fromValue.toString() ); + if ( project ) + { + QModelIndex ix = index( mProjects.indexOf( project ) ); + return data( ix, desiredRole ); + } + return QVariant(); } - i++; + + case ProjectFilePath: + { + for ( int i = 0; i < mProjects.size(); i++ ) + { + if ( mProjects[i]->isLocal() && mProjects[i]->local->qgisProjectFilePath == fromValue.toString() ) + { + QModelIndex ix = index( i ); + return data( ix, desiredRole ); + } + } + } + default: return QVariant(); } - return -1; + + return QVariant(); } -void ProjectModel::deleteProject( int row ) +void ProjectsModel::onListProjectsFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, int projectsCount, int page, QString requestId ) { - if ( row < 0 || row >= mProjectFiles.length() ) + qDebug() << "PMR: onListProjectsFinished(): received response with requestId = " << requestId; + if ( mLastRequestId != requestId ) { - InputUtils::log( "Deleting local project error", QStringLiteral( "Unable to delete local project, index out of bounds" ) ); + qDebug() << "PMR: onListProjectsFinished(): ignoring request with id " << requestId; return; } - ProjectFile project = mProjectFiles.at( row ); + qDebug() << "PMR: onListProjectsFinished(): project count = " << projectsCount << " but mergin projects emited: " << merginProjects.size(); - mLocalProjects.deleteProjectDirectory( mLocalProjects.dataDir() + "/" + project.folderName ); + if ( page == 1 ) + { + // if we are populating first page, reset model and throw away previous projects + beginResetModel(); + mergeProjects( merginProjects, pendingProjects, false ); + printProjects(); + endResetModel(); + } + else + { + // paginating next page, keep previous projects and emit model add items + beginInsertRows( QModelIndex(), mProjects.size(), mProjects.size() + merginProjects.size() - 1 ); + mergeProjects( merginProjects, pendingProjects, true ); + printProjects(); + endInsertRows(); + } + + mServerProjectsCount = projectsCount; + mPaginatedPage = page; + emit hasMoreProjectsChanged(); +} + +void ProjectsModel::onListProjectsByNameFinished( const MerginProjectsList &merginProjects, Transactions pendingProjects, QString requestId ) +{ + qDebug() << "PMR: onListProjectsByNameFinished(): received response with requestId = " << requestId; + if ( mLastRequestId != requestId ) + { + qDebug() << "PMR: onListProjectsByNameFinished(): ignoring request with id " << requestId; + return; + } beginResetModel(); - mProjectFiles.removeAt( row ); + mergeProjects( merginProjects, pendingProjects ); + printProjects(); endResetModel(); } -int ProjectModel::rowCount( const QModelIndex &parent ) const +void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious ) { - Q_UNUSED( parent ); - return mProjectFiles.count(); + LocalProjectsList localProjects = mLocalProjectsManager->projects(); + + qDebug() << "PMR: mergeProjects(): # of local projects = " << localProjects.size() << " # of mergin projects = " << merginProjects.size(); + + 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( new LocalProject( localProject ) ); + + MerginProject remoteEntry; + remoteEntry.projectName = project->local->projectName; + remoteEntry.projectNamespace = project->local->projectNamespace; + + if ( merginProjects.contains( remoteEntry ) ) + { + int i = merginProjects.indexOf( remoteEntry ); + project->mergin = std::unique_ptr( new MerginProject( merginProjects[i] ) ); + + if ( pendingProjects.contains( project->mergin->id() ) ) + { + TransactionStatus projectTransaction = pendingProjects.value( project->mergin->id() ); + project->mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; + 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 ) + { + // Keep all remote projects and ignore all non mergin projects from local projects + for ( const auto &remoteEntry : merginProjects ) + { + std::shared_ptr project = std::shared_ptr( new Project() ); + project->mergin = std::unique_ptr( new MerginProject( remoteEntry ) ); + + if ( pendingProjects.contains( project->mergin->id() ) ) + { + TransactionStatus projectTransaction = pendingProjects.value( project->mergin->id() ); + project->mergin->progress = projectTransaction.transferedSize / projectTransaction.totalSize; + project->mergin->pending = true; + } + + // find downloaded projects + LocalProject localProject; + localProject.projectName = project->mergin->projectName; + localProject.projectNamespace = project->mergin->projectNamespace; + + if ( localProjects.contains( localProject ) ) + { + int ix = localProjects.indexOf( localProject ); + project->local = std::unique_ptr( new LocalProject( localProjects[ix] ) ); + } + project->mergin->status = ProjectStatus::projectStatus( project ); + + mProjects << project; + } + } } -QString ProjectModel::dataDir() const +void ProjectsModel::syncProject( const QString &projectId ) { - return mLocalProjects.dataDir(); + std::shared_ptr project = projectFromId( projectId ); + + if ( project == nullptr ) + { + qDebug() << "PMR: project" << projectId << "not in projects list"; + return; + } + + if ( !project->isMergin() ) + { + qDebug() << "PMR: project" << projectId << "is not a mergin project"; + return; + } + + if ( project->mergin->pending ) + { + qDebug() << "PMR: project" << projectId << "is already syncing"; + return; + } + + if ( project->mergin->status == ProjectStatus::NoVersion || project->mergin->status == ProjectStatus::OutOfDate ) + { + qDebug() << "PMR: updating project:" << project->mergin->id(); + + bool useAuth = !mBackend->userAuth()->hasAuthData() && mModelType == ProjectModelTypes::PublicProjectsModel; + mBackend->updateProject( project->mergin->projectNamespace, project->mergin->projectName, useAuth ); + } + else if ( project->mergin->status == ProjectStatus::Modified ) + { + qDebug() << "PMR: uploading project:" << project->mergin->id(); + mBackend->uploadProject( project->mergin->projectNamespace, project->mergin->projectName ); + } } -QString ProjectModel::searchExpression() const +void ProjectsModel::stopProjectSync( const QString &projectId ) { - return mSearchExpression; + std::shared_ptr project = projectFromId( projectId ); + + if ( project == nullptr ) + { + qDebug() << "PMR: project" << projectId << "not in projects list"; + return; + } + + if ( !project->isMergin() ) + { + qDebug() << "PMR: project" << projectId << "is not a mergin project"; + return; + } + + if ( !project->mergin->pending ) + { + qDebug() << "PMR: project" << projectId << "is not pending"; + return; + } + + if ( project->mergin->status == ProjectStatus::NoVersion || project->mergin->status == ProjectStatus::OutOfDate ) + { + qDebug() << "PMR: cancelling update of project:" << project->mergin->id(); + mBackend->updateCancel( project->mergin->id() ); + } + else if ( project->mergin->status == ProjectStatus::Modified ) + { + qDebug() << "PMR: cancelling upload of project:" << project->mergin->id(); + mBackend->uploadCancel( project->mergin->id() ); + } +} + +void ProjectsModel::removeLocalProject( const QString &projectId ) +{ + mLocalProjectsManager->removeLocalProject( projectId ); +} + +void ProjectsModel::migrateProject( const QString &projectId ) +{ + // if it is indeed a local project + std::shared_ptr project = projectFromId( projectId ); + + if ( project->isLocal() ) + mBackend->migrateProjectToMergin( project->local->projectName ); } -void ProjectModel::setSearchExpression( const QString &searchExpression ) +void ProjectsModel::onProjectSyncFinished( const QString &projectDir, const QString &projectFullName, bool successfully ) { - if ( searchExpression != mSearchExpression ) + Q_UNUSED( projectDir ) + + std::shared_ptr project = projectFromId( projectFullName ); + if ( !project || !project->isMergin() || !successfully ) + return; + + project->mergin->pending = false; + project->mergin->progress = 0; + + QModelIndex ix = index( mProjects.indexOf( project ) ); + emit dataChanged( ix, ix ); + + qDebug() << "PMR: Project " << projectFullName << " finished sync"; +} + +void ProjectsModel::onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ) +{ + std::shared_ptr project = projectFromId( projectFullName ); + if ( !project || !project->isMergin() ) + return; + + project->mergin->pending = progress >= 0; + project->mergin->progress = progress >= 0 ? progress : 0; + + QModelIndex ix = index( mProjects.indexOf( project ) ); + emit dataChanged( ix, ix ); + + qDebug() << "PMR: Project " << projectFullName << " changed sync progress to " << progress; +} + +void ProjectsModel::onProjectAdded( const LocalProject &project ) +{ + // 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( new LocalProject( project ) ); + 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::shared_ptr( new Project() ); + newProject->local = std::unique_ptr( new LocalProject( project ) ); + + int insertIndex = mProjects.size(); + + beginInsertRows( QModelIndex(), insertIndex, insertIndex ); + mProjects << newProject; + endInsertRows(); + } + + qDebug() << "PMR: Added project" << project.id(); +} + +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(); + + qDebug() << "PMR: Deleted project" << project.id(); + } + 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( new LocalProject( project ) ); + if ( proj->isMergin() ) + proj->mergin->status = ProjectStatus::projectStatus( proj ); + + QModelIndex editIndex = index( mProjects.indexOf( proj ) ); + + emit dataChanged( editIndex, editIndex ); + } + qDebug() << "PMR: Data changed in project" << project.id(); +} + +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 &projectFullName ) { - return mLocalProjects.hasMerginProject( projectNamespace, projectName ); + // To ensure project will be in sync with server, send listProjectByName request. + // In theory we could send that request only for this one project. + listProjectsByName(); + + qDebug() << "PMR: Project attached to mergin " << projectFullName; } -void ProjectModel::syncedProjectFinished( const QString &projectDir, const QString &projectFullName, bool successfully ) +void ProjectsModel::setMerginApi( MerginApi *merginApi ) { + if ( !merginApi || mBackend == merginApi ) + return; - // Do basic validity check - if ( successfully ) + mBackend = merginApi; + qDebug() << "PMR: New MA: " << mBackend; + initializeProjectsModel(); +} + +void ProjectsModel::setLocalProjectsManager( LocalProjectsManager *localProjectsManager ) +{ + if ( !localProjectsManager || mLocalProjectsManager == localProjectsManager ) + return; + + mLocalProjectsManager = localProjectsManager; + qDebug() << "PMR: New LPM: " << mLocalProjectsManager; + initializeProjectsModel(); +} + +void ProjectsModel::setModelType( ProjectsModel::ProjectModelTypes modelType ) +{ + if ( mModelType == modelType ) + return; + + mModelType = modelType; + qDebug() << "PMR: New Type: " << mModelType; + initializeProjectsModel(); +} + +QString ProjectsModel::modelTypeToFlag() const +{ + switch ( mModelType ) { - QString errMsg; - mLocalProjects.findQgisProjectFile( projectDir, errMsg ); - mLocalProjects.updateProjectErrors( projectDir, errMsg ); + case CreatedProjectsModel: + return QStringLiteral( "created" ); + case SharedProjectsModel: + return QStringLiteral( "shared" ); + default: + return QStringLiteral( "" ); } +} - reloadProjectFiles( projectDir, projectFullName, successfully ); +void ProjectsModel::printProjects() const // TODO: Helper function, remove after refactoring is done +{ + qDebug() << "PMR: Model " << this << " with type " << modelTypeToFlag() << " has projects: "; + for ( const auto &proj : mProjects ) + { + QString lcl = proj->isLocal() ? "local" : ""; + QString mrgn = proj->isMergin() ? "mergin" : ""; + + if ( proj->isLocal() ) + { + qDebug() << " - " << proj->local->projectNamespace << proj->local->projectName << lcl << mrgn; + } + else if ( proj->isMergin() ) + { + qDebug() << " - " << proj->mergin->projectNamespace << proj->mergin->projectName << lcl << mrgn; + } + } +} + +QStringList ProjectsModel::projectNames() const +{ + QStringList projectNames; + LocalProjectsList projects = mLocalProjectsManager->projects(); + + for ( const auto &proj : projects ) + { + if ( !proj.projectName.isEmpty() && !proj.projectNamespace.isEmpty() ) + projectNames << proj.id(); + } + + return projectNames; } -void ProjectModel::addLocalProject( const QString &projectDir ) // TODO: not needed if localProjectsManager emits added signal +void ProjectsModel::loadLocalProjects() { - Q_UNUSED( projectDir ); - mLocalProjects.reloadProjectDir(); - findProjectFiles(); + if ( mModelType == LocalProjectsModel ) + { + beginResetModel(); + mergeProjects( MerginProjectsList(), Transactions() ); // Fills model with local projects + printProjects(); + 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 ( int ix = 0; ix < mProjects.size(); ++ix ) + { + if ( mProjects[ix]->isMergin() && mProjects[ix]->mergin->id() == projectId ) + return mProjects[ix]; + else if ( mProjects[ix]->isLocal() && mProjects[ix]->local->id() == projectId ) + return mProjects[ix]; + } - Q_UNUSED( projectName ); - findProjectFiles(); + return nullptr; +} + +ProjectsModel::ProjectModelTypes ProjectsModel::modelType() const +{ + return mModelType; } diff --git a/app/projectsmodel.h b/app/projectsmodel.h index 6a2905c7b..07814ac1c 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,152 @@ * * ***************************************************************************/ - #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 */ -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, // Filled with ProjectFullName for time being + ProjectDirectory, + ProjectDescription, + ProjectPending, + ProjectIsMergin, + ProjectIsLocal, + ProjectFilePath, + ProjectIsValid, + ProjectSyncStatus, + ProjectSyncProgress, + ProjectRemoteError }; - Q_ENUMS( Roles ) + Q_ENUM( Roles ) + + /** + * \brief The ProjectModelTypes enum + */ + enum ProjectModelTypes + { + EmptyProjectsModel = 0, // default, holding no projects ~ invalid model + LocalProjectsModel, + CreatedProjectsModel, + SharedProjectsModel, + PublicProjectsModel, + RecentProjectsModel + }; + Q_ENUM( ProjectModelTypes ) + + ProjectsModel( QObject *parent = nullptr ); + ~ProjectsModel() override {}; - explicit ProjectModel( LocalProjectsManager &localProjects, QObject *parent = nullptr ); - ~ProjectModel() 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 ) + Q_PROPERTY( bool hasMoreProjects READ hasMoreProjects NOTIFY hasMoreProjectsChanged ) + + // Needed methods from QAbstractListModel Q_INVOKABLE QVariant data( const QModelIndex &index, int role ) const override; Q_INVOKABLE QModelIndex index( int row, int column = 0, const QModelIndex &parent = QModelIndex() ) const override; - Q_INVOKABLE 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; + //! Called to list projects, either fetch more or get first + Q_INVOKABLE void listProjects( const QString &searchExpression = QString(), int page = 1 ); + + //! Called to list projects, either fetch more or get first + Q_INVOKABLE void listProjectsByName(); + + //! Syncs specified project - upload or update + Q_INVOKABLE void syncProject( const QString &projectId ); + + //! Stops running project upload or update + Q_INVOKABLE void stopProjectSync( const QString &projectId ); - QString searchExpression() const; - void setSearchExpression( const QString &searchExpression ); + //! Forwards call to LocalProjectsManager to remove local project + Q_INVOKABLE void removeLocalProject( const QString &projectId ); - // Test function - bool containsProject( const QString &projectNamespace, const QString &projectName ); + //! Migrates local project to mergin + Q_INVOKABLE void migrateProject( const QString &projectId ); + + Q_INVOKABLE void fetchAnotherPage( const QString &searchExpression ); + + Q_INVOKABLE QVariant dataFrom( int fromRole, QVariant fromValue, int desiredRole ) const; + + //! Method merging local and remote projects based on the model type + void mergeProjects( const MerginProjectsList &merginProjects, Transactions pendingProjects, bool keepPrevious = false ); + + ProjectsModel::ProjectModelTypes modelType() const; + + MerginApi *merginApi() const { return mBackend; } + + LocalProjectsManager *localProjectsManager() const { return mLocalProjectsManager; } + + bool hasMoreProjects() const; 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 = true ); + void onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ); + void onProjectDetachedFromMergin( const QString &projectFullName ); + void onProjectAttachedToMergin( const QString &projectFullName ); + + // 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(); private: - void reloadProjectFiles( QString projectFolder, QString projectName, bool successful ); + QString modelTypeToFlag() const; + void printProjects() 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; // Description! - bool isValid; - - /** - * Ordering of local projects: first non-mergin projects (using folder name), - * then mergin projects (sorted first by namespace, then project name) - */ - bool operator < ( const ProjectFile &other ) const - { - if ( projectNamespace.isEmpty() && other.projectNamespace.isEmpty() ) - { - return folderName.compare( other.folderName, Qt::CaseInsensitive ) < 0; - } - if ( !projectNamespace.isEmpty() && other.projectNamespace.isEmpty() ) - { - return false; - } - if ( projectNamespace.isEmpty() && !other.projectNamespace.isEmpty() ) - { - return true; - } - - if ( projectNamespace.compare( other.projectNamespace, Qt::CaseInsensitive ) == 0 ) - { - return projectName.compare( other.projectName, Qt::CaseInsensitive ) < 0; - } - if ( projectNamespace.compare( other.projectNamespace, Qt::CaseInsensitive ) < 0 ) - { - return true; - } - else - return false; - } - }; - LocalProjectsManager &mLocalProjects; - QList mProjectFiles; - QString mSearchExpression; + bool containsProject( QString projectId ) const; + std::shared_ptr projectFromId( QString projectId ) const; + + 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; }; #endif // PROJECTSMODEL_H diff --git a/app/projectsproxymodel.cpp b/app/projectsproxymodel.cpp new file mode 100644 index 000000000..c355423c7 --- /dev/null +++ b/app/projectsproxymodel.cpp @@ -0,0 +1,98 @@ +/*************************************************************************** + * * + * 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(); + + 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(); + qDebug() << "Comparing " << lProjectFullName << rProjectFullName; + + return lProjectFullName.compare( rProjectFullName, Qt::CaseInsensitive ) < 0; + } + if ( !lProjectIsMergin && rProjectIsMergin ) + { + return true; + } + if ( lProjectIsMergin && !rProjectIsMergin ) + { + return false; + } + + QString lNamespace = mModel->data( left, ProjectsModel::ProjectNamespace ).toString(); + QString lProjectName = mModel->data( left, ProjectsModel::ProjectName ).toString(); + QString rNamespace = mModel->data( right, ProjectsModel::ProjectNamespace ).toString(); + QString rProjectName = mModel->data( right, ProjectsModel::ProjectName ).toString(); + + if ( lNamespace == rNamespace ) + { + return lProjectName.compare( rProjectName, Qt::CaseInsensitive ) < 0; + } + return lNamespace.compare( rNamespace, Qt::CaseInsensitive ) < 0; + } + return false; +} diff --git a/app/projectsproxymodel.h b/app/projectsproxymodel.h new file mode 100644 index 000000000..3c9dd885f --- /dev/null +++ b/app/projectsproxymodel.h @@ -0,0 +1,53 @@ +/*************************************************************************** + * * + * 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 + */ +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; + ProjectsModel::ProjectModelTypes mModelType = ProjectsModel::EmptyProjectsModel; + QString mSearchExpression; +}; + +#endif // PROJECTSPROXYMODEL_H 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 87a5989cf..b2a8da657 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" @@ -81,6 +82,10 @@ QtObject { property var valueRelationIcon: "qrc:/value_relation_open.svg" property var comboboxIcon: "qrc:/combobox.svg" property var qrCodeIcon: "qrc:/qrcode.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 cc59b824a..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.fontColorBright : 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..947e235c5 --- /dev/null +++ b/app/qml/ProjectListPage.qml @@ -0,0 +1,62 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick 2.0 +import lc 1.0 + +import "./components" + +Item { + id: root + + property int projectModelType: ProjectsModel.EmptyProjectsModel + property string activeProjectId: "" + + property alias list: projectlist + + signal openProjectRequested( string projectId, string projectFilePath ) + signal showLocalChangesRequested( string projectId ) + + function refreshProjectsList() { + searchBar.deactivate() + projectlist.refreshProjectList() + } + + SearchBar { + id: searchBar + + anchors { + top: parent.top + left: parent.left + right: parent.right + bottom: projectlist.top + } + + allowTimer: true + + onSearchTextChanged: projectlist.searchTextChanged( text ) + } + + ProjectList { + id: projectlist + + projectModelType: root.projectModelType + activeProjectId: root.activeProjectId + + anchors { + left: parent.left + right: parent.right + top: searchBar.bottom + bottom: parent.bottom + } + + onOpenProjectRequested: root.openProjectRequested( projectId, projectFilePath ) + onShowLocalChangesRequested: root.showLocalChangesRequested( projectId ) + } +} diff --git a/app/qml/ProjectPanel.qml b/app/qml/ProjectPanel.qml new file mode 100644 index 000000000..e0774f153 --- /dev/null +++ b/app/qml/ProjectPanel.qml @@ -0,0 +1,653 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtGraphicalEffects 1.0 +import QtQuick.Dialogs 1.2 +import QgsQuick 0.1 as QgsQuick +import lc 1.0 +import "." // import InputStyle singleton +import "./components/" + +Item { + id: root + + property string activeProjectId: "" + property string activeProjectPath: "" + + property real rowHeight: InputStyle.rowHeightHeader * 1.2 + property real panelMargin: InputStyle.panelMargin + + signal openProjectRequested( string projectId, string projectPath ) + + function openPanel() { + root.visible = true + stackView.visible = true + } + + function hidePanel() { + root.visible = false + stackView.clearStackAndClose() + } + + visible: false + focus: true + + onFocusChanged: { // pass focus to stackview + stackView.focus = true + } + + StackView { + id: stackView + + initialItem: projectsPanelComp + anchors.fill: parent + focus: true + visible: false + z: root.z + 1 + property bool pending: false + + function clearStackAndClose() { + if ( stackView.depth > 1 ) + stackView.pop( null ) // pops everything besides an initialItem + + stackView.visible = false + } + + function popOnePageOrClose() { + if ( stackView.depth > 1 ) + { + stackView.pop() + } + } + + Keys.onReleased: { + if (event.key === Qt.Key_Back || event.key === Qt.Key_Escape) { + event.accepted = true; + + if (stackView.depth > 1) { + stackView.currentItem.back() + } + else if (root.activeProjectPath) { + stackView.clearStackAndClose() + root.visible = false + } + } + } + + onVisibleChanged: { + if ( stackView.visible ) + stackView.forceActiveFocus() + } + } + + BusyIndicator { + id: busyIndicator + width: parent.width/8 + height: width + running: stackView.pending + visible: running + anchors.centerIn: parent + z: parent.z + 1 + } + + Component { + id: projectsPanelComp + + Page { + id: projectsPage + + function setupProjectOpen( projectId, projectPath ) { + activeProjectId = projectId + activeProjectPath = projectPath + openProjectRequested( projectId, projectPath ) + + if ( projectId && projectPath ) // this is not project reset + hidePanel() + } + + function showChanges( projectId ) { + if ( __merginProjectStatusModel.loadProjectInfo( projectId ) ) { + stackView.push( statusPanelComp ) + } + else __inputUtils.showNotification( qsTr( "No Changes" ) ) + } + + function refreshProjectList() { + + stackView.pending = true + switch( pageContent.state ) { + case "local": + localProjectsPage.refreshProjectsList() + break + case "created": + createdProjectsPage.refreshProjectsList() + break + case "shared": + sharedProjectsPage.refreshProjectsList() + break + case "public": + publicProjectsPage.refreshProjectsList() + break + } + } + + header: PanelHeader { + id: pageHeader + + titleText: qsTr("Projects") + color: InputStyle.clrPanelMain + height: InputStyle.rowHeightHeader + rowHeight: InputStyle.rowHeightHeader + + onBack: { + if ( root.activeProjectId ) { + root.hidePanel() + } + } + withBackButton: root.activeProjectPath + + Item { + id: avatar + + width: InputStyle.rowHeightHeader * 0.8 + height: InputStyle.rowHeightHeader + anchors.right: parent.right + anchors.rightMargin: root.panelMargin + + Rectangle { + id: avatarImage + + anchors.centerIn: parent + width: avatar.width + height: avatar.width + color: InputStyle.fontColor + radius: width*0.5 + antialiasing: true + + MouseArea { + anchors.fill: parent + onClicked: { + if (__merginApi.userAuth.hasAuthData() && __merginApi.apiVersionStatus === MerginApiStatus.OK) { + __merginApi.getUserInfo() + stackView.push( accountPanelComp ) + reloadList.visible = false + } + else + stackView.push( authPanelComp, { state: "login" }) + } + } + + Image { + id: userIcon + + anchors.centerIn: avatarImage + source: 'account.svg' + height: avatarImage.height * 0.8 + width: height + sourceSize.width: width + sourceSize.height: height + fillMode: Image.PreserveAspectFit + } + + ColorOverlay { + anchors.fill: userIcon + source: userIcon + color: "#FFFFFF" + } + } + } + } + + background: Rectangle { + anchors.fill: parent + color: InputStyle.clrPanelMain + } + + Item { + id: pageContent + + anchors.fill: parent + + states: [ + State { + name: "local" + }, + State { + name: "created" + }, + State { + name: "shared" + }, + State { + name: "public" + } + ] + + onStateChanged: { + refreshProjectList() + console.log("New state: ", pageContent.state) + } + + StackLayout { + id: projectListLayout + + anchors.fill: parent + currentIndex: pageFooter.currentIndex + + ProjectListPage { + id: localProjectsPage + + projectModelType: ProjectsModel.LocalProjectsModel + activeProjectId: root.activeProjectId + list.visible: !stackView.pending + + onOpenProjectRequested: setupProjectOpen( projectId, projectFilePath ) + onShowLocalChangesRequested: showChanges( projectId ) + list.onActiveProjectDeleted: setupProjectOpen( "", "" ) + } + + ProjectListPage { + id: createdProjectsPage + + projectModelType: ProjectsModel.CreatedProjectsModel + activeProjectId: root.activeProjectId + list.visible: !stackView.pending + + onOpenProjectRequested: setupProjectOpen( projectId, projectFilePath ) + onShowLocalChangesRequested: showChanges( projectId ) + list.onActiveProjectDeleted: setupProjectOpen( "", "" ) + } + + ProjectListPage { + id: sharedProjectsPage + + projectModelType: ProjectsModel.SharedProjectsModel + activeProjectId: root.activeProjectId + list.visible: !stackView.pending + + onOpenProjectRequested: setupProjectOpen( projectId, projectFilePath ) + onShowLocalChangesRequested: showChanges( projectId ) + list.onActiveProjectDeleted: setupProjectOpen( "", "" ) + } + + ProjectListPage { + id: publicProjectsPage + + projectModelType: ProjectsModel.PublicProjectsModel + activeProjectId: root.activeProjectId + list.visible: !stackView.pending + + onOpenProjectRequested: setupProjectOpen( projectId, projectFilePath ) + onShowLocalChangesRequested: showChanges( projectId ) + list.onActiveProjectDeleted: setupProjectOpen( "", "" ) + } + } + } + + footer: TabBar { + id: pageFooter + + property int itemSize: pageFooter.height * 0.8 + + spacing: 0 + contentHeight: InputStyle.rowHeightHeader + + TabButton { + id: localProjectsBtn + + background: Rectangle { + anchors.fill: parent + color: InputStyle.fontColor + } + + MainPanelButton { + id: localProjectsInnerBtn + + text: qsTr("Home") + imageSource: "home.svg" + width: pageFooter.itemSize + + handleClicks: false + faded: pageFooter.currentIndex !== localProjectsBtn.TabBar.index + } + + onClicked: pageContent.state = "local" + } + + TabButton { + id: createdProjectsBtn + + background: Rectangle { + anchors.fill: parent + color: InputStyle.fontColor + } + + MainPanelButton { + id: createdProjectsInnerBtn + + text: qsTr("My projects") + imageSource: "account.svg" + width: pageFooter.itemSize + + handleClicks: false + faded: pageFooter.currentIndex !== createdProjectsBtn.TabBar.index + } + + onClicked: pageContent.state = "created" + } + + TabButton { + id: sharedProjectsBtn + + background: Rectangle { + anchors.fill: parent + color: InputStyle.fontColor + } + + MainPanelButton { + id: sharedProjectsInnerBtn + + imageSource: "account-multi.svg" + width: pageFooter.itemSize + text: parent.width > sharedProjectsInnerBtn.width * 2 ? qsTr("Shared with me") : qsTr("Shared") + + handleClicks: false + faded: pageFooter.currentIndex !== sharedProjectsBtn.TabBar.index + } + + onClicked: pageContent.state = "shared" + } + + TabButton { + id: publicProjectsBtn + + background: Rectangle { + anchors.fill: parent + color: InputStyle.fontColor + } + + MainPanelButton { + id: publicProjectsInnerBtn + + text: qsTr("Explore") + imageSource: "explore.svg" + width: pageFooter.itemSize + + handleClicks: false + faded: pageFooter.currentIndex !== publicProjectsBtn.TabBar.index + } + + onClicked: pageContent.state = "public" + } + } + +//------------------------------------------------- +// Text { +// id: noProjectsText +// anchors.fill: parent +// textFormat: Text.RichText +// text: "" + +// qsTr("No downloaded projects found.%1Learn %2how to create projects%3 and %4download them%3 onto your device.") +// .arg("
") +// .arg("") +// .arg("") +// .arg("") + +// onLinkActivated: Qt.openUrlExternally(link) +// visible: grid.count === 0 && !storagePermissionText.visible +// color: InputStyle.fontColor +// font.pixelSize: InputStyle.fontPixelSizeNormal +// font.bold: true +// verticalAlignment: Text.AlignVCenter +// horizontalAlignment: Text.AlignHCenter +// wrapMode: Text.WordWrap +// padding: InputStyle.panelMargin/2 +// } + +// Text { +// id: storagePermissionText +// anchors.fill: parent +// textFormat: Text.RichText +// text: "" + +// qsTr("Input needs a storage permission, %1click to grant it%2 and then restart application.") +// .arg("") +// .arg("") + +// onLinkActivated: { +// if ( __inputUtils.acquireStoragePermission() ) { +// restartAppDialog.open() +// } +// } +// visible: !__inputUtils.hasStoragePermission() +// color: InputStyle.fontColor +// font.pixelSize: InputStyle.fontPixelSizeNormal +// font.bold: true +// verticalAlignment: Text.AlignVCenter +// horizontalAlignment: Text.AlignHCenter +// wrapMode: Text.WordWrap +// padding: InputStyle.panelMargin/2 +// } +// } +//------------------------------------------------- + +//------------------------------------------------- +// TODO: unable to get list of the projects +// Label { +// anchors.fill: parent +// horizontalAlignment: Qt.AlignHCenter +// verticalAlignment: Qt.AlignVCenter +// visible: !merginProjectsList.contentHeight +// text: reloadList.visible ? qsTr("Unable to get the list of projects.") : qsTr("No projects found!") +// color: InputStyle.fontColor +// font.pixelSize: InputStyle.fontPixelSizeNormal +// font.bold: true +// } +//------------------------------------------------- + +// Toolbar +//------------------------------------------------- + +// onHighlightedChanged: { +//// searchBar.deactivate() +// if (toolbar.highlighted === homeBtn.text) { +//// __projectsModel.searchExpression = "" +// } else { +// __merginApi.pingMergin() <------------------!!! +// } +// } +//------------------------------------------------- + + // Other components + + Item { + id: reloadList + width: parent.width + height: root.rowHeight + visible: false + Layout.alignment: Qt.AlignVCenter + y: root.height/3 * 2 + + Button { + id: reloadBtn + width: reloadList.width - 2* InputStyle.panelMargin + height: reloadList.height + text: qsTr("Retry") + font.pixelSize: reloadBtn.height/2 + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + stackView.pending = true + // filters suppose to not change + __merginApi.listProjects( searchBar.text ) + reloadList.visible = false + } + background: Rectangle { + color: InputStyle.highlightColor + } + + contentItem: Text { + text: reloadBtn.text + font: reloadBtn.font + color: InputStyle.clrPanelMain + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + } + } + + Connections { + target: __projectWizard + onProjectCreated: { + if (stackView.currentItem.objectName === "projectWizard") { + stackView.popOnePageOrClose() + } + } + } + + Connections { + target: __merginApi + onListProjectsFinished: { + stackView.pending = false + } + onListProjectsFailed: { + reloadList.visible = true + } + onListProjectsByNameFinished: stackView.pending = false + onApiVersionStatusChanged: { + stackView.pending = false + if (__merginApi.apiVersionStatus === MerginApiStatus.OK && stackView.currentItem.objectName === "authPanel") { + if (__merginApi.userAuth.hasAuthData()) { + refreshProjectList() + } else if (toolbar.highlighted !== homeBtn.text) { + if (stackView.currentItem.objectName !== "authPanel") { + stackView.push(authPanelComp, {state: "login"}) + } + } + } + } + onAuthRequested: { + stackView.pending = false + stackView.push(authPanelComp, {state: "login"}) + } + onAuthChanged: { + stackView.pending = false + if (__merginApi.userAuth.hasAuthData()) { + refreshProjectList() + root.forceActiveFocus() + } + stackView.popOnePageOrClose() + + } + onAuthFailed: { + homeBtn.activated() + stackView.pending = false + root.forceActiveFocus() + } + onRegistrationFailed: stackView.pending = false + onRegistrationSucceeded: stackView.pending = false + + //TODO: on sync project failed: push auth panel too + } + } + } + + Component { + id: authPanelComp + AuthPanel { + id: authPanel + objectName: "authPanel" + visible: false + pending: stackView.pending + height: root.height + width: root.width + toolbarHeight: InputStyle.rowHeightHeader + onBack: { + stackView.popOnePageOrClose() + if (stackView.currentItem.objectName === "projectsPanel") { + __merginApi.authFailed() // activate homeBtn + } + } + } + } + + Component { + id: statusPanelComp + ProjectStatusPanel { + id: statusPanel + height: root.height + width: root.width + visible: false + onBack: stackView.popOnePageOrClose() + } + } + + Component { + id: accountPanelComp + + AccountPage { + id: accountPanel + height: root.height + width: root.width + visible: true + onBack: { + stackView.popOnePageOrClose() + } + onManagePlansClicked: { + if (__purchasing.hasInAppPurchases && (__purchasing.hasManageSubscriptionCapability || !__merginApi.userInfo.ownsActiveSubscription )) { + stackView.push( subscribePanelComp) + } else { + Qt.openUrlExternally(__purchasing.subscriptionManageUrl); + } + } + onSignOutClicked: { + if (__merginApi.userAuth.hasAuthData()) { + __merginApi.clearAuth() + stackView.popOnePageOrClose() + } + } + onRestorePurchasesClicked: { + __purchasing.restore() + } + } + } + + Component { + id: subscribePanelComp + + SubscribePage { + id: subscribePanel + height: root.height + width: root.width + onBackClicked: { + stackView.popOnePageOrClose() + } + onSubscribeClicked: { + stackView.popOnePageOrClose() + } + } + } + + Component { + id: projectWizardComp + + ProjectWizardPage { + id: projectWizardPanel + objectName: "projectWizard" + height: root.height + width: root.width + onBack: { + stackView.popOnePageOrClose() + } + } + } + +} diff --git a/app/qml/components/ProjectDelegateItem.qml b/app/qml/components/ProjectDelegateItem.qml new file mode 100644 index 000000000..7b475365c --- /dev/null +++ b/app/qml/components/ProjectDelegateItem.qml @@ -0,0 +1,366 @@ +/*************************************************************************** + * * + * 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(',') + + if ( contextMenu.contentData ) + while( contextMenu.contentData.length > 0 ) + contextMenu.takeItem( 0 ); + + items.forEach( item => { contextMenu.addItem( + menuItemComponent.createObject( null, itemsMap[item] ) ) + }) + contextMenu.height = items.length * root.menuItemHeight + } + + onProjectStatusChanged: { + console.log("Project " + projectId + " status changed to: " + projectStatus) + fillMoreMenu() + } + + color: root.highlight || !projectIsValid ? InputStyle.fontColorBright : 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: contextMenu.open() + } + } + } + + Rectangle { // border line + color: InputStyle.panelBackground2 + width: root.width + height: root.borderWidth + anchors.bottom: parent.bottom + } + + // More Menu + Menu { + id: contextMenu + + width: Math.min( root.width, 300 * QgsQuick.Utils.dp ) + leftMargin: Math.max( root.width - width, 0 ) + z: 100 + + enter: Transition { + ParallelAnimation { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 100 } + } + } + exit: Transition { + ParallelAnimation { + NumberAnimation { property: "opacity"; from: 1.0; to: 0; duration: 100 } + } + } + + Component.onCompleted: fillMoreMenu() + + //! sets y-offset either above or below related item according relative position to end of the list + onAboutToShow: { + let itemRelativeY = parent.y - root.viewContentY + if ( itemRelativeY + contextMenu.height >= root.viewHeight ) + contextMenu.y = -contextMenu.height + parent.height / 3 + else + contextMenu.y = ( parent.height * 2 ) / 3 + } + } + + Component { + id: menuItemComponent + + MenuItem { + id: menuItem + + property string name: "" + property var callback: function cb() {} // default callback + property string iconSource: "" + + height: root.menuItemHeight + + ExtendedMenuItem { + height: parent.height + rowHeight: parent.height * 0.8 + width: parent.width + contentText: menuItem.name + imageSource: menuItem.iconSource + overlayImage: true + } + + onClicked: callback() + } + } +} diff --git a/app/qml/components/ProjectList.qml b/app/qml/components/ProjectList.qml new file mode 100644 index 000000000..499ddbefa --- /dev/null +++ b/app/qml/components/ProjectList.qml @@ -0,0 +1,179 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Dialogs 1.2 +import lc 1.0 +import "../" +import "." + +Item { + id: root + + property int projectModelType: ProjectsModel.EmptyProjectsModel + property string activeProjectId: "" + + signal openProjectRequested( string projectId, string projectFilePath ) + signal showLocalChangesRequested( string projectId ) + signal activeProjectDeleted() + + function searchTextChanged( searchText ) { + if ( projectModelType === ProjectsModel.PublicProjectsModel ) + { + controllerModel.listProjects( searchText ) + } + else viewModel.searchExpression = searchText + } + + function refreshProjectList() { + controllerModel.listProjects() + } + + function modelData( fromRole, fromValue, desiredRole ) { + controllerModel.dataFrom( fromRole, fromValue, desiredRole ) + } + + ListView { + id: listview + + Component.onCompleted: { + // set proper footer (add project / fetch more) + if ( root.projectModelType === ProjectsModel.LocalProjectsModel ) + listview.footer = addProjectButtonComponent + else + listview.footer = fetchMoreButtonComponent + } + + anchors.fill: parent + clip: true + maximumFlickVelocity: __androidUtils.isAndroid ? InputStyle.scrollVelocityAndroid : maximumFlickVelocity + + // Proxy model with source projects model + model: ProjectsProxyModel { + id: viewModel + + projectSourceModel: ProjectsModel { + id: controllerModel + + merginApi: __merginApi + localProjectsManager: __localProjectsManager + modelType: root.projectModelType + } + } + + // Project delegate + delegate: ProjectDelegateItem { + id: projectDelegate + + width: parent.width + height: InputStyle.rowHeightHeader * 1.2 + + 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: root.openProjectRequested( projectId, model.ProjectFilePath ) + onSyncRequested: controllerModel.syncProject( projectId ) + onMigrateRequested: controllerModel.migrateProject( projectId ) + onRemoveRequested: { + removeDialog.relatedProjectId = projectId + removeDialog.open() + } + onStopSyncRequested: controllerModel.stopProjectSync( projectId ) + onShowChangesRequested: root.showLocalChangesRequested( projectId ) + } + } + + Component { + id: fetchMoreButtonComponent + + DelegateButton { + width: parent.width + height: visible ? InputStyle.rowHeight : 0 + text: qsTr( "Fetch more" ) + + visible: controllerModel.hasMoreProjects + onClicked: controllerModel.fetchAnotherPage( viewModel.searchExpression ) + } + } + + Component { + id: addProjectButtonComponent + + DelegateButton { + width: parent.width + height: InputStyle.rowHeight + text: qsTr("Create project") + + onClicked: { + if ( __inputUtils.hasStoragePermission() ) { + stackView.push(projectWizardComp) + } + else if ( __inputUtils.acquireStoragePermission() ) { +// TODO: reload project dir ~> rather connect to permission granted signal and reload project dir + restartAppDialog.open() + } + } + } + } + + MessageDialog { + id: removeDialog + property string relatedProjectId + + title: qsTr( "Remove project" ) + text: qsTr( "Any unsynchronized changes will be lost." ) + icon: StandardIcon.Warning + standardButtons: StandardButton.Ok | StandardButton.Cancel + + //! Using onButtonClicked instead of onAccepted,onRejected which have been called twice + onButtonClicked: { + if (clickedButton === StandardButton.Ok) { + if (relatedProjectId === "") + return + + if ( root.activeProjectId === relatedProjectId ) + root.activeProjectDeleted() + + controllerModel.removeLocalProject( relatedProjectId ) + + removeDialog.relatedProjectId = "" + visible = false + } + else if (clickedButton === StandardButton.Cancel) { + removeDialog.relatedProjectId = "" + visible = false + } + } + } + + MessageDialog { + id: restartAppDialog + + title: qsTr( "Input needs to be restarted" ) + text: qsTr( "To apply changes after granting storage permission, Input needs to be restarted. Click close and open Input again." ) + icon: StandardIcon.Warning + visible: false + standardButtons: StandardButton.Close + onRejected: __inputUtils.quitApp() + } +} diff --git a/app/qml/main.qml b/app/qml/main.qml index caf6e222d..7a3151996 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -237,21 +237,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 @@ -433,7 +434,7 @@ ApplicationWindow { gpsIndicatorColor: getGpsIndicatorColor() - onOpenProjectClicked: openProjectPanel.openPanel() + onOpenProjectClicked: projectPanel.openPanel() onOpenMapThemesClicked: mapThemesPanel.visible = true onMyLocationClicked: { mapCanvas.mapSettings.setCenter(positionKit.projectedPosition) @@ -559,26 +560,26 @@ ApplicationWindow { } } - MerginProjectPanel { - id: openProjectPanel + ProjectPanel { + id: projectPanel height: window.height width: window.width z: zPanel onVisibleChanged: { - if (openProjectPanel.visible) - openProjectPanel.forceActiveFocus() + if (projectPanel.visible) + projectPanel.forceActiveFocus() else { mainPanel.forceActiveFocus() } } - onActiveProjectIndexChanged: { - openProjectPanel.activeProjectPath = __projectsModel.data(__projectsModel.index(openProjectPanel.activeProjectIndex), ProjectModel.Path) - __appSettings.defaultProject = openProjectPanel.activeProjectPath - __appSettings.activeProject = openProjectPanel.activeProjectPath - __loader.load(openProjectPanel.activeProjectPath) + onOpenProjectRequested: { + console.log("loading ", projectPath) + __appSettings.defaultProject = projectPath + __appSettings.activeProject = projectPath + __loader.load( projectPath ) } } @@ -657,17 +658,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 0eee56e99..17972bdec 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 @@ -59,8 +58,11 @@ components/SettingsSwitch.qml components/SimpleTextWithIcon.qml components/Symbol.qml + components/ProjectDelegateItem.qml CodeReader.qml CodeReaderHandler.qml CodeReaderOverlay.qml + components/ProjectList.qml + ProjectListPage.qml diff --git a/app/sources.pri b/app/sources.pri index f270a9b89..9522138d5 100644 --- a/app/sources.pri +++ b/app/sources.pri @@ -7,7 +7,6 @@ layersproxymodel.cpp \ localprojectsmanager.cpp \ main.cpp \ merginprojectmetadata.cpp \ -projectsmodel.cpp \ projectwizard.cpp \ loader.cpp \ digitizingcontroller.cpp \ @@ -17,7 +16,6 @@ merginapi.cpp \ merginapistatus.cpp \ merginsubscriptionstatus.cpp \ merginsubscriptiontype.cpp \ -merginprojectmodel.cpp \ merginprojectstatusmodel.cpp \ merginuserauth.cpp \ merginuserinfo.cpp \ @@ -32,9 +30,9 @@ ios/iosutils.cpp \ inputprojutils.cpp \ codefilter.cpp \ qrdecoder.cpp \ -models/projectsproxymodel_future.cpp \ -models/projectsmodel_future.cpp \ -project_future.cpp \ +project.cpp \ +projectsmodel.cpp \ +projectsproxymodel.cpp \ exists(merginsecrets.cpp) { message("Using production Mergin API_KEYS") @@ -52,7 +50,6 @@ layersmodel.h \ layersproxymodel.h \ localprojectsmanager.h \ merginprojectmetadata.h \ -projectsmodel.h \ projectwizard.h \ loader.h \ digitizingcontroller.h \ @@ -62,7 +59,6 @@ merginapi.h \ merginapistatus.h \ merginsubscriptionstatus.h \ merginsubscriptiontype.h \ -merginprojectmodel.h \ merginprojectstatusmodel.h \ merginuserauth.h \ merginuserinfo.h \ @@ -77,9 +73,9 @@ ios/iosutils.h \ inputprojutils.h \ codefilter.h \ qrdecoder.h \ -models/projectsproxymodel_future.h \ -models/projectsmodel_future.h \ -project_future.h \ +project.h \ +projectsmodel.h \ +projectsproxymodel.h \ contains(DEFINES, INPUT_TEST) { diff --git a/app/test/testmerginapi.cpp b/app/test/testmerginapi.cpp index f0cbc2656..6f30522bd 100644 --- a/app/test/testmerginapi.cpp +++ b/app/test/testmerginapi.cpp @@ -159,7 +159,7 @@ void TestMerginApi::testDownloadProject() // check that the local projects are updated QVERIFY( mApi->localProjectsManager().hasMerginProject( mUsername, projectName ) ); - LocalProjectInfo project = mApi->localProjectsManager().projectFromMerginName( projectNamespace, projectName ); + LocalProjectInfo project = mApi->localProjectsManager().projectFromFullName( projectNamespace, projectName ); QVERIFY( project.isValid() ); QCOMPARE( project.projectDir, mApi->projectsPath() + "/" + projectName ); QCOMPARE( project.serverVersion, 1 ); @@ -208,7 +208,7 @@ void TestMerginApi::createRemoteProject( MerginApi *api, const QString &projectN QCOMPARE( info.size(), 0 ); QVERIFY( dir.isEmpty() ); - api->localProjectsManager().removeProject( projectDir ); + api->localProjectsManager().removeLocalProject( projectDir ); QCOMPARE( QFileInfo( projectDir ).size(), 0 ); QVERIFY( QDir( projectDir ).isEmpty() ); @@ -1302,7 +1302,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 @@ -1348,7 +1348,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 ); @@ -1401,7 +1401,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 ); @@ -1471,7 +1471,7 @@ void TestMerginApi::deleteLocalProject( MerginApi *api, const QString &projectNa QDir projectDir( project.projectDir ); projectDir.removeRecursively(); - api->localProjectsManager().removeProject( project.projectDir ); + api->localProjectsManager().removeLocalProject( project.projectDir ); } void TestMerginApi::downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName )