From 698e12356dc3034ab703274903f193d8d04951d8 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Wed, 25 Dec 2024 10:42:42 +0700 Subject: [PATCH 01/43] Add a qtwebdav vcpkg port --- src/core/CMakeLists.txt | 11 ++++++++ vcpkg.json | 1 + vcpkg/ports/qtwebdav/portfile.cmake | 24 +++++++++++++++++ vcpkg/ports/qtwebdav/static.patch | 40 +++++++++++++++++++++++++++++ vcpkg/ports/qtwebdav/vcpkg.json | 16 ++++++++++++ 5 files changed, 92 insertions(+) create mode 100644 vcpkg/ports/qtwebdav/portfile.cmake create mode 100644 vcpkg/ports/qtwebdav/static.patch create mode 100644 vcpkg/ports/qtwebdav/vcpkg.json diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 89c843e16e..15766e4eea 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -369,6 +369,17 @@ target_link_libraries( Qca::qca libzip::zip) +find_library(QtWebDAV-LIBRARY QtWebDAV) +if(QtWebDAV-LIBRARY) + message(STATUS "Found QtWebDAV: ${QtWebDAV-LIBRARY}") + target_link_libraries(qfield_core PUBLIC ${QtWebDAV-LIBRARY}) +else() + message( + FATAL_ERROR + "Fail to find QtWebDAV library, make sure it is present in CMAKE_PREFIX_PATH" + ) +endif() + if(WITH_BLUETOOTH) find_package( Qt6 diff --git a/vcpkg.json b/vcpkg.json index c9a77539ec..dba87ba99c 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -101,6 +101,7 @@ "host": true, "default-features": false }, + "qtwebdav", { "name": "qtwebsockets", "features": [ diff --git a/vcpkg/ports/qtwebdav/portfile.cmake b/vcpkg/ports/qtwebdav/portfile.cmake new file mode 100644 index 0000000000..794dda165b --- /dev/null +++ b/vcpkg/ports/qtwebdav/portfile.cmake @@ -0,0 +1,24 @@ +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO PikachuHy/QtWebDAV + REF 4739a0f09cd005b9584f740637882be41ec0f062 + SHA512 1d9eed1765178e52b6e06db952d0e30b0caf6db4b78d2d5b9492a3a56f8dd17ff58d84726fa05d756e524751ac1718a77d2215e03957afb28ca2e4e444f1b912 + HEAD_REF master + PATCHES + static.patch +) + +list(APPEND QTWEBDAV_OPTIONS -DQT_HOST_PATH=${CURRENT_HOST_INSTALLED_DIR}) +if(VCPKG_CROSSCOMPILING) + list(APPEND QTWEBDAV_OPTIONS -DQT_VERSION_MAJOR=6) + list(APPEND QTWEBDAV_OPTIONS -DQT_HOST_PATH_CMAKE_DIR:PATH=${CURRENT_HOST_INSTALLED_DIR}/share) +endif() + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + OPTIONS ${QTWEBDAV_OPTIONS} +) + +vcpkg_cmake_install() + +file(INSTALL "${SOURCE_PATH}/LICENSE" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}" RENAME copyright) diff --git a/vcpkg/ports/qtwebdav/static.patch b/vcpkg/ports/qtwebdav/static.patch new file mode 100644 index 0000000000..0f8d569e27 --- /dev/null +++ b/vcpkg/ports/qtwebdav/static.patch @@ -0,0 +1,40 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index b2b3fef..4302e3b 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -4,16 +4,16 @@ find_package(QT NAMES Qt6 Qt5 COMPONENTS Network Xml REQUIRED) + find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Network Xml REQUIRED) + set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}) + set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}) +-add_library(QtWebDAV SHARED +- QNaturalSort.cpp +- QNaturalSort.h +- QWebDAV.cpp +- QWebDAV.h +- QWebDAV_global.h +- QWebDAVDirParser.cpp +- QWebDAVDirParser.h +- QWebDAVItem.cpp +- QWebDAVItem.h ++add_library(QtWebDAV STATIC ++ qnaturalsort.cpp ++ qnaturalsort.h ++ qwebdav.cpp ++ qwebdav.h ++ qwebdav_global.h ++ qwebdavdirparser.cpp ++ qwebdavdirparser.h ++ qwebdavitem.cpp ++ qwebdavitem.h + ) + + target_link_libraries(QtWebDAV PUBLIC +@@ -28,3 +28,8 @@ option(BUILD_EXAMPLE "Build with example") + if (BUILD_EXAMPLE) + add_subdirectory(example) + endif() ++ ++install(TARGETS QtWebDAV LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) ++ ++file(GLOB QTWEBDAV_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/*.h") ++install(FILES ${QTWEBDAV_HEADERS} DESTINATION "include") diff --git a/vcpkg/ports/qtwebdav/vcpkg.json b/vcpkg/ports/qtwebdav/vcpkg.json new file mode 100644 index 0000000000..f55102ac0c --- /dev/null +++ b/vcpkg/ports/qtwebdav/vcpkg.json @@ -0,0 +1,16 @@ +{ + "name": "qtwebdav", + "version-string": "dev", + "description": " Qt library for WebDAV with support for HTTP/HTTPS.", + "homepage": "https://github.com/PikachuHy/QtWebDAV", + "dependencies": [ + { + "name": "qtbase", + "default-features": false + }, + { + "name": "vcpkg-cmake", + "host": true + } + ] +} From 9d331aa045aaf78238e34a291eeae0d6f5967de2 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Wed, 25 Dec 2024 15:16:54 +0700 Subject: [PATCH 02/43] Rely on modernized qtwebdav cmake configuration --- src/core/CMakeLists.txt | 15 +++-------- vcpkg/ports/qtwebdav/portfile.cmake | 14 +++++----- vcpkg/ports/qtwebdav/static.patch | 40 ----------------------------- 3 files changed, 9 insertions(+), 60 deletions(-) delete mode 100644 vcpkg/ports/qtwebdav/static.patch diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 15766e4eea..3e8a56cebd 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -288,6 +288,7 @@ endif() find_package(SQLite3 REQUIRED) find_package(ZXing REQUIRED) +find_package(QtWebDAV REQUIRED) add_library(qfield_core STATIC ${QFIELD_CORE_SRCS} ${QFIELD_CORE_HDRS}) @@ -367,18 +368,8 @@ target_link_libraries( GDAL::GDAL SQLite::SQLite3 Qca::qca - libzip::zip) - -find_library(QtWebDAV-LIBRARY QtWebDAV) -if(QtWebDAV-LIBRARY) - message(STATUS "Found QtWebDAV: ${QtWebDAV-LIBRARY}") - target_link_libraries(qfield_core PUBLIC ${QtWebDAV-LIBRARY}) -else() - message( - FATAL_ERROR - "Fail to find QtWebDAV library, make sure it is present in CMAKE_PREFIX_PATH" - ) -endif() + libzip::zip + QtWebDAV::QtWebDAV) if(WITH_BLUETOOTH) find_package( diff --git a/vcpkg/ports/qtwebdav/portfile.cmake b/vcpkg/ports/qtwebdav/portfile.cmake index 794dda165b..aacdeec829 100644 --- a/vcpkg/ports/qtwebdav/portfile.cmake +++ b/vcpkg/ports/qtwebdav/portfile.cmake @@ -1,16 +1,14 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH - REPO PikachuHy/QtWebDAV - REF 4739a0f09cd005b9584f740637882be41ec0f062 - SHA512 1d9eed1765178e52b6e06db952d0e30b0caf6db4b78d2d5b9492a3a56f8dd17ff58d84726fa05d756e524751ac1718a77d2215e03957afb28ca2e4e444f1b912 - HEAD_REF master - PATCHES - static.patch + REPO m-kuhn/QtWebDAV + REF b0f868c31090fe6f7ea4ca27a71f91b54d1915c3 + SHA512 3327deb2ed39c46db2f2475e5afa352f75fbd98d090a6a1d2be7756f48ea70cf69913cef2419d55f3a998e3b4f64e561d74d123213aef8142967ea54861e8ef9 + HEAD_REF main ) -list(APPEND QTWEBDAV_OPTIONS -DQT_HOST_PATH=${CURRENT_HOST_INSTALLED_DIR}) +list(APPEND QTWEBDAV_OPTIONS -DBUILD_WITH_QT6=True) if(VCPKG_CROSSCOMPILING) - list(APPEND QTWEBDAV_OPTIONS -DQT_VERSION_MAJOR=6) + list(APPEND QTWEBDAV_OPTIONS -DQT_HOST_PATH=${CURRENT_HOST_INSTALLED_DIR}) list(APPEND QTWEBDAV_OPTIONS -DQT_HOST_PATH_CMAKE_DIR:PATH=${CURRENT_HOST_INSTALLED_DIR}/share) endif() diff --git a/vcpkg/ports/qtwebdav/static.patch b/vcpkg/ports/qtwebdav/static.patch deleted file mode 100644 index 0f8d569e27..0000000000 --- a/vcpkg/ports/qtwebdav/static.patch +++ /dev/null @@ -1,40 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index b2b3fef..4302e3b 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -4,16 +4,16 @@ find_package(QT NAMES Qt6 Qt5 COMPONENTS Network Xml REQUIRED) - find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Network Xml REQUIRED) - set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}) - set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}) --add_library(QtWebDAV SHARED -- QNaturalSort.cpp -- QNaturalSort.h -- QWebDAV.cpp -- QWebDAV.h -- QWebDAV_global.h -- QWebDAVDirParser.cpp -- QWebDAVDirParser.h -- QWebDAVItem.cpp -- QWebDAVItem.h -+add_library(QtWebDAV STATIC -+ qnaturalsort.cpp -+ qnaturalsort.h -+ qwebdav.cpp -+ qwebdav.h -+ qwebdav_global.h -+ qwebdavdirparser.cpp -+ qwebdavdirparser.h -+ qwebdavitem.cpp -+ qwebdavitem.h - ) - - target_link_libraries(QtWebDAV PUBLIC -@@ -28,3 +28,8 @@ option(BUILD_EXAMPLE "Build with example") - if (BUILD_EXAMPLE) - add_subdirectory(example) - endif() -+ -+install(TARGETS QtWebDAV LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) -+ -+file(GLOB QTWEBDAV_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/*.h") -+install(FILES ${QTWEBDAV_HEADERS} DESTINATION "include") From dc0c5ec76d9e3029da27dada08aa9d8cc89d3a73 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Fri, 3 Jan 2025 14:59:35 +0700 Subject: [PATCH 03/43] Make better sense of application vs app data directories on windows/linux/macos platforms --- src/core/platforms/platformutilities.cpp | 37 +++++++++++++++++------- src/qml/qgismobileapp.qml | 16 +++++----- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/core/platforms/platformutilities.cpp b/src/core/platforms/platformutilities.cpp index 800f4b0066..5d172fe706 100644 --- a/src/core/platforms/platformutilities.cpp +++ b/src/core/platforms/platformutilities.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -97,14 +98,18 @@ void PlatformUtilities::afterUpdate() const QStringList dirs = appDataDirs(); for ( const QString &dir : dirs ) { - QDir appDir( dir ); - appDir.mkpath( QStringLiteral( "proj" ) ); - appDir.mkpath( QStringLiteral( "auth" ) ); - appDir.mkpath( QStringLiteral( "fonts" ) ); - appDir.mkpath( QStringLiteral( "basemaps" ) ); - appDir.mkpath( QStringLiteral( "logs" ) ); - appDir.mkpath( QStringLiteral( "plugins" ) ); + QDir appDataDir( dir ); + appDataDir.mkpath( QStringLiteral( "proj" ) ); + appDataDir.mkpath( QStringLiteral( "auth" ) ); + appDataDir.mkpath( QStringLiteral( "fonts" ) ); + appDataDir.mkpath( QStringLiteral( "basemaps" ) ); + appDataDir.mkpath( QStringLiteral( "logs" ) ); + appDataDir.mkpath( QStringLiteral( "plugins" ) ); } + + QDir applicationDir( applicationDirectory() ); + applicationDir.mkpath( QStringLiteral( "Imported Projects" ) ); + applicationDir.mkpath( QStringLiteral( "Imported Datasets" ) ); } QString PlatformUtilities::systemSharedDataLocation() const @@ -153,7 +158,7 @@ void PlatformUtilities::loadQgsProject() const QStringList PlatformUtilities::appDataDirs() const { - return QStringList() << QStandardPaths::standardLocations( QStandardPaths::DocumentsLocation ).first() + QStringLiteral( "/QField/" ); + return QStringList() << QStandardPaths::standardLocations( QStandardPaths::DocumentsLocation ).first() + QStringLiteral( "/QField Documents/QField/" ); } QStringList PlatformUtilities::availableGrids() const @@ -208,7 +213,7 @@ bool PlatformUtilities::renameFile( const QString &oldFilePath, const QString &n QString PlatformUtilities::applicationDirectory() const { - return QStandardPaths::standardLocations( QStandardPaths::DocumentsLocation ).first() + QStringLiteral( "/QField/" ); + return QStandardPaths::standardLocations( QStandardPaths::DocumentsLocation ).first() + QStringLiteral( "/QField Documents/" ); } QStringList PlatformUtilities::additionalApplicationDirectories() const @@ -218,7 +223,19 @@ QStringList PlatformUtilities::additionalApplicationDirectories() const QStringList PlatformUtilities::rootDirectories() const { - return QStringList() << QString(); + QStringList rootDirectories; + rootDirectories << QDir::homePath(); + for ( const QStorageInfo &volume : QStorageInfo::mountedVolumes() ) + { + if ( volume.isReady() && !volume.isReadOnly() ) + { + if ( volume.fileSystemType() != QLatin1String( "tmpfs" ) && !volume.rootPath().startsWith( QLatin1String( "/boot" ) ) ) + { + rootDirectories << volume.rootPath(); + } + } + } + return rootDirectories; } void PlatformUtilities::importProjectFolder() const diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index 5726a68727..52047f3f5d 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -3947,14 +3947,14 @@ ApplicationWindow { anchors.fill: parent onOpenLocalDataPicker: { - if (platformUtilities.capabilities & PlatformUtilities.CustomLocalDataPicker) { - welcomeScreen.visible = false; - qfieldLocalDataPickerScreen.projectFolderView = false; - qfieldLocalDataPickerScreen.model.resetToRoot(); - qfieldLocalDataPickerScreen.visible = true; - } else { - __projectSource = platformUtilities.openProject(this); - } + //if (platformUtilities.capabilities & PlatformUtilities.CustomLocalDataPicker) { + welcomeScreen.visible = false; + qfieldLocalDataPickerScreen.projectFolderView = false; + qfieldLocalDataPickerScreen.model.resetToRoot(); + qfieldLocalDataPickerScreen.visible = true; + //} else { + // __projectSource = platformUtilities.openProject(this); + //} } onShowQFieldCloudScreen: { From 065a2f2e62b94aef10b46e46f9cb9ac636d9469b Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Fri, 3 Jan 2025 18:37:55 +0700 Subject: [PATCH 04/43] wip --- src/qml/QFieldLocalDataPickerScreen.qml | 74 +++++++++++++++++++++++++ src/qml/imports/Theme/QfTextField.qml | 2 +- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index b07bfeaef6..4bd0ab852e 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -591,6 +591,20 @@ Page { } } + MenuItem { + id: importWebdav + + font: Theme.defaultFont + width: parent.width + leftPadding: Theme.menuItemLeftPadding + + text: qsTr("Import WebDAV folder") + onTriggered: { + importWebdavDialog.open(); + importWebdavUrlInput.focus(); + } + } + MenuSeparator { width: parent.width } @@ -687,6 +701,66 @@ Page { } } + QfDialog { + id: importWebdavDialog + title: "Import WebDAV folder" + focus: true + y: (mainWindow.height - height - 80) / 2 + + onAboutToShow: { + importWebdavUrlInput.text = ''; + } + + Column { + width: childrenRect.width + height: childrenRect.height + spacing: 10 + + TextMetrics { + id: importWebdavUrlLabelMetrics + font: importWebdavUrlLabel.font + text: importWebdavUrlLabel.text + } + + Label { + id: importWebdavUrlLabel + width: mainWindow.width - 60 < importWebdavUrlLabelMetrics.width ? mainWindow.width - 60 : importWebdavUrlLabelMetrics.width + text: qsTr("Type the WebDAV details below to import a remote folder:") + wrapMode: Text.WordWrap + font: Theme.defaultFont + color: Theme.mainTextColor + } + + QfTextField { + id: importWebdavUrlInput + width: importWebdavUrlLabel.width + placeholderText: qsTr("WebDAV server URL") + } + + QfTextField { + id: importWebdavUserInput + width: importWebdavUrlLabel.width + placeholderText: qsTr("User") + } + + QfTextField { + id: importWebdavPasswordInput + width: importWebdavUrlLabel.width + placeholderText: qsTr("Password") + echoMode: TextInput.Password + } + + QfButton { + width: importWebdavUrlLabel.width + text: qsTr("Connect") + } + } + + onAccepted: { + iface.importUrl(importUrlInput.text); + } + } + Connections { target: iface diff --git a/src/qml/imports/Theme/QfTextField.qml b/src/qml/imports/Theme/QfTextField.qml index 7453406172..fb3748f47d 100644 --- a/src/qml/imports/Theme/QfTextField.qml +++ b/src/qml/imports/Theme/QfTextField.qml @@ -7,7 +7,7 @@ TextField { font: Theme.defaultFont placeholderTextColor: Theme.accentLightColor rightPadding: showPasswordButton.visible ? showPasswordButton.width : 0 - leftPadding: rightPadding + leftPadding: showPasswordButton.visible && horizontalAlignment !== Text.AlignLeft ? rightPadding : 0 topPadding: 10 bottomPadding: 10 inputMethodHints: Qt.ImhNone From fabb9c1b924f1fcafebcfc8970a2556ba5eaea30 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 4 Jan 2025 12:15:51 +0700 Subject: [PATCH 05/43] Implement a webdav connection object, add paths fetching functionality --- src/core/CMakeLists.txt | 4 +- src/core/qgismobileapp.cpp | 2 + src/core/webdavconnection.cpp | 146 ++++++++++++++++++++++++ src/core/webdavconnection.h | 91 +++++++++++++++ src/qml/QFieldLocalDataPickerScreen.qml | 85 +++++++++++++- 5 files changed, 322 insertions(+), 6 deletions(-) create mode 100644 src/core/webdavconnection.cpp create mode 100644 src/core/webdavconnection.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 3e8a56cebd..5a596428e8 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -119,7 +119,8 @@ set(QFIELD_CORE_SRCS valuemapmodel.cpp valuemapmodelbase.cpp vertexmodel.cpp - viewstatus.cpp) + viewstatus.cpp + webdavconnection.cpp) set(QFIELD_CORE_HDRS platforms/platformutilities.h @@ -244,6 +245,7 @@ set(QFIELD_CORE_HDRS valuemapmodelbase.h vertexmodel.h viewstatus.h + webdavconnection.h ${CMAKE_CURRENT_BINARY_DIR}/qfield.h) list(APPEND QFIELD_CORE_SRCS permissions.cpp) diff --git a/src/core/qgismobileapp.cpp b/src/core/qgismobileapp.cpp index 70ca03bb77..cb277f427b 100644 --- a/src/core/qgismobileapp.cpp +++ b/src/core/qgismobileapp.cpp @@ -130,6 +130,7 @@ #include "urlutils.h" #include "valuemapmodel.h" #include "vertexmodel.h" +#include "webdavconnection.h" #include #include @@ -511,6 +512,7 @@ void QgisMobileapp::initDeclarative( QQmlEngine *engine ) qmlRegisterType( "org.qfield", 1, 0, "Positioning" ); qmlRegisterType( "org.qfield", 1, 0, "PositioningInformationModel" ); qmlRegisterType( "org.qfield", 1, 0, "PositioningDeviceModel" ); + qmlRegisterType( "org.qfield", 1, 0, "WebdavConnection" ); qmlRegisterType( "org.qfield", 1, 0, "AudioRecorder" ); qmlRegisterType( "org.qfield", 1, 0, "BarcodeDecoder" ); qmlRegisterType( "org.qfield", 1, 0, "QfCameraPermission" ); diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp new file mode 100644 index 0000000000..bd769aae9d --- /dev/null +++ b/src/core/webdavconnection.cpp @@ -0,0 +1,146 @@ +/*************************************************************************** + webdavconnection.cpp + ------------------- + begin : January 2025 + copyright : (C) 2025 by Mathieu Pellerin + email : mathieu@opengis.ch +***************************************************************************/ + +/*************************************************************************** + * * + * 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 "webdavconnection.h" + +#include +#include +#include + +WebdavConnection::WebdavConnection( QObject *parent ) + : QObject( parent ) +{ + connect( &mWebdavConnection, &QWebdav::errorChanged, this, [=]( const QString &error ) { + qDebug() << "connection error:" << error; + } ); + connect( &mWebdavDirParser, &QWebdavDirParser::errorChanged, this, [=]( const QString &error ) { + qDebug() << "parser error:" << error; + } ); + connect( &mWebdavDirParser, &QWebdavDirParser::finished, this, &WebdavConnection::processDirParserFinished ); +} + +void WebdavConnection::setUrl( const QString &url ) +{ + if ( mUrl == url.trimmed() ) + return; + + mUrl = url.trimmed(); + emit urlChanged(); + + if ( !mAvailablePaths.isEmpty() ) + { + mAvailablePaths.clear(); + emit availablePathsChanged(); + } + + checkStoredPassword(); +} + +void WebdavConnection::setUsername( const QString &username ) +{ + if ( mUrl == username ) + return; + + mUsername = username; + emit usernameChanged(); + + if ( !mAvailablePaths.isEmpty() ) + { + mAvailablePaths.clear(); + emit availablePathsChanged(); + } + + checkStoredPassword(); +} + +void WebdavConnection::setPassword( const QString &password ) +{ + if ( mUrl == password ) + return; + + mPassword = password; + emit passwordChanged(); +} + +void WebdavConnection::checkStoredPassword() +{ + mStoredPassword.clear(); + + if ( !mUrl.isEmpty() && !mUsername.isEmpty() ) + { + QgsAuthManager *authManager = QgsApplication::instance()->authManager(); + const QgsAuthMethodConfigsMap configs = authManager->availableAuthMethodConfigs(); + for ( const QgsAuthMethodConfig &config : configs ) + { + if ( config.uri() == mUrl && config.config( QStringLiteral( "username" ) ) == mUsername ) + { + mStoredPassword = config.config( QStringLiteral( "password" ) ); + } + } + } + + emit isPasswordStoredChanged(); +} + +void WebdavConnection::setupConnection() +{ + QUrl connectionUrl( mUrl ); + bool isHttps = connectionUrl.scheme() == QStringLiteral( "https" ); + mWebdavConnection.setConnectionSettings( isHttps ? QWebdav::HTTPS : QWebdav::HTTP, connectionUrl.host(), connectionUrl.path( QUrl::FullyEncoded ), mUsername, !mPassword.isEmpty() ? mPassword : mStoredPassword ); +} + +void WebdavConnection::fetchAvailablePaths() +{ + if ( mUrl.isEmpty() || mUsername.isEmpty() || ( mPassword.isEmpty() && mStoredPassword.isEmpty() ) ) + return; + + mAvailablePaths.clear(); + emit availablePathsChanged(); + + setupConnection(); + + mFetchPendingPaths << QStringLiteral( "/" ); + emit isFetchingAvailablePathsChanged(); + + mWebdavDirParser.listDirectory( &mWebdavConnection, mFetchPendingPaths.first() ); +} + +void WebdavConnection::processDirParserFinished() +{ + const QList list = mWebdavDirParser.getList(); + for ( const QWebdavItem &item : list ) + { + if ( item.isDir() ) + { + mFetchPendingPaths << item.path(); + } + } + + mAvailablePaths << mFetchPendingPaths.takeFirst(); + if ( !mFetchPendingPaths.isEmpty() ) + { + mWebdavDirParser.listDirectory( &mWebdavConnection, mFetchPendingPaths.first() ); + } + else + { + emit isFetchingAvailablePathsChanged(); + + mAvailablePaths.sort(); + emit availablePathsChanged(); + } +} diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h new file mode 100644 index 0000000000..0d931b4e5e --- /dev/null +++ b/src/core/webdavconnection.h @@ -0,0 +1,91 @@ +/*************************************************************************** + webdavconnection.h + ------------------- + begin : January 2025 + copyright : (C) 2025 by Mathieu Pellerin + email : mathieu@opengis.ch +***************************************************************************/ + +/*************************************************************************** + * * + * 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 WEBDAVCONNECTION_H +#define WEBDAVCONNECTION_H + +#include +#include +#include + +/** + * The webdav connection objects allows for connection to and push/pull + * operations of content. + * \ingroup core + */ +class WebdavConnection : public QObject +{ + Q_OBJECT + + Q_PROPERTY( QString url READ url WRITE setUrl NOTIFY urlChanged ); + Q_PROPERTY( QString username READ username WRITE setUsername NOTIFY usernameChanged ) + Q_PROPERTY( QString password READ password WRITE setPassword NOTIFY passwordChanged ) + Q_PROPERTY( bool isPasswordStored READ isPasswordStored NOTIFY isPasswordStoredChanged ) + + Q_PROPERTY( QStringList availablePaths READ availablePaths NOTIFY availablePathsChanged ); + Q_PROPERTY( bool isFetchingAvailablePaths READ isFetchingAvailablePaths NOTIFY isFetchingAvailablePathsChanged ) + + public: + explicit WebdavConnection( QObject *parent = nullptr ); + ~WebdavConnection() = default; + + QString url() const { return mUrl; } + + void setUrl( const QString &url ); + + QString username() const { return mUsername; } + + void setUsername( const QString &username ); + + QString password() const { return mPassword; } + + void setPassword( const QString &password ); + + bool isPasswordStored() const { return !mStoredPassword.isEmpty(); } + + QStringList availablePaths() const { return isFetchingAvailablePaths() ? QStringList() : mAvailablePaths; } + + bool isFetchingAvailablePaths() const { return !mFetchPendingPaths.isEmpty(); } + + Q_INVOKABLE void fetchAvailablePaths(); + + signals: + void urlChanged(); + void usernameChanged(); + void passwordChanged(); + void isPasswordStoredChanged(); + void availablePathsChanged(); + void isFetchingAvailablePathsChanged(); + + private: + void checkStoredPassword(); + void setupConnection(); + void processDirParserFinished(); + + QString mUrl; + QString mUsername; + QString mPassword; + QString mStoredPassword; + + QStringList mAvailablePaths; + QStringList mFetchPendingPaths; + + QWebdav mWebdavConnection; + QWebdavDirParser mWebdavDirParser; +}; + +#endif // WEBDAVCONNECTION_H diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index 4bd0ab852e..023b2cdd61 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -601,7 +601,7 @@ Page { text: qsTr("Import WebDAV folder") onTriggered: { importWebdavDialog.open(); - importWebdavUrlInput.focus(); + importWebdavUrlInput.focus = true; } } @@ -701,6 +701,23 @@ Page { } } + Component { + id: webdavConnectionComponent + + WebdavConnection { + id: webdavConnection + + url: importWebdavUrlInput.text + password: importWebdavPasswordInput.text + } + } + + Loader { + id: webdavConnectionLoader + active: importWebdavDialog.opened + sourceComponent: webdavConnectionComponent + } + QfDialog { id: importWebdavDialog title: "Import WebDAV folder" @@ -733,31 +750,89 @@ Page { QfTextField { id: importWebdavUrlInput + enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width placeholderText: qsTr("WebDAV server URL") + + onDisplayTextChanged: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.url = displayText; + } + } } QfTextField { id: importWebdavUserInput + enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width placeholderText: qsTr("User") + + onDisplayTextChanged: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.username = displayText; + } + } } QfTextField { id: importWebdavPasswordInput + enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width placeholderText: qsTr("Password") echoMode: TextInput.Password + + onDisplayTextChanged: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.password = text; + } + } } - QfButton { + Row { + visible: !webdavConnectionLoader.item || webdavConnectionLoader.item.availablePaths.length === 0 + spacing: 5 + + QfButton { + id: importWebdavFetchFoldersButton + enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths + width: importWebdavUrlLabel.width - (importWebdavFetchFoldersIndicator.visible ? importWebdavFetchFoldersIndicator.width + 5 : 0) + text: !enabled ? qsTr("Fetching remote folders") : qsTr("Fetch remote folders") + + onClicked: { + webdavConnectionLoader.item.fetchAvailablePaths(); + } + } + + BusyIndicator { + id: importWebdavFetchFoldersIndicator + anchors.verticalCenter: importWebdavFetchFoldersButton.verticalCenter + width: 48 + height: 48 + visible: webdavConnectionLoader.item && webdavConnectionLoader.item.isFetchingAvailablePaths + running: visible + } + } + + Label { width: importWebdavUrlLabel.width - text: qsTr("Connect") + visible: importWebdavPathInput.visible + text: qsTr("Select the remote folder to import:") + wrapMode: Text.WordWrap + font: Theme.defaultFont + color: Theme.mainTextColor + } + + ComboBox { + id: importWebdavPathInput + width: importWebdavUrlLabel.width + visible: webdavConnectionLoader.item && webdavConnectionLoader.item.availablePaths.length > 0 + model: [''].concat(webdavConnectionLoader.item ? webdavConnectionLoader.item.availablePaths : []) } } - onAccepted: { - iface.importUrl(importUrlInput.text); + onAccepted: + // TODO + { } } From 6f8d0fb1152a005c7bae4743ad53efb465abc0f4 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 4 Jan 2025 14:16:18 +0700 Subject: [PATCH 06/43] Implement import webdav folder functionality --- src/core/platforms/platformutilities.h | 2 +- src/core/webdavconnection.cpp | 100 +++++++++++++++++++++--- src/core/webdavconnection.h | 19 ++++- src/qml/QFieldLocalDataPickerScreen.qml | 12 +-- vcpkg/ports/qtwebdav/portfile.cmake | 1 + 5 files changed, 113 insertions(+), 21 deletions(-) diff --git a/src/core/platforms/platformutilities.h b/src/core/platforms/platformutilities.h index c14cff2a83..f52b65fe91 100644 --- a/src/core/platforms/platformutilities.h +++ b/src/core/platforms/platformutilities.h @@ -122,7 +122,7 @@ class QFIELD_CORE_EXPORT PlatformUtilities : public QObject /** * The main application directory within which projects and datasets can be imported. */ - virtual QString applicationDirectory() const; + Q_INVOKABLE virtual QString applicationDirectory() const; /** * Secondary application directories which can be used by individual platforms. diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index bd769aae9d..a4b338f585 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -18,6 +18,7 @@ #include "webdavconnection.h" +#include #include #include #include @@ -114,33 +115,108 @@ void WebdavConnection::fetchAvailablePaths() setupConnection(); - mFetchPendingPaths << QStringLiteral( "/" ); + mIsFetchingAvailablePaths = true; emit isFetchingAvailablePathsChanged(); - mWebdavDirParser.listDirectory( &mWebdavConnection, mFetchPendingPaths.first() ); + mWebdavDirParser.listDirectory( &mWebdavConnection, QStringLiteral( "/" ), true ); } void WebdavConnection::processDirParserFinished() { const QList list = mWebdavDirParser.getList(); - for ( const QWebdavItem &item : list ) + if ( mIsFetchingAvailablePaths ) { - if ( item.isDir() ) + mAvailablePaths << QStringLiteral( "/" ); + for ( const QWebdavItem &item : list ) { - mFetchPendingPaths << item.path(); + if ( item.isDir() ) + { + mAvailablePaths << item.path(); + } + } + + mIsFetchingAvailablePaths = false; + emit isFetchingAvailablePathsChanged(); + + mAvailablePaths.sort(); + emit availablePathsChanged(); + } + else if ( mIsImportingPath ) + { + QDir importLocalDir( mImportLocalPath ); + for ( const QWebdavItem &item : list ) + { + if ( item.isDir() ) + { + importLocalDir.mkpath( item.path().mid( mImportRemotePath.size() ) ); + } + else + { + mImportItems << item.path(); + } } + + processImportItems(); } +} - mAvailablePaths << mFetchPendingPaths.takeFirst(); - if ( !mFetchPendingPaths.isEmpty() ) +void WebdavConnection::processImportItems() +{ + if ( !mImportItems.isEmpty() ) { - mWebdavDirParser.listDirectory( &mWebdavConnection, mFetchPendingPaths.first() ); + const QString itemPath = mImportItems.first(); + QNetworkReply *reply = mWebdavConnection.get( itemPath ); + QTemporaryFile *temporaryFile = new QTemporaryFile( reply ); + temporaryFile->setFileTemplate( QStringLiteral( "%1%2.XXXXXXXXXXXX" ).arg( mImportLocalPath, itemPath.mid( mImportRemotePath.size() ) ) ); + temporaryFile->open(); + connect( reply, &QNetworkReply::downloadProgress, this, [=]( int bytesReceived, int bytesTotal ) { + temporaryFile->write( reply->readAll() ); + } ); + connect( reply, &QNetworkReply::finished, this, [=]() { + if ( reply->error() == QNetworkReply::NoError ) + { + temporaryFile->write( reply->readAll() ); + temporaryFile->setAutoRemove( false ); + temporaryFile->close(); + temporaryFile->rename( mImportLocalPath + itemPath.mid( mImportRemotePath.size() ) ); + } + else + { + //TODO + qDebug() << reply->error() << reply->errorString(); + } + + mImportItems.removeFirst(); + processImportItems(); + reply->deleteLater(); + } ); } else { - emit isFetchingAvailablePathsChanged(); - - mAvailablePaths.sort(); - emit availablePathsChanged(); + mIsImportingPath = false; + emit isImportingPathChanged(); } } + +void WebdavConnection::importPath( const QString &remotePath, const QString &localPath ) +{ + if ( mUrl.isEmpty() || mUsername.isEmpty() || ( mPassword.isEmpty() && mStoredPassword.isEmpty() ) ) + return; + + setupConnection(); + + QString localFolder = QStringLiteral( "%1 - %2 - %3" ).arg( mWebdavConnection.hostname(), mWebdavConnection.username(), remotePath ); + localFolder.replace( QRegularExpression( "[\\\\\\/\\<\\>\\:\\|\\?\\*\\\"]" ), QString( "_" ) ); + + QDir localDir( localPath ); + localDir.mkpath( localFolder ); + + mImportRemotePath = remotePath; + mImportLocalPath = QDir::cleanPath( localPath + QDir::separator() + localFolder ) + QDir::separator(); + + mImportItems.clear(); + mIsImportingPath = true; + emit isImportingPathChanged(); + + mWebdavDirParser.listDirectory( &mWebdavConnection, mImportRemotePath, true ); +} diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h index 0d931b4e5e..1df16aa3fe 100644 --- a/src/core/webdavconnection.h +++ b/src/core/webdavconnection.h @@ -37,7 +37,9 @@ class WebdavConnection : public QObject Q_PROPERTY( bool isPasswordStored READ isPasswordStored NOTIFY isPasswordStoredChanged ) Q_PROPERTY( QStringList availablePaths READ availablePaths NOTIFY availablePathsChanged ); + Q_PROPERTY( bool isFetchingAvailablePaths READ isFetchingAvailablePaths NOTIFY isFetchingAvailablePathsChanged ) + Q_PROPERTY( bool isImportingPath READ isImportingPath NOTIFY isImportingPathChanged ) public: explicit WebdavConnection( QObject *parent = nullptr ); @@ -57,12 +59,16 @@ class WebdavConnection : public QObject bool isPasswordStored() const { return !mStoredPassword.isEmpty(); } - QStringList availablePaths() const { return isFetchingAvailablePaths() ? QStringList() : mAvailablePaths; } + QStringList availablePaths() const { return mIsFetchingAvailablePaths ? QStringList() : mAvailablePaths; } + + bool isFetchingAvailablePaths() const { return mIsFetchingAvailablePaths; } - bool isFetchingAvailablePaths() const { return !mFetchPendingPaths.isEmpty(); } + bool isImportingPath() const { return mIsImportingPath; } Q_INVOKABLE void fetchAvailablePaths(); + Q_INVOKABLE void importPath( const QString &remotePath, const QString &localPath ); + signals: void urlChanged(); void usernameChanged(); @@ -70,19 +76,26 @@ class WebdavConnection : public QObject void isPasswordStoredChanged(); void availablePathsChanged(); void isFetchingAvailablePathsChanged(); + void isImportingPathChanged(); private: void checkStoredPassword(); void setupConnection(); void processDirParserFinished(); + void processImportItems(); QString mUrl; QString mUsername; QString mPassword; QString mStoredPassword; + bool mIsFetchingAvailablePaths = false; QStringList mAvailablePaths; - QStringList mFetchPendingPaths; + + bool mIsImportingPath = false; + QStringList mImportItems; + QString mImportRemotePath; + QString mImportLocalPath; QWebdav mWebdavConnection; QWebdavDirParser mWebdavDirParser; diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index 023b2cdd61..0c307d6dc7 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -714,7 +714,7 @@ Page { Loader { id: webdavConnectionLoader - active: importWebdavDialog.opened + active: importWebdavDialog.openedOnce sourceComponent: webdavConnectionComponent } @@ -724,8 +724,9 @@ Page { focus: true y: (mainWindow.height - height - 80) / 2 + property bool openedOnce: false onAboutToShow: { - importWebdavUrlInput.text = ''; + openedOnce = true; } Column { @@ -830,9 +831,10 @@ Page { } } - onAccepted: - // TODO - { + onAccepted: { + if (importWebdavPathInput.displayText !== '') { + webdavConnectionLoader.item.importPath(importWebdavPathInput.displayText, platformUtilities.applicationDirectory() + "Imported Projects/"); + } } } diff --git a/vcpkg/ports/qtwebdav/portfile.cmake b/vcpkg/ports/qtwebdav/portfile.cmake index aacdeec829..d898299746 100644 --- a/vcpkg/ports/qtwebdav/portfile.cmake +++ b/vcpkg/ports/qtwebdav/portfile.cmake @@ -4,6 +4,7 @@ vcpkg_from_github( REF b0f868c31090fe6f7ea4ca27a71f91b54d1915c3 SHA512 3327deb2ed39c46db2f2475e5afa352f75fbd98d090a6a1d2be7756f48ea70cf69913cef2419d55f3a998e3b4f64e561d74d123213aef8142967ea54861e8ef9 HEAD_REF main + PATCHES recursive.patch ) list(APPEND QTWEBDAV_OPTIONS -DBUILD_WITH_QT6=True) From c389566302835d48b2a422bab5c31b80024bcd8b Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 4 Jan 2025 14:37:13 +0700 Subject: [PATCH 07/43] Implement blocking progress overlay when importing webdav folder --- src/core/webdavconnection.cpp | 22 ++++++++++++++++++++++ src/core/webdavconnection.h | 10 +++++++++- src/qml/QFieldLocalDataPickerScreen.qml | 16 ++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index a4b338f585..03b2244f5a 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -153,8 +153,10 @@ void WebdavConnection::processDirParserFinished() else { mImportItems << item.path(); + mImportingBytesTotal += item.size(); } } + emit progressChanged(); processImportItems(); } @@ -170,9 +172,14 @@ void WebdavConnection::processImportItems() temporaryFile->setFileTemplate( QStringLiteral( "%1%2.XXXXXXXXXXXX" ).arg( mImportLocalPath, itemPath.mid( mImportRemotePath.size() ) ) ); temporaryFile->open(); connect( reply, &QNetworkReply::downloadProgress, this, [=]( int bytesReceived, int bytesTotal ) { + mImportingCurrentBytesReceived = bytesReceived; + emit progressChanged(); + temporaryFile->write( reply->readAll() ); } ); connect( reply, &QNetworkReply::finished, this, [=]() { + mImportingBytesReceived += mImportingCurrentBytesReceived; + mImportingCurrentBytesReceived = 0; if ( reply->error() == QNetworkReply::NoError ) { temporaryFile->write( reply->readAll() ); @@ -215,8 +222,23 @@ void WebdavConnection::importPath( const QString &remotePath, const QString &loc mImportLocalPath = QDir::cleanPath( localPath + QDir::separator() + localFolder ) + QDir::separator(); mImportItems.clear(); + + mImportingBytesReceived = 0; + mImportingBytesTotal = 0; + emit progressChanged(); + mIsImportingPath = true; emit isImportingPathChanged(); mWebdavDirParser.listDirectory( &mWebdavConnection, mImportRemotePath, true ); } + +double WebdavConnection::progress() const +{ + if ( mIsImportingPath && mImportingBytesTotal > 0 ) + { + return static_cast( mImportingBytesReceived + mImportingCurrentBytesReceived ) / mImportingBytesTotal; + } + + return 0; +} diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h index 1df16aa3fe..b40b79dcb9 100644 --- a/src/core/webdavconnection.h +++ b/src/core/webdavconnection.h @@ -36,11 +36,13 @@ class WebdavConnection : public QObject Q_PROPERTY( QString password READ password WRITE setPassword NOTIFY passwordChanged ) Q_PROPERTY( bool isPasswordStored READ isPasswordStored NOTIFY isPasswordStoredChanged ) - Q_PROPERTY( QStringList availablePaths READ availablePaths NOTIFY availablePathsChanged ); + Q_PROPERTY( QStringList availablePaths READ availablePaths NOTIFY availablePathsChanged ) Q_PROPERTY( bool isFetchingAvailablePaths READ isFetchingAvailablePaths NOTIFY isFetchingAvailablePathsChanged ) Q_PROPERTY( bool isImportingPath READ isImportingPath NOTIFY isImportingPathChanged ) + Q_PROPERTY( double progress READ progress NOTIFY progressChanged ) + public: explicit WebdavConnection( QObject *parent = nullptr ); ~WebdavConnection() = default; @@ -65,6 +67,8 @@ class WebdavConnection : public QObject bool isImportingPath() const { return mIsImportingPath; } + double progress() const; + Q_INVOKABLE void fetchAvailablePaths(); Q_INVOKABLE void importPath( const QString &remotePath, const QString &localPath ); @@ -77,6 +81,7 @@ class WebdavConnection : public QObject void availablePathsChanged(); void isFetchingAvailablePathsChanged(); void isImportingPathChanged(); + void progressChanged(); private: void checkStoredPassword(); @@ -93,6 +98,9 @@ class WebdavConnection : public QObject QStringList mAvailablePaths; bool mIsImportingPath = false; + qint64 mImportingCurrentBytesReceived = 0; + qint64 mImportingBytesReceived = 0; + qint64 mImportingBytesTotal = 0; QStringList mImportItems; QString mImportRemotePath; QString mImportLocalPath; diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index 0c307d6dc7..f43a299fc3 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -709,6 +709,22 @@ Page { url: importWebdavUrlInput.text password: importWebdavPasswordInput.text + + onIsImportingPathChanged: { + if (isImportingPath) { + busyOverlay.text = qsTr("Importing WebDAV folder"); + busyOverlay.progress = 0; + busyOverlay.state = "visible"; + } else { + busyOverlay.state = "hidden"; + } + } + + onProgressChanged: { + if (isImportingPath) { + busyOverlay.progress = progress; + } + } } } From 44bea2bcf690fe6ebc6f26064da7b22176fae207 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 4 Jan 2025 14:59:35 +0700 Subject: [PATCH 08/43] Basic error handling by throwing toasters --- src/core/webdavconnection.cpp | 20 ++++++++++++++------ src/core/webdavconnection.h | 12 +++++++++++- src/qml/QFieldLocalDataPickerScreen.qml | 4 ++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index 03b2244f5a..e0c0fbfe0b 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -26,12 +26,8 @@ WebdavConnection::WebdavConnection( QObject *parent ) : QObject( parent ) { - connect( &mWebdavConnection, &QWebdav::errorChanged, this, [=]( const QString &error ) { - qDebug() << "connection error:" << error; - } ); - connect( &mWebdavDirParser, &QWebdavDirParser::errorChanged, this, [=]( const QString &error ) { - qDebug() << "parser error:" << error; - } ); + connect( &mWebdavConnection, &QWebdav::errorChanged, this, &WebdavConnection::processConnectionError ); + connect( &mWebdavDirParser, &QWebdavDirParser::errorChanged, this, &WebdavConnection::processDirParserError ); connect( &mWebdavDirParser, &QWebdavDirParser::finished, this, &WebdavConnection::processDirParserFinished ); } @@ -242,3 +238,15 @@ double WebdavConnection::progress() const return 0; } + +void WebdavConnection::processConnectionError( const QString &error ) +{ + mLastError = error; + emit lastErrorChanged(); +} + +void WebdavConnection::processDirParserError( const QString &error ) +{ + mLastError = error; + emit lastErrorChanged(); +} diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h index b40b79dcb9..64a2fcdbac 100644 --- a/src/core/webdavconnection.h +++ b/src/core/webdavconnection.h @@ -43,6 +43,8 @@ class WebdavConnection : public QObject Q_PROPERTY( double progress READ progress NOTIFY progressChanged ) + Q_PROPERTY( QString lastError READ lastError NOTIFY lastErrorChanged ) + public: explicit WebdavConnection( QObject *parent = nullptr ); ~WebdavConnection() = default; @@ -69,6 +71,8 @@ class WebdavConnection : public QObject double progress() const; + QString lastError() const { return mLastError; } + Q_INVOKABLE void fetchAvailablePaths(); Q_INVOKABLE void importPath( const QString &remotePath, const QString &localPath ); @@ -82,11 +86,16 @@ class WebdavConnection : public QObject void isFetchingAvailablePathsChanged(); void isImportingPathChanged(); void progressChanged(); + void lastErrorChanged(); + + private slots: + void processDirParserFinished(); + void processConnectionError( const QString &error ); + void processDirParserError( const QString &error ); private: void checkStoredPassword(); void setupConnection(); - void processDirParserFinished(); void processImportItems(); QString mUrl; @@ -107,6 +116,7 @@ class WebdavConnection : public QObject QWebdav mWebdavConnection; QWebdavDirParser mWebdavDirParser; + QString mLastError; }; #endif // WEBDAVCONNECTION_H diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index f43a299fc3..4ce161412d 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -725,6 +725,10 @@ Page { busyOverlay.progress = progress; } } + + onLastErrorChanged: { + displayToast(qsTr("WebDAV error: ") + lastError); + } } } From 0ce41545a3e088a5aa95a7815ce3c41240e1035e Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 4 Jan 2025 15:28:00 +0700 Subject: [PATCH 09/43] More error handling, avoid showing root directory when wrong user/password/url --- src/core/webdavconnection.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index e0c0fbfe0b..9f30528160 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -122,12 +122,15 @@ void WebdavConnection::processDirParserFinished() const QList list = mWebdavDirParser.getList(); if ( mIsFetchingAvailablePaths ) { - mAvailablePaths << QStringLiteral( "/" ); - for ( const QWebdavItem &item : list ) + if ( !list.isEmpty() ) { - if ( item.isDir() ) + mAvailablePaths << QStringLiteral( "/" ); + for ( const QWebdavItem &item : list ) { - mAvailablePaths << item.path(); + if ( item.isDir() ) + { + mAvailablePaths << item.path(); + } } } @@ -185,8 +188,7 @@ void WebdavConnection::processImportItems() } else { - //TODO - qDebug() << reply->error() << reply->errorString(); + mLastError = tr( "Failed to download file %1 due to network error (%1)" ).arg( reply->error() ); } mImportItems.removeFirst(); From affbefe5a6a19fd75fc03ded04d89d79bf62f9a7 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 4 Jan 2025 15:33:41 +0700 Subject: [PATCH 10/43] Clear access authentication cache when changing user/password --- src/core/webdavconnection.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index 9f30528160..3c23868861 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -62,6 +62,7 @@ void WebdavConnection::setUsername( const QString &username ) emit availablePathsChanged(); } + mWebdavConnection.clearAccessCache(); checkStoredPassword(); } @@ -72,6 +73,14 @@ void WebdavConnection::setPassword( const QString &password ) mPassword = password; emit passwordChanged(); + + if ( !mAvailablePaths.isEmpty() ) + { + mAvailablePaths.clear(); + emit availablePathsChanged(); + } + + mWebdavConnection.clearAccessCache(); } void WebdavConnection::checkStoredPassword() From 361eb999b8064bd6592d873ede27fde8c8487b12 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 4 Jan 2025 16:33:05 +0700 Subject: [PATCH 11/43] Implement mechanism to remember password across sessions --- src/core/webdavconnection.cpp | 113 +++++++++++++++++++++--- src/core/webdavconnection.h | 16 +++- src/qml/QFieldLocalDataPickerScreen.qml | 33 ++++--- 3 files changed, 126 insertions(+), 36 deletions(-) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index 3c23868861..8fe6ff95e1 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -83,6 +83,15 @@ void WebdavConnection::setPassword( const QString &password ) mWebdavConnection.clearAccessCache(); } +void WebdavConnection::setStorePassword( bool storePassword ) +{ + if ( mStorePassword == storePassword ) + return; + + mStorePassword = storePassword; + emit storePasswordChanged(); +} + void WebdavConnection::checkStoredPassword() { mStoredPassword.clear(); @@ -90,12 +99,18 @@ void WebdavConnection::checkStoredPassword() if ( !mUrl.isEmpty() && !mUsername.isEmpty() ) { QgsAuthManager *authManager = QgsApplication::instance()->authManager(); - const QgsAuthMethodConfigsMap configs = authManager->availableAuthMethodConfigs(); - for ( const QgsAuthMethodConfig &config : configs ) + QgsAuthMethodConfigsMap configs = authManager->availableAuthMethodConfigs(); + for ( QgsAuthMethodConfig &config : configs ) { - if ( config.uri() == mUrl && config.config( QStringLiteral( "username" ) ) == mUsername ) + qDebug() << config.name(); + qDebug() << config.uri(); + if ( config.uri() == mUrl ) { - mStoredPassword = config.config( QStringLiteral( "password" ) ); + authManager->loadAuthenticationConfig( config.id(), config, true ); + if ( config.config( QStringLiteral( "username" ) ) == mUsername ) + { + mStoredPassword = config.config( QStringLiteral( "password" ) ); + } } } } @@ -103,6 +118,69 @@ void WebdavConnection::checkStoredPassword() emit isPasswordStoredChanged(); } +void WebdavConnection::applyStoredPassword() +{ + QgsAuthManager *authManager = QgsApplication::instance()->authManager(); + QgsAuthMethodConfigsMap configs = authManager->availableAuthMethodConfigs(); + if ( mStorePassword ) + { + if ( !mPassword.isEmpty() ) + { + bool found = false; + for ( QgsAuthMethodConfig &config : configs ) + { + if ( config.uri() == mUrl ) + { + authManager->loadAuthenticationConfig( config.id(), config, true ); + if ( config.config( QStringLiteral( "username" ) ) == mUsername ) + { + if ( config.config( QStringLiteral( "password" ) ) != mPassword ) + { + config.setConfig( "password", mPassword ); + authManager->updateAuthenticationConfig( config ); + + mStoredPassword = mPassword; + emit isPasswordStoredChanged(); + } + + found = true; + break; + } + } + } + + if ( !found ) + { + QgsAuthMethodConfig config( QStringLiteral( "Basic" ) ); + config.setName( QStringLiteral( "WebDAV created on %1" ).arg( QDateTime::currentDateTime().toString() ) ); + config.setUri( mUrl ); + config.setConfig( "username", mUsername ); + config.setConfig( "password", mPassword ); + authManager->storeAuthenticationConfig( config ); + + mStoredPassword = mPassword; + emit isPasswordStoredChanged(); + } + } + } + else + { + for ( const QgsAuthMethodConfig &config : configs ) + { + if ( config.uri() == mUrl && config.config( QStringLiteral( "username" ) ) == mUsername ) + { + authManager->removeAuthenticationConfig( config.id() ); + } + } + + if ( !mStoredPassword.isEmpty() ) + { + mStoredPassword = mPassword; + emit isPasswordStoredChanged(); + } + } +} + void WebdavConnection::setupConnection() { QUrl connectionUrl( mUrl ); @@ -133,6 +211,8 @@ void WebdavConnection::processDirParserFinished() { if ( !list.isEmpty() ) { + applyStoredPassword(); + mAvailablePaths << QStringLiteral( "/" ); for ( const QWebdavItem &item : list ) { @@ -151,20 +231,25 @@ void WebdavConnection::processDirParserFinished() } else if ( mIsImportingPath ) { - QDir importLocalDir( mImportLocalPath ); - for ( const QWebdavItem &item : list ) + if ( !list.isEmpty() ) { - if ( item.isDir() ) - { - importLocalDir.mkpath( item.path().mid( mImportRemotePath.size() ) ); - } - else + applyStoredPassword(); + + QDir importLocalDir( mImportLocalPath ); + for ( const QWebdavItem &item : list ) { - mImportItems << item.path(); - mImportingBytesTotal += item.size(); + if ( item.isDir() ) + { + importLocalDir.mkpath( item.path().mid( mImportRemotePath.size() ) ); + } + else + { + mImportItems << item.path(); + mImportingBytesTotal += item.size(); + } } + emit progressChanged(); } - emit progressChanged(); processImportItems(); } diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h index 64a2fcdbac..bd26220e5f 100644 --- a/src/core/webdavconnection.h +++ b/src/core/webdavconnection.h @@ -34,15 +34,15 @@ class WebdavConnection : public QObject Q_PROPERTY( QString url READ url WRITE setUrl NOTIFY urlChanged ); Q_PROPERTY( QString username READ username WRITE setUsername NOTIFY usernameChanged ) Q_PROPERTY( QString password READ password WRITE setPassword NOTIFY passwordChanged ) - Q_PROPERTY( bool isPasswordStored READ isPasswordStored NOTIFY isPasswordStoredChanged ) - Q_PROPERTY( QStringList availablePaths READ availablePaths NOTIFY availablePathsChanged ) + Q_PROPERTY( bool storePassword READ storePassword WRITE setStorePassword NOTIFY storePasswordChanged ) + Q_PROPERTY( bool isPasswordStored READ isPasswordStored NOTIFY isPasswordStoredChanged ) Q_PROPERTY( bool isFetchingAvailablePaths READ isFetchingAvailablePaths NOTIFY isFetchingAvailablePathsChanged ) Q_PROPERTY( bool isImportingPath READ isImportingPath NOTIFY isImportingPathChanged ) + Q_PROPERTY( QStringList availablePaths READ availablePaths NOTIFY availablePathsChanged ) Q_PROPERTY( double progress READ progress NOTIFY progressChanged ) - Q_PROPERTY( QString lastError READ lastError NOTIFY lastErrorChanged ) public: @@ -61,6 +61,10 @@ class WebdavConnection : public QObject void setPassword( const QString &password ); + bool storePassword() const { return mStorePassword; } + + void setStorePassword( bool storePassword ); + bool isPasswordStored() const { return !mStoredPassword.isEmpty(); } QStringList availablePaths() const { return mIsFetchingAvailablePaths ? QStringList() : mAvailablePaths; } @@ -81,10 +85,11 @@ class WebdavConnection : public QObject void urlChanged(); void usernameChanged(); void passwordChanged(); + void storePasswordChanged(); void isPasswordStoredChanged(); - void availablePathsChanged(); void isFetchingAvailablePathsChanged(); void isImportingPathChanged(); + void availablePathsChanged(); void progressChanged(); void lastErrorChanged(); @@ -95,12 +100,15 @@ class WebdavConnection : public QObject private: void checkStoredPassword(); + void applyStoredPassword(); void setupConnection(); void processImportItems(); QString mUrl; QString mUsername; QString mPassword; + + bool mStorePassword = false; QString mStoredPassword; bool mIsFetchingAvailablePaths = false; diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index 4ce161412d..d1e94ba98d 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -708,7 +708,9 @@ Page { id: webdavConnection url: importWebdavUrlInput.text + username: importWebdavUserInput.text password: importWebdavPasswordInput.text + storePassword: importWebdavStorePasswordCheck.checked onIsImportingPathChanged: { if (isImportingPath) { @@ -729,6 +731,10 @@ Page { onLastErrorChanged: { displayToast(qsTr("WebDAV error: ") + lastError); } + + onIsPasswordStoredChanged: { + console.log(isPasswordStored ? "stored" : "not stored"); + } } } @@ -774,12 +780,6 @@ Page { enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width placeholderText: qsTr("WebDAV server URL") - - onDisplayTextChanged: { - if (webdavConnectionLoader.item) { - webdavConnectionLoader.item.url = displayText; - } - } } QfTextField { @@ -787,26 +787,23 @@ Page { enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width placeholderText: qsTr("User") - - onDisplayTextChanged: { - if (webdavConnectionLoader.item) { - webdavConnectionLoader.item.username = displayText; - } - } } QfTextField { id: importWebdavPasswordInput enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width - placeholderText: qsTr("Password") + placeholderText: text === "" && webdavConnectionLoader.item && webdavConnectionLoader.item.isPasswordStored ? qsTr("Password (leave empty to use remembered)") : qsTr("Password") echoMode: TextInput.Password + } - onDisplayTextChanged: { - if (webdavConnectionLoader.item) { - webdavConnectionLoader.item.password = text; - } - } + CheckBox { + id: importWebdavStorePasswordCheck + width: importWebdavUrlLabel.width + enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths + text: qsTr('Remember password') + font: Theme.defaultFont + checked: true } Row { From f494a332b94e557d42eb5cc1f864428a2cc1c9e5 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 4 Jan 2025 16:45:44 +0700 Subject: [PATCH 12/43] Harmonize QfTextField with generic Material input items --- src/qml/imports/Theme/QfTextField.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qml/imports/Theme/QfTextField.qml b/src/qml/imports/Theme/QfTextField.qml index fb3748f47d..0dac2a95f9 100644 --- a/src/qml/imports/Theme/QfTextField.qml +++ b/src/qml/imports/Theme/QfTextField.qml @@ -20,7 +20,7 @@ TextField { y: textField.height - height - textField.bottomPadding / 2 width: textField.width height: textField.activeFocus ? 2 : 1 - color: textField.activeFocus ? Theme.accentColor : Theme.accentLightColor + color: textField.activeFocus ? Theme.mainColor : Theme.secondaryTextColor } } From 78413a951702d391a70994b5d1aceb2f5b78f822 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 4 Jan 2025 16:46:10 +0700 Subject: [PATCH 13/43] Add missing qtwebdav patch --- vcpkg/ports/qtwebdav/recursive.patch | 54 ++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 vcpkg/ports/qtwebdav/recursive.patch diff --git a/vcpkg/ports/qtwebdav/recursive.patch b/vcpkg/ports/qtwebdav/recursive.patch new file mode 100644 index 0000000000..6da48eaf1f --- /dev/null +++ b/vcpkg/ports/qtwebdav/recursive.patch @@ -0,0 +1,54 @@ +diff --git a/qwebdav.cpp b/qwebdav.cpp +index 28478cd..bd8c646 100644 +--- a/qwebdav.cpp ++++ b/qwebdav.cpp +@@ -410,13 +410,12 @@ QNetworkReply* QWebdav::get(const QString& path) + + QUrl reqUrl(m_baseUrl); + reqUrl.setPath(absolutePath(path)); ++ req.setUrl(reqUrl); + + #ifdef DEBUG_WEBDAV + qDebug() << "QWebdav::get() url = " << req.url().toString(QUrl::RemoveUserInfo); + #endif + +- req.setUrl(reqUrl); +- + return QNetworkAccessManager::get(req); + } + +diff --git a/qwebdavdirparser.cpp b/qwebdavdirparser.cpp +index a48f3cd..15cfc99 100644 +--- a/qwebdavdirparser.cpp ++++ b/qwebdavdirparser.cpp +@@ -70,7 +70,7 @@ QWebdavDirParser::~QWebdavDirParser() + } + } + +-bool QWebdavDirParser::listDirectory(QWebdav *pWebdav, const QString &path) ++bool QWebdavDirParser::listDirectory(QWebdav *pWebdav, const QString &path, bool recursive) + { + if (m_busy) + return false; +@@ -93,7 +93,7 @@ bool QWebdavDirParser::listDirectory(QWebdav *pWebdav, const QString &path) + m_abort = false; + m_includeRequestedURI = false; + +- m_reply = pWebdav->list(path); ++ m_reply = pWebdav->list(path, recursive ? 2 : 1); + connect(m_reply, SIGNAL(finished()), this, SLOT(replyFinished())); + + if (!m_dirList.isEmpty()) +diff --git a/qwebdavdirparser.h b/qwebdavdirparser.h +index 2943e7f..cb1422b 100644 +--- a/qwebdavdirparser.h ++++ b/qwebdavdirparser.h +@@ -73,7 +73,7 @@ public: + ~QWebdavDirParser(); + + //! get all items of a collection +- bool listDirectory(QWebdav *pWebdav, const QString &path); ++ bool listDirectory(QWebdav *pWebdav, const QString &path, bool recursive = false); + //! get only information about the collection + bool getDirectoryInfo(QWebdav *pWebdav, const QString &path); + //! get only information about a file From 99b6e5f0592222e1e90af951e8a9d283475f113f Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 4 Jan 2025 16:52:25 +0700 Subject: [PATCH 14/43] Further tweak --- src/qml/imports/Theme/QfTextField.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qml/imports/Theme/QfTextField.qml b/src/qml/imports/Theme/QfTextField.qml index 0dac2a95f9..a1e3581cd2 100644 --- a/src/qml/imports/Theme/QfTextField.qml +++ b/src/qml/imports/Theme/QfTextField.qml @@ -20,7 +20,7 @@ TextField { y: textField.height - height - textField.bottomPadding / 2 width: textField.width height: textField.activeFocus ? 2 : 1 - color: textField.activeFocus ? Theme.mainColor : Theme.secondaryTextColor + color: textField.activeFocus ? Theme.mainColor : textField.enabled ? Theme.secondaryTextColor : "transparent" } } From db687bcbe1b4f1263c741542226463c0530f0a3b Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 4 Jan 2025 18:53:55 +0700 Subject: [PATCH 15/43] Remove qDebug() crumbs --- src/core/webdavconnection.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index 8fe6ff95e1..9bb6ee75c7 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -102,8 +102,6 @@ void WebdavConnection::checkStoredPassword() QgsAuthMethodConfigsMap configs = authManager->availableAuthMethodConfigs(); for ( QgsAuthMethodConfig &config : configs ) { - qDebug() << config.name(); - qDebug() << config.uri(); if ( config.uri() == mUrl ) { authManager->loadAuthenticationConfig( config.id(), config, true ); From 947c073c1f6dee0bacc0fb80e7577920f29fdc8f Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 4 Jan 2025 19:13:49 +0700 Subject: [PATCH 16/43] Use raw material text fields within dialogs, it just looks more immersive --- src/qml/MessageLog.qml | 2 +- src/qml/QFieldLocalDataPickerScreen.qml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qml/MessageLog.qml b/src/qml/MessageLog.qml index d28b2c2c36..3fc460c7b0 100644 --- a/src/qml/MessageLog.qml +++ b/src/qml/MessageLog.qml @@ -179,7 +179,7 @@ Page { color: Theme.mainTextColor } - QfTextField { + TextField { id: appliationLogInput width: applicationLogLabel.width placeholderText: qsTr("Type optional details") diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index d1e94ba98d..3c4a55d2e5 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -775,21 +775,21 @@ Page { color: Theme.mainTextColor } - QfTextField { + TextField { id: importWebdavUrlInput enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width placeholderText: qsTr("WebDAV server URL") } - QfTextField { + TextField { id: importWebdavUserInput enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width placeholderText: qsTr("User") } - QfTextField { + TextField { id: importWebdavPasswordInput enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width From fb7d4ad70fd4762dd55bbe0b093c140ce903a70d Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 4 Jan 2025 19:30:57 +0700 Subject: [PATCH 17/43] Further tiny tweaks to QfTextField --- src/qml/imports/Theme/QfTextField.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qml/imports/Theme/QfTextField.qml b/src/qml/imports/Theme/QfTextField.qml index a1e3581cd2..f5aecdbd5c 100644 --- a/src/qml/imports/Theme/QfTextField.qml +++ b/src/qml/imports/Theme/QfTextField.qml @@ -20,7 +20,7 @@ TextField { y: textField.height - height - textField.bottomPadding / 2 width: textField.width height: textField.activeFocus ? 2 : 1 - color: textField.activeFocus ? Theme.mainColor : textField.enabled ? Theme.secondaryTextColor : "transparent" + color: textField.activeFocus ? Theme.mainColor : textField.hovered ? textField.color : Theme.secondaryTextColor } } From 9cee8544dd56ba4e817639c5af8835cc714181a9 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 10:06:39 +0700 Subject: [PATCH 18/43] Update QtWebDAV to see if it fixes windows builds --- vcpkg/ports/qtwebdav/fix.patch | 12 +++++++ vcpkg/ports/qtwebdav/portfile.cmake | 6 ++-- vcpkg/ports/qtwebdav/recursive.patch | 54 ---------------------------- 3 files changed, 15 insertions(+), 57 deletions(-) create mode 100644 vcpkg/ports/qtwebdav/fix.patch delete mode 100644 vcpkg/ports/qtwebdav/recursive.patch diff --git a/vcpkg/ports/qtwebdav/fix.patch b/vcpkg/ports/qtwebdav/fix.patch new file mode 100644 index 0000000000..232f73014a --- /dev/null +++ b/vcpkg/ports/qtwebdav/fix.patch @@ -0,0 +1,12 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 6164046..36bbf15 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -28,7 +28,6 @@ set(QtWebDAV_SOURCES + set(QtWebDAV_HEADERS + qnaturalsort.h + qwebdav.h +- qwebdav_global.h + qwebdavdirparser.h + qwebdavitem.h + ) diff --git a/vcpkg/ports/qtwebdav/portfile.cmake b/vcpkg/ports/qtwebdav/portfile.cmake index d898299746..efd4f2dd8b 100644 --- a/vcpkg/ports/qtwebdav/portfile.cmake +++ b/vcpkg/ports/qtwebdav/portfile.cmake @@ -1,10 +1,10 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO m-kuhn/QtWebDAV - REF b0f868c31090fe6f7ea4ca27a71f91b54d1915c3 - SHA512 3327deb2ed39c46db2f2475e5afa352f75fbd98d090a6a1d2be7756f48ea70cf69913cef2419d55f3a998e3b4f64e561d74d123213aef8142967ea54861e8ef9 + REF 6363cabf99368bb8a659af63584ac790f04cc9c6 + SHA512 059e99181fdd751b4598986997721b9e0e7debccbad7f3a258cba2745bd4b28896611566017c50a7bb9ace523bca388551a327b17f76968032c8a6e9a6bb2d34 HEAD_REF main - PATCHES recursive.patch + PATCHES fix.patch ) list(APPEND QTWEBDAV_OPTIONS -DBUILD_WITH_QT6=True) diff --git a/vcpkg/ports/qtwebdav/recursive.patch b/vcpkg/ports/qtwebdav/recursive.patch deleted file mode 100644 index 6da48eaf1f..0000000000 --- a/vcpkg/ports/qtwebdav/recursive.patch +++ /dev/null @@ -1,54 +0,0 @@ -diff --git a/qwebdav.cpp b/qwebdav.cpp -index 28478cd..bd8c646 100644 ---- a/qwebdav.cpp -+++ b/qwebdav.cpp -@@ -410,13 +410,12 @@ QNetworkReply* QWebdav::get(const QString& path) - - QUrl reqUrl(m_baseUrl); - reqUrl.setPath(absolutePath(path)); -+ req.setUrl(reqUrl); - - #ifdef DEBUG_WEBDAV - qDebug() << "QWebdav::get() url = " << req.url().toString(QUrl::RemoveUserInfo); - #endif - -- req.setUrl(reqUrl); -- - return QNetworkAccessManager::get(req); - } - -diff --git a/qwebdavdirparser.cpp b/qwebdavdirparser.cpp -index a48f3cd..15cfc99 100644 ---- a/qwebdavdirparser.cpp -+++ b/qwebdavdirparser.cpp -@@ -70,7 +70,7 @@ QWebdavDirParser::~QWebdavDirParser() - } - } - --bool QWebdavDirParser::listDirectory(QWebdav *pWebdav, const QString &path) -+bool QWebdavDirParser::listDirectory(QWebdav *pWebdav, const QString &path, bool recursive) - { - if (m_busy) - return false; -@@ -93,7 +93,7 @@ bool QWebdavDirParser::listDirectory(QWebdav *pWebdav, const QString &path) - m_abort = false; - m_includeRequestedURI = false; - -- m_reply = pWebdav->list(path); -+ m_reply = pWebdav->list(path, recursive ? 2 : 1); - connect(m_reply, SIGNAL(finished()), this, SLOT(replyFinished())); - - if (!m_dirList.isEmpty()) -diff --git a/qwebdavdirparser.h b/qwebdavdirparser.h -index 2943e7f..cb1422b 100644 ---- a/qwebdavdirparser.h -+++ b/qwebdavdirparser.h -@@ -73,7 +73,7 @@ public: - ~QWebdavDirParser(); - - //! get all items of a collection -- bool listDirectory(QWebdav *pWebdav, const QString &path); -+ bool listDirectory(QWebdav *pWebdav, const QString &path, bool recursive = false); - //! get only information about the collection - bool getDirectoryInfo(QWebdav *pWebdav, const QString &path); - //! get only information about a file From 58851aa5a7543c4f9067128f758eba8c3f9652e0 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 12:18:19 +0700 Subject: [PATCH 19/43] Add a qfield_webdav_settings.json file when importing folder --- src/core/webdavconnection.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index 9bb6ee75c7..3e5555464c 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -290,6 +290,17 @@ void WebdavConnection::processImportItems() } else { + QVariantMap webdavConfiguration; + webdavConfiguration[QStringLiteral( "url" )] = mUrl; + webdavConfiguration[QStringLiteral( "username" )] = mUsername; + webdavConfiguration[QStringLiteral( "remote_path" )] = mImportRemotePath; + + QJsonDocument jsonDocument = QJsonDocument::fromVariant( webdavConfiguration ); + QFile jsonFile( QStringLiteral( "%1qfield_webdav_settings.json" ).arg( mImportLocalPath ) ); + jsonFile.open( QFile::WriteOnly ); + jsonFile.write( jsonDocument.toJson() ); + jsonFile.close(); + mIsImportingPath = false; emit isImportingPathChanged(); } From f5911732c10e4f27fd5a1124ad08a99cf6770240 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 12:43:29 +0700 Subject: [PATCH 20/43] Implement webdav configuration awareness into the local files model --- src/core/localfilesmodel.cpp | 13 ++++ src/core/localfilesmodel.h | 1 + src/core/webdavconnection.cpp | 2 +- src/qml/QFieldLocalDataPickerScreen.qml | 86 ++++++++++++++++++++----- 4 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/core/localfilesmodel.cpp b/src/core/localfilesmodel.cpp index cd7f57ccc4..7ccc6be80e 100644 --- a/src/core/localfilesmodel.cpp +++ b/src/core/localfilesmodel.cpp @@ -59,6 +59,7 @@ QHash LocalFilesModel::roleNames() const roles[ItemSizeRole] = "ItemSize"; roles[ItemHasThumbnailRole] = "ItemHasThumbnail"; roles[ItemIsFavoriteRole] = "ItemIsFavorite"; + roles[ItemHasWebdavConfigurationRole] = "ItemHasWebdavConfiguration"; return roles; } @@ -332,6 +333,18 @@ QVariant LocalFilesModel::data( const QModelIndex &index, int role ) const case ItemIsFavoriteRole: return mFavorites.contains( mItems[index.row()].path ); + + case ItemHasWebdavConfigurationRole: + { + const QFileInfo fileInfo( mItems[index.row()].path ); + QDir dir( fileInfo.isFile() ? fileInfo.absolutePath() : fileInfo.absoluteFilePath() ); + bool webdavConfigurationExists = dir.exists( "qfield_webdav_configuration.json" ); + while ( !webdavConfigurationExists && dir.cdUp() ) + { + webdavConfigurationExists = dir.exists( "qfield_webdav_configuration.json" ); + } + return webdavConfigurationExists; + } } return QVariant(); diff --git a/src/core/localfilesmodel.h b/src/core/localfilesmodel.h index d44fe64a32..7aab08821f 100644 --- a/src/core/localfilesmodel.h +++ b/src/core/localfilesmodel.h @@ -86,6 +86,7 @@ class LocalFilesModel : public QAbstractListModel ItemSizeRole, ItemHasThumbnailRole, ItemIsFavoriteRole, + ItemHasWebdavConfigurationRole, }; Q_ENUM( Role ) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index 3e5555464c..d03f37910d 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -296,7 +296,7 @@ void WebdavConnection::processImportItems() webdavConfiguration[QStringLiteral( "remote_path" )] = mImportRemotePath; QJsonDocument jsonDocument = QJsonDocument::fromVariant( webdavConfiguration ); - QFile jsonFile( QStringLiteral( "%1qfield_webdav_settings.json" ).arg( mImportLocalPath ) ); + QFile jsonFile( QStringLiteral( "%1qfield_webdav_configuration.json" ).arg( mImportLocalPath ) ); jsonFile.open( QFile::WriteOnly ); jsonFile.write( jsonDocument.toJson() ); jsonFile.close(); diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index 3c4a55d2e5..8a2acd35bd 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -255,6 +255,7 @@ Page { itemMenu.itemType = ItemType; itemMenu.itemPath = ItemPath; itemMenu.itemIsFavorite = ItemIsFavorite; + itemMenu.itemHasWebdavConfiguration = ItemHasWebdavConfiguration; itemMenu.popup(gc.x + width - itemMenu.width, gc.y - height); } } @@ -351,6 +352,7 @@ Page { property int itemType: 0 property string itemPath: '' property bool itemIsFavorite: false + property bool itemHasWebdavConfiguration: false title: qsTr('Item Actions') @@ -368,6 +370,7 @@ Page { topMargin: sceneTopMargin bottomMargin: sceneBottomMargin + // File items MenuItem { id: sendDatasetTo enabled: itemMenu.itemMetaType === LocalFilesModel.File || (platformUtilities.capabilities & PlatformUtilities.CustomSend && itemMenu.itemMetaType == LocalFilesModel.Dataset) @@ -418,9 +421,10 @@ Page { } } + // Folder items MenuItem { - id: removeDataset - enabled: itemMenu.itemMetaType == LocalFilesModel.Dataset && !qfieldLocalDataPickerScreen.projectFolderView && table.model.isDeletedAllowedInCurrentPath + id: toggleFavoriteState + enabled: itemMenu.itemMetaType == LocalFilesModel.Folder && localFilesModel.isPathFavoriteEditable(itemMenu.itemPath) visible: enabled font: Theme.defaultFont @@ -428,12 +432,23 @@ Page { height: enabled ? undefined : 0 leftPadding: Theme.menuItemLeftPadding - text: qsTr("Remove dataset") + text: !itemMenu.itemIsFavorite ? qsTr("Add to favorites") : qsTr("Remove from favorites") onTriggered: { - platformUtilities.removeDataset(itemMenu.itemPath); + if (!itemMenu.itemIsFavorite) { + localFilesModel.addToFavorites(itemMenu.itemPath); + } else { + localFilesModel.removeFromFavorites(itemMenu.itemPath); + } } } + MenuSeparator { + enabled: toggleFavoriteState.visible + visible: enabled + width: parent.width + height: enabled ? undefined : 0 + } + MenuItem { id: exportFolderTo enabled: platformUtilities.capabilities & PlatformUtilities.CustomExport && itemMenu.itemMetaType == LocalFilesModel.Folder @@ -451,8 +466,8 @@ Page { } MenuItem { - id: toggleFavoriteState - enabled: itemMenu.itemMetaType == LocalFilesModel.Folder && localFilesModel.isPathFavoriteEditable(itemMenu.itemPath) + id: sendCompressedFolderTo + enabled: platformUtilities.capabilities & PlatformUtilities.CustomSend && itemMenu.itemMetaType == LocalFilesModel.Folder visible: enabled font: Theme.defaultFont @@ -460,19 +475,15 @@ Page { height: enabled ? undefined : 0 leftPadding: Theme.menuItemLeftPadding - text: !itemMenu.itemIsFavorite ? qsTr("Add to favorites") : qsTr("Remove from favorites") + text: qsTr("Send compressed folder to...") onTriggered: { - if (!itemMenu.itemIsFavorite) { - localFilesModel.addToFavorites(itemMenu.itemPath); - } else { - localFilesModel.removeFromFavorites(itemMenu.itemPath); - } + platformUtilities.sendCompressedFolderTo(itemMenu.itemPath); } } MenuItem { - id: sendCompressedFolderTo - enabled: platformUtilities.capabilities & PlatformUtilities.CustomSend && itemMenu.itemMetaType == LocalFilesModel.Folder + id: uploadFolderToWebdav + enabled: itemMenu.itemHasWebdavConfiguration visible: enabled font: Theme.defaultFont @@ -480,9 +491,50 @@ Page { height: enabled ? undefined : 0 leftPadding: Theme.menuItemLeftPadding - text: qsTr("Send compressed folder to...") + text: qsTr("Upload folder to WebDAV server...") + onTriggered: + //platformUtilities.sendCompressedFolderTo(itemMenu.itemPath); + { + } + } + + MenuItem { + id: downloadFolderFromWebdav + enabled: itemMenu.itemHasWebdavConfiguration + visible: enabled + + font: Theme.defaultFont + width: parent.width + height: enabled ? undefined : 0 + leftPadding: Theme.menuItemLeftPadding + + text: qsTr("Download folder from WebDAV server...") + onTriggered: + //platformUtilities.sendCompressedFolderTo(itemMenu.itemPath); + { + } + } + + MenuSeparator { + enabled: removeProjectFolder.visible + visible: enabled + width: parent.width + height: enabled ? undefined : 0 + } + + MenuItem { + id: removeDataset + enabled: itemMenu.itemMetaType == LocalFilesModel.Dataset && !qfieldLocalDataPickerScreen.projectFolderView && table.model.isDeletedAllowedInCurrentPath + visible: enabled + + font: Theme.defaultFont + width: parent.width + height: enabled ? undefined : 0 + leftPadding: Theme.menuItemLeftPadding + + text: qsTr("Remove dataset") onTriggered: { - platformUtilities.sendCompressedFolderTo(itemMenu.itemPath); + platformUtilities.removeDataset(itemMenu.itemPath); } } @@ -496,7 +548,7 @@ Page { height: enabled ? undefined : 0 leftPadding: Theme.menuItemLeftPadding - text: qsTr("Remove project folder") + text: qsTr("Remove folder") onTriggered: { platformUtilities.removeFolder(itemMenu.itemPath); } From e8ef676bf2e5f918401c46d8dccb321359c42770 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 12:43:57 +0700 Subject: [PATCH 21/43] Fix item/folder deletion permission --- src/core/localfilesmodel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/localfilesmodel.cpp b/src/core/localfilesmodel.cpp index 7ccc6be80e..24a932fe39 100644 --- a/src/core/localfilesmodel.cpp +++ b/src/core/localfilesmodel.cpp @@ -178,14 +178,14 @@ bool LocalFilesModel::isDeletedAllowedInCurrentPath() const { const QString path = currentPath(); const QString applicationDirectory = PlatformUtilities::instance()->applicationDirectory(); - if ( !applicationDirectory.isEmpty() && path.startsWith( applicationDirectory + QDir::separator() ) ) + if ( !applicationDirectory.isEmpty() && path.startsWith( applicationDirectory ) ) { return true; } else { const QStringList additionalApplicationDirectories = PlatformUtilities::instance()->additionalApplicationDirectories(); - if ( std::any_of( additionalApplicationDirectories.begin(), additionalApplicationDirectories.end(), [&path]( const QString &directory ) { return ( !directory.isEmpty() && path.startsWith( directory + QDir::separator() ) ); } ) ) + if ( std::any_of( additionalApplicationDirectories.begin(), additionalApplicationDirectories.end(), [&path]( const QString &directory ) { return ( !directory.isEmpty() && path.startsWith( directory ) ); } ) ) { return true; } From 2c1e82caf312ec8a6505817952ec1275848ee305 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 13:16:47 +0700 Subject: [PATCH 22/43] Attach last modified date value to imported files --- src/core/webdavconnection.cpp | 18 +++++++++++++++--- src/core/webdavconnection.h | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index d03f37910d..2b4ccb88d1 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -242,7 +242,7 @@ void WebdavConnection::processDirParserFinished() } else { - mImportItems << item.path(); + mImportItems << item; mImportingBytesTotal += item.size(); } } @@ -257,7 +257,8 @@ void WebdavConnection::processImportItems() { if ( !mImportItems.isEmpty() ) { - const QString itemPath = mImportItems.first(); + const QString itemPath = mImportItems.first().path(); + const QDateTime itemLastModified = mImportItems.first().lastModified(); QNetworkReply *reply = mWebdavConnection.get( itemPath ); QTemporaryFile *temporaryFile = new QTemporaryFile( reply ); temporaryFile->setFileTemplate( QStringLiteral( "%1%2.XXXXXXXXXXXX" ).arg( mImportLocalPath, itemPath.mid( mImportRemotePath.size() ) ) ); @@ -275,8 +276,19 @@ void WebdavConnection::processImportItems() { temporaryFile->write( reply->readAll() ); temporaryFile->setAutoRemove( false ); - temporaryFile->close(); temporaryFile->rename( mImportLocalPath + itemPath.mid( mImportRemotePath.size() ) ); + temporaryFile->close(); + delete temporaryFile; + + // Attach last modified date value coming from the server (cannot be done via QTemporaryFile) + QFile file( QStringLiteral( "%1%2" ).arg( mImportLocalPath, itemPath.mid( mImportRemotePath.size() ) ) ); + file.open( QFile::Append ); + file.setFileTime( itemLastModified, QFileDevice::FileModificationTime ); + file.setFileTime( itemLastModified, QFileDevice::FileAccessTime ); + file.close(); + + QFileInfo fi( file ); + qDebug() << itemLastModified << fi.fileTime( QFileDevice::FileBirthTime ) << fi.fileTime( QFileDevice::FileModificationTime ) << fi.fileTime( QFileDevice::FileAccessTime ); } else { diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h index bd26220e5f..a41e7c98ac 100644 --- a/src/core/webdavconnection.h +++ b/src/core/webdavconnection.h @@ -118,7 +118,7 @@ class WebdavConnection : public QObject qint64 mImportingCurrentBytesReceived = 0; qint64 mImportingBytesReceived = 0; qint64 mImportingBytesTotal = 0; - QStringList mImportItems; + QList mImportItems; QString mImportRemotePath; QString mImportLocalPath; From 275f820f090f56e981be38aab67bb41092c282b5 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 14:27:07 +0700 Subject: [PATCH 23/43] Implement download from webdav folder functionality --- src/core/webdavconnection.cpp | 157 +++++++++++++++++------- src/core/webdavconnection.h | 22 ++-- src/qml/QFieldLocalDataPickerScreen.qml | 33 +++-- 3 files changed, 154 insertions(+), 58 deletions(-) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index 2b4ccb88d1..cefe47ce76 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -227,94 +227,121 @@ void WebdavConnection::processDirParserFinished() mAvailablePaths.sort(); emit availablePathsChanged(); } - else if ( mIsImportingPath ) + else if ( mIsImportingPath || mIsDownloadingPath ) { if ( !list.isEmpty() ) { applyStoredPassword(); - QDir importLocalDir( mImportLocalPath ); + QDir localDir( mGetLocalPath ); for ( const QWebdavItem &item : list ) { if ( item.isDir() ) { - importLocalDir.mkpath( item.path().mid( mImportRemotePath.size() ) ); + localDir.mkpath( item.path().mid( mGetRemotePath.size() ) ); } else { - mImportItems << item; - mImportingBytesTotal += item.size(); + if ( mIsDownloadingPath ) + { + QFileInfo fileInfo( mGetLocalPath + item.path().mid( mGetRemotePath.size() ) ); + if ( !fileInfo.exists() || ( fileInfo.fileTime( QFileDevice::FileModificationTime ) != item.lastModified() ) ) + { + mWebdavItems << item; + mBytesTotal += item.size(); + } + } + else + { + mWebdavItems << item; + mBytesTotal += item.size(); + } } } emit progressChanged(); } - processImportItems(); + getWebdavItems(); } } -void WebdavConnection::processImportItems() +void WebdavConnection::getWebdavItems() { - if ( !mImportItems.isEmpty() ) + if ( !mWebdavItems.isEmpty() ) { - const QString itemPath = mImportItems.first().path(); - const QDateTime itemLastModified = mImportItems.first().lastModified(); + const QString itemPath = mWebdavItems.first().path(); + const QDateTime itemLastModified = mWebdavItems.first().lastModified(); QNetworkReply *reply = mWebdavConnection.get( itemPath ); QTemporaryFile *temporaryFile = new QTemporaryFile( reply ); - temporaryFile->setFileTemplate( QStringLiteral( "%1%2.XXXXXXXXXXXX" ).arg( mImportLocalPath, itemPath.mid( mImportRemotePath.size() ) ) ); + temporaryFile->setFileTemplate( QStringLiteral( "%1%2.XXXXXXXXXXXX" ).arg( mGetLocalPath, itemPath.mid( mGetRemotePath.size() ) ) ); temporaryFile->open(); + connect( reply, &QNetworkReply::downloadProgress, this, [=]( int bytesReceived, int bytesTotal ) { - mImportingCurrentBytesReceived = bytesReceived; + mCurrentBytesReceived = bytesReceived; emit progressChanged(); temporaryFile->write( reply->readAll() ); } ); + connect( reply, &QNetworkReply::finished, this, [=]() { - mImportingBytesReceived += mImportingCurrentBytesReceived; - mImportingCurrentBytesReceived = 0; + mBytesReceived += mCurrentBytesReceived; + mCurrentBytesReceived = 0; if ( reply->error() == QNetworkReply::NoError ) { + QFile file( mGetLocalPath + itemPath.mid( mGetRemotePath.size() ) ); + if ( file.exists() ) + { + // Remove pre-existing file + file.remove(); + } + temporaryFile->write( reply->readAll() ); temporaryFile->setAutoRemove( false ); - temporaryFile->rename( mImportLocalPath + itemPath.mid( mImportRemotePath.size() ) ); + temporaryFile->rename( mGetLocalPath + itemPath.mid( mGetRemotePath.size() ) ); temporaryFile->close(); delete temporaryFile; // Attach last modified date value coming from the server (cannot be done via QTemporaryFile) - QFile file( QStringLiteral( "%1%2" ).arg( mImportLocalPath, itemPath.mid( mImportRemotePath.size() ) ) ); file.open( QFile::Append ); file.setFileTime( itemLastModified, QFileDevice::FileModificationTime ); file.setFileTime( itemLastModified, QFileDevice::FileAccessTime ); file.close(); QFileInfo fi( file ); - qDebug() << itemLastModified << fi.fileTime( QFileDevice::FileBirthTime ) << fi.fileTime( QFileDevice::FileModificationTime ) << fi.fileTime( QFileDevice::FileAccessTime ); } else { mLastError = tr( "Failed to download file %1 due to network error (%1)" ).arg( reply->error() ); } - mImportItems.removeFirst(); - processImportItems(); + mWebdavItems.removeFirst(); + getWebdavItems(); reply->deleteLater(); } ); } else { - QVariantMap webdavConfiguration; - webdavConfiguration[QStringLiteral( "url" )] = mUrl; - webdavConfiguration[QStringLiteral( "username" )] = mUsername; - webdavConfiguration[QStringLiteral( "remote_path" )] = mImportRemotePath; - - QJsonDocument jsonDocument = QJsonDocument::fromVariant( webdavConfiguration ); - QFile jsonFile( QStringLiteral( "%1qfield_webdav_configuration.json" ).arg( mImportLocalPath ) ); - jsonFile.open( QFile::WriteOnly ); - jsonFile.write( jsonDocument.toJson() ); - jsonFile.close(); - - mIsImportingPath = false; - emit isImportingPathChanged(); + if ( mIsImportingPath ) + { + QVariantMap webdavConfiguration; + webdavConfiguration[QStringLiteral( "url" )] = mUrl; + webdavConfiguration[QStringLiteral( "username" )] = mUsername; + webdavConfiguration[QStringLiteral( "remote_path" )] = mGetRemotePath; + + QJsonDocument jsonDocument = QJsonDocument::fromVariant( webdavConfiguration ); + QFile jsonFile( QStringLiteral( "%1qfield_webdav_configuration.json" ).arg( mGetLocalPath ) ); + jsonFile.open( QFile::WriteOnly ); + jsonFile.write( jsonDocument.toJson() ); + jsonFile.close(); + + mIsImportingPath = false; + emit isImportingPathChanged(); + } + else if ( mIsDownloadingPath ) + { + mIsDownloadingPath = false; + emit isDownloadingPathChanged(); + } } } @@ -331,26 +358,74 @@ void WebdavConnection::importPath( const QString &remotePath, const QString &loc QDir localDir( localPath ); localDir.mkpath( localFolder ); - mImportRemotePath = remotePath; - mImportLocalPath = QDir::cleanPath( localPath + QDir::separator() + localFolder ) + QDir::separator(); - - mImportItems.clear(); + mGetRemotePath = remotePath; + mGetLocalPath = QDir::cleanPath( localPath + QDir::separator() + localFolder ) + QDir::separator(); - mImportingBytesReceived = 0; - mImportingBytesTotal = 0; + mWebdavItems.clear(); + mBytesReceived = 0; + mBytesTotal = 0; emit progressChanged(); mIsImportingPath = true; emit isImportingPathChanged(); - mWebdavDirParser.listDirectory( &mWebdavConnection, mImportRemotePath, true ); + mWebdavDirParser.listDirectory( &mWebdavConnection, mGetRemotePath, true ); +} + +void WebdavConnection::downloadPath( const QString &localPath ) +{ + QDir dir( localPath ); + bool webdavConfigurationExists = dir.exists( "qfield_webdav_configuration.json" ); + QStringList remoteChildrenPath; + while ( !webdavConfigurationExists ) + { + remoteChildrenPath << dir.dirName(); + if ( !dir.cdUp() ) + break; + + webdavConfigurationExists = dir.exists( "qfield_webdav_configuration.json" ); + } + + if ( webdavConfigurationExists ) + { + QFile webdavConfigurationFile( dir.absolutePath() + QDir::separator() + QStringLiteral( "qfield_webdav_configuration.json" ) ); + webdavConfigurationFile.open( QFile::ReadOnly ); + QJsonDocument jsonDocument = QJsonDocument::fromJson( webdavConfigurationFile.readAll() ); + if ( !jsonDocument.isEmpty() ) + { + QVariantMap webdavConfiguration = jsonDocument.toVariant().toMap(); + setUrl( webdavConfiguration["url"].toString() ); + setUsername( webdavConfiguration["username"].toString() ); + if ( isPasswordStored() ) + { + setupConnection(); + + mGetRemotePath = webdavConfiguration["remote_path"].toString(); + if ( !remoteChildrenPath.isEmpty() ) + { + mGetRemotePath = mGetRemotePath + remoteChildrenPath.join( "/" ) + QStringLiteral( "/" ); + } + mGetLocalPath = QDir::cleanPath( localPath ) + QDir::separator(); + + mWebdavItems.clear(); + mBytesReceived = 0; + mBytesTotal = 0; + emit progressChanged(); + + mIsDownloadingPath = true; + emit isDownloadingPathChanged(); + + mWebdavDirParser.listDirectory( &mWebdavConnection, mGetRemotePath, true ); + } + } + } } double WebdavConnection::progress() const { - if ( mIsImportingPath && mImportingBytesTotal > 0 ) + if ( ( mIsImportingPath || mIsDownloadingPath ) && mBytesTotal > 0 ) { - return static_cast( mImportingBytesReceived + mImportingCurrentBytesReceived ) / mImportingBytesTotal; + return static_cast( mBytesReceived + mCurrentBytesReceived ) / mBytesTotal; } return 0; diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h index a41e7c98ac..866da7ec9b 100644 --- a/src/core/webdavconnection.h +++ b/src/core/webdavconnection.h @@ -40,6 +40,7 @@ class WebdavConnection : public QObject Q_PROPERTY( bool isPasswordStored READ isPasswordStored NOTIFY isPasswordStoredChanged ) Q_PROPERTY( bool isFetchingAvailablePaths READ isFetchingAvailablePaths NOTIFY isFetchingAvailablePathsChanged ) Q_PROPERTY( bool isImportingPath READ isImportingPath NOTIFY isImportingPathChanged ) + Q_PROPERTY( bool isDownloadingPath READ isDownloadingPath NOTIFY isDownloadingPathChanged ) Q_PROPERTY( QStringList availablePaths READ availablePaths NOTIFY availablePathsChanged ) Q_PROPERTY( double progress READ progress NOTIFY progressChanged ) @@ -73,6 +74,8 @@ class WebdavConnection : public QObject bool isImportingPath() const { return mIsImportingPath; } + bool isDownloadingPath() const { return mIsDownloadingPath; } + double progress() const; QString lastError() const { return mLastError; } @@ -80,6 +83,7 @@ class WebdavConnection : public QObject Q_INVOKABLE void fetchAvailablePaths(); Q_INVOKABLE void importPath( const QString &remotePath, const QString &localPath ); + Q_INVOKABLE void downloadPath( const QString &localPath ); signals: void urlChanged(); @@ -89,6 +93,7 @@ class WebdavConnection : public QObject void isPasswordStoredChanged(); void isFetchingAvailablePathsChanged(); void isImportingPathChanged(); + void isDownloadingPathChanged(); void availablePathsChanged(); void progressChanged(); void lastErrorChanged(); @@ -102,7 +107,7 @@ class WebdavConnection : public QObject void checkStoredPassword(); void applyStoredPassword(); void setupConnection(); - void processImportItems(); + void getWebdavItems(); QString mUrl; QString mUsername; @@ -115,12 +120,15 @@ class WebdavConnection : public QObject QStringList mAvailablePaths; bool mIsImportingPath = false; - qint64 mImportingCurrentBytesReceived = 0; - qint64 mImportingBytesReceived = 0; - qint64 mImportingBytesTotal = 0; - QList mImportItems; - QString mImportRemotePath; - QString mImportLocalPath; + bool mIsDownloadingPath = false; + + QString mGetRemotePath; + QString mGetLocalPath; + + QList mWebdavItems; + qint64 mCurrentBytesReceived = 0; + qint64 mBytesReceived = 0; + qint64 mBytesTotal = 0; QWebdav mWebdavConnection; QWebdavDirParser mWebdavDirParser; diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index 8a2acd35bd..a14e03e04e 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -11,6 +11,7 @@ import Theme Page { id: qfieldLocalDataPickerScreen + property bool openedOnce: false property bool projectFolderView: false property alias model: table.model @@ -18,6 +19,12 @@ Page { focus: visible + onVisibleChanged: { + if (visible) { + openedOnce = true; + } + } + header: QfPageHeader { title: projectFolderView ? qsTr("Project Folder") : qsTr("Local Projects & Datasets") @@ -509,9 +516,10 @@ Page { leftPadding: Theme.menuItemLeftPadding text: qsTr("Download folder from WebDAV server...") - onTriggered: - //platformUtilities.sendCompressedFolderTo(itemMenu.itemPath); - { + onTriggered: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.downloadPath(itemMenu.itemPath); + } } } @@ -774,8 +782,18 @@ Page { } } + onIsDownloadingPathChanged: { + if (isDownloadingPath) { + busyOverlay.text = qsTr("Downloading WebDAV folder"); + busyOverlay.progress = 0; + busyOverlay.state = "visible"; + } else { + busyOverlay.state = "hidden"; + } + } + onProgressChanged: { - if (isImportingPath) { + if (isImportingPath || isDownloadingPath) { busyOverlay.progress = progress; } } @@ -792,7 +810,7 @@ Page { Loader { id: webdavConnectionLoader - active: importWebdavDialog.openedOnce + active: qfieldLocalDataPickerScreen.openedOnce sourceComponent: webdavConnectionComponent } @@ -802,11 +820,6 @@ Page { focus: true y: (mainWindow.height - height - 80) / 2 - property bool openedOnce: false - onAboutToShow: { - openedOnce = true; - } - Column { width: childrenRect.width height: childrenRect.height From c39f2befcaf49708bca280d7208f812d4070918a Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 15:15:55 +0700 Subject: [PATCH 24/43] Implement upload to webdav folder funcationality --- src/core/webdavconnection.cpp | 178 +++++++++++++++++++++--- src/core/webdavconnection.h | 18 ++- src/qml/QFieldLocalDataPickerScreen.qml | 17 ++- 3 files changed, 184 insertions(+), 29 deletions(-) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index cefe47ce76..9131c56106 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -233,18 +233,18 @@ void WebdavConnection::processDirParserFinished() { applyStoredPassword(); - QDir localDir( mGetLocalPath ); + QDir localDir( mProcessLocalPath ); for ( const QWebdavItem &item : list ) { if ( item.isDir() ) { - localDir.mkpath( item.path().mid( mGetRemotePath.size() ) ); + localDir.mkpath( item.path().mid( mProcessRemotePath.size() ) ); } else { if ( mIsDownloadingPath ) { - QFileInfo fileInfo( mGetLocalPath + item.path().mid( mGetRemotePath.size() ) ); + QFileInfo fileInfo( mProcessLocalPath + item.path().mid( mProcessRemotePath.size() ) ); if ( !fileInfo.exists() || ( fileInfo.fileTime( QFileDevice::FileModificationTime ) != item.lastModified() ) ) { mWebdavItems << item; @@ -263,6 +263,40 @@ void WebdavConnection::processDirParserFinished() getWebdavItems(); } + else if ( mIsUploadingPath ) + { + applyStoredPassword(); + + for ( const QWebdavItem &item : list ) + { + if ( !item.isDir() ) + { + QFileInfo fileInfo( mProcessLocalPath + item.path().mid( mProcessRemotePath.size() ) ); + if ( fileInfo.exists() ) + { + auto localFileInfo = std::find_if( mLocalItems.begin(), mLocalItems.end(), [&fileInfo]( const QFileInfo &entry ) { + return entry.absoluteFilePath() == fileInfo.absoluteFilePath(); + } ); + + if ( localFileInfo != mLocalItems.end() ) + { + if ( localFileInfo->fileTime( QFileDevice::FileModificationTime ) == item.lastModified() ) + { + mLocalItems.remove( localFileInfo - mLocalItems.begin(), 1 ); + } + } + } + } + } + + for ( const QFileInfo &fileInfo : mLocalItems ) + { + mBytesTotal += fileInfo.size(); + } + emit progressChanged(); + + putLocalItems(); + } } void WebdavConnection::getWebdavItems() @@ -273,22 +307,22 @@ void WebdavConnection::getWebdavItems() const QDateTime itemLastModified = mWebdavItems.first().lastModified(); QNetworkReply *reply = mWebdavConnection.get( itemPath ); QTemporaryFile *temporaryFile = new QTemporaryFile( reply ); - temporaryFile->setFileTemplate( QStringLiteral( "%1%2.XXXXXXXXXXXX" ).arg( mGetLocalPath, itemPath.mid( mGetRemotePath.size() ) ) ); + temporaryFile->setFileTemplate( QStringLiteral( "%1%2.XXXXXXXXXXXX" ).arg( mProcessLocalPath, itemPath.mid( mProcessRemotePath.size() ) ) ); temporaryFile->open(); connect( reply, &QNetworkReply::downloadProgress, this, [=]( int bytesReceived, int bytesTotal ) { - mCurrentBytesReceived = bytesReceived; + mCurrentBytesProcessed = bytesReceived; emit progressChanged(); temporaryFile->write( reply->readAll() ); } ); connect( reply, &QNetworkReply::finished, this, [=]() { - mBytesReceived += mCurrentBytesReceived; - mCurrentBytesReceived = 0; + mBytesProcessed += mCurrentBytesProcessed; + mCurrentBytesProcessed = 0; if ( reply->error() == QNetworkReply::NoError ) { - QFile file( mGetLocalPath + itemPath.mid( mGetRemotePath.size() ) ); + QFile file( mProcessLocalPath + itemPath.mid( mProcessRemotePath.size() ) ); if ( file.exists() ) { // Remove pre-existing file @@ -297,7 +331,7 @@ void WebdavConnection::getWebdavItems() temporaryFile->write( reply->readAll() ); temporaryFile->setAutoRemove( false ); - temporaryFile->rename( mGetLocalPath + itemPath.mid( mGetRemotePath.size() ) ); + temporaryFile->rename( mProcessLocalPath + itemPath.mid( mProcessRemotePath.size() ) ); temporaryFile->close(); delete temporaryFile; @@ -326,10 +360,10 @@ void WebdavConnection::getWebdavItems() QVariantMap webdavConfiguration; webdavConfiguration[QStringLiteral( "url" )] = mUrl; webdavConfiguration[QStringLiteral( "username" )] = mUsername; - webdavConfiguration[QStringLiteral( "remote_path" )] = mGetRemotePath; + webdavConfiguration[QStringLiteral( "remote_path" )] = mProcessRemotePath; QJsonDocument jsonDocument = QJsonDocument::fromVariant( webdavConfiguration ); - QFile jsonFile( QStringLiteral( "%1qfield_webdav_configuration.json" ).arg( mGetLocalPath ) ); + QFile jsonFile( QStringLiteral( "%1qfield_webdav_configuration.json" ).arg( mProcessLocalPath ) ); jsonFile.open( QFile::WriteOnly ); jsonFile.write( jsonDocument.toJson() ); jsonFile.close(); @@ -345,6 +379,49 @@ void WebdavConnection::getWebdavItems() } } +void WebdavConnection::putLocalItems() +{ + if ( !mLocalItems.isEmpty() ) + { + const QString itemPath = mLocalItems.first().absoluteFilePath(); + qDebug() << "itemPath " << itemPath; + const QString remoteItemPath = mProcessRemotePath + itemPath.mid( mProcessLocalPath.size() ).replace( QDir::separator(), "/" ); + qDebug() << "remoteItemPath " << remoteItemPath; + + QFile *file = new QFile( itemPath ); + file->open( QFile::ReadOnly ); + QNetworkReply *reply = mWebdavConnection.put( remoteItemPath, file ); + file->setParent( reply ); + + connect( reply, &QNetworkReply::uploadProgress, this, [=]( int bytesSent, int bytesTotal ) { + mCurrentBytesProcessed = bytesSent; + emit progressChanged(); + } ); + + connect( reply, &QNetworkReply::finished, this, [=]() { + mBytesProcessed += mCurrentBytesProcessed; + mCurrentBytesProcessed = 0; + emit progressChanged(); + if ( reply->error() != QNetworkReply::NoError ) + { + mLastError = tr( "Failed to upload file %1 due to network error (%1)" ).arg( reply->error() ); + } + + mLocalItems.removeFirst(); + putLocalItems(); + reply->deleteLater(); + } ); + } + else + { + if ( mIsUploadingPath ) + { + mIsUploadingPath = false; + emit isUploadingPathChanged(); + } + } +} + void WebdavConnection::importPath( const QString &remotePath, const QString &localPath ) { if ( mUrl.isEmpty() || mUsername.isEmpty() || ( mPassword.isEmpty() && mStoredPassword.isEmpty() ) ) @@ -358,18 +435,18 @@ void WebdavConnection::importPath( const QString &remotePath, const QString &loc QDir localDir( localPath ); localDir.mkpath( localFolder ); - mGetRemotePath = remotePath; - mGetLocalPath = QDir::cleanPath( localPath + QDir::separator() + localFolder ) + QDir::separator(); + mProcessRemotePath = remotePath; + mProcessLocalPath = QDir::cleanPath( localPath + QDir::separator() + localFolder ) + QDir::separator(); mWebdavItems.clear(); - mBytesReceived = 0; + mBytesProcessed = 0; mBytesTotal = 0; emit progressChanged(); mIsImportingPath = true; emit isImportingPathChanged(); - mWebdavDirParser.listDirectory( &mWebdavConnection, mGetRemotePath, true ); + mWebdavDirParser.listDirectory( &mWebdavConnection, mProcessRemotePath, true ); } void WebdavConnection::downloadPath( const QString &localPath ) @@ -400,22 +477,81 @@ void WebdavConnection::downloadPath( const QString &localPath ) { setupConnection(); - mGetRemotePath = webdavConfiguration["remote_path"].toString(); + mProcessRemotePath = webdavConfiguration["remote_path"].toString(); if ( !remoteChildrenPath.isEmpty() ) { - mGetRemotePath = mGetRemotePath + remoteChildrenPath.join( "/" ) + QStringLiteral( "/" ); + mProcessRemotePath = mProcessRemotePath + remoteChildrenPath.join( "/" ) + QStringLiteral( "/" ); } - mGetLocalPath = QDir::cleanPath( localPath ) + QDir::separator(); + mProcessLocalPath = QDir::cleanPath( localPath ) + QDir::separator(); mWebdavItems.clear(); - mBytesReceived = 0; + mBytesProcessed = 0; mBytesTotal = 0; emit progressChanged(); mIsDownloadingPath = true; emit isDownloadingPathChanged(); - mWebdavDirParser.listDirectory( &mWebdavConnection, mGetRemotePath, true ); + mWebdavDirParser.listDirectory( &mWebdavConnection, mProcessRemotePath, true ); + } + } + } +} + +void WebdavConnection::uploadPath( const QString &localPath ) +{ + QDir dir( localPath ); + bool webdavConfigurationExists = dir.exists( "qfield_webdav_configuration.json" ); + QStringList remoteChildrenPath; + while ( !webdavConfigurationExists ) + { + remoteChildrenPath << dir.dirName(); + if ( !dir.cdUp() ) + break; + + webdavConfigurationExists = dir.exists( "qfield_webdav_configuration.json" ); + } + + if ( webdavConfigurationExists ) + { + QFile webdavConfigurationFile( dir.absolutePath() + QDir::separator() + QStringLiteral( "qfield_webdav_configuration.json" ) ); + webdavConfigurationFile.open( QFile::ReadOnly ); + QJsonDocument jsonDocument = QJsonDocument::fromJson( webdavConfigurationFile.readAll() ); + if ( !jsonDocument.isEmpty() ) + { + QVariantMap webdavConfiguration = jsonDocument.toVariant().toMap(); + setUrl( webdavConfiguration["url"].toString() ); + setUsername( webdavConfiguration["username"].toString() ); + if ( isPasswordStored() ) + { + setupConnection(); + + mProcessRemotePath = webdavConfiguration["remote_path"].toString(); + if ( !remoteChildrenPath.isEmpty() ) + { + mProcessRemotePath = mProcessRemotePath + remoteChildrenPath.join( "/" ) + QStringLiteral( "/" ); + } + mProcessLocalPath = QDir::cleanPath( localPath ) + QDir::separator(); + + mLocalItems.clear(); + QDirIterator it( mProcessLocalPath, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories ); + while ( it.hasNext() ) + { + it.next(); + if ( it.fileName() != QStringLiteral( "qfield_webdav_configuration.json" ) ) + { + mLocalItems << it.fileInfo(); + } + } + + mBytesProcessed = 0; + mBytesTotal = 0; + emit progressChanged(); + + mIsUploadingPath = true; + emit isUploadingPathChanged(); + + mWebdavDirParser.listDirectory( &mWebdavConnection, mProcessRemotePath, true ); } } } @@ -425,7 +561,7 @@ double WebdavConnection::progress() const { if ( ( mIsImportingPath || mIsDownloadingPath ) && mBytesTotal > 0 ) { - return static_cast( mBytesReceived + mCurrentBytesReceived ) / mBytesTotal; + return static_cast( mBytesProcessed + mCurrentBytesProcessed ) / mBytesTotal; } return 0; diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h index 866da7ec9b..036f519a26 100644 --- a/src/core/webdavconnection.h +++ b/src/core/webdavconnection.h @@ -41,6 +41,7 @@ class WebdavConnection : public QObject Q_PROPERTY( bool isFetchingAvailablePaths READ isFetchingAvailablePaths NOTIFY isFetchingAvailablePathsChanged ) Q_PROPERTY( bool isImportingPath READ isImportingPath NOTIFY isImportingPathChanged ) Q_PROPERTY( bool isDownloadingPath READ isDownloadingPath NOTIFY isDownloadingPathChanged ) + Q_PROPERTY( bool isUploadingPath READ isUploadingPath NOTIFY isUploadingPathChanged ) Q_PROPERTY( QStringList availablePaths READ availablePaths NOTIFY availablePathsChanged ) Q_PROPERTY( double progress READ progress NOTIFY progressChanged ) @@ -76,6 +77,8 @@ class WebdavConnection : public QObject bool isDownloadingPath() const { return mIsDownloadingPath; } + bool isUploadingPath() const { return mIsUploadingPath; } + double progress() const; QString lastError() const { return mLastError; } @@ -84,6 +87,7 @@ class WebdavConnection : public QObject Q_INVOKABLE void importPath( const QString &remotePath, const QString &localPath ); Q_INVOKABLE void downloadPath( const QString &localPath ); + Q_INVOKABLE void uploadPath( const QString &localPath ); signals: void urlChanged(); @@ -94,6 +98,7 @@ class WebdavConnection : public QObject void isFetchingAvailablePathsChanged(); void isImportingPathChanged(); void isDownloadingPathChanged(); + void isUploadingPathChanged(); void availablePathsChanged(); void progressChanged(); void lastErrorChanged(); @@ -108,6 +113,7 @@ class WebdavConnection : public QObject void applyStoredPassword(); void setupConnection(); void getWebdavItems(); + void putLocalItems(); QString mUrl; QString mUsername; @@ -121,13 +127,15 @@ class WebdavConnection : public QObject bool mIsImportingPath = false; bool mIsDownloadingPath = false; - - QString mGetRemotePath; - QString mGetLocalPath; + bool mIsUploadingPath = false; QList mWebdavItems; - qint64 mCurrentBytesReceived = 0; - qint64 mBytesReceived = 0; + QList mLocalItems; + + QString mProcessRemotePath; + QString mProcessLocalPath; + qint64 mCurrentBytesProcessed = 0; + qint64 mBytesProcessed = 0; qint64 mBytesTotal = 0; QWebdav mWebdavConnection; diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index a14e03e04e..771a5a20d4 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -499,9 +499,10 @@ Page { leftPadding: Theme.menuItemLeftPadding text: qsTr("Upload folder to WebDAV server...") - onTriggered: - //platformUtilities.sendCompressedFolderTo(itemMenu.itemPath); - { + onTriggered: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.uploadPath(itemMenu.itemPath); + } } } @@ -792,6 +793,16 @@ Page { } } + onIsUploadingPathChanged: { + if (isUploadingPath) { + busyOverlay.text = qsTr("Uploading WebDAV folder"); + busyOverlay.progress = 0; + busyOverlay.state = "visible"; + } else { + busyOverlay.state = "hidden"; + } + } + onProgressChanged: { if (isImportingPath || isDownloadingPath) { busyOverlay.progress = progress; From fdab3995356af3047ce956993663fb488877e1f7 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 15:42:49 +0700 Subject: [PATCH 25/43] Insure paths exists remotely prior to uploading files --- src/core/webdavconnection.cpp | 51 ++++++++++++++++++++++--- src/core/webdavconnection.h | 1 + src/qml/QFieldLocalDataPickerScreen.qml | 2 +- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index 9131c56106..4c1bbc37bb 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -267,9 +267,14 @@ void WebdavConnection::processDirParserFinished() { applyStoredPassword(); + QStringList remoteDirs; for ( const QWebdavItem &item : list ) { - if ( !item.isDir() ) + if ( item.isDir() ) + { + remoteDirs << item.path(); + } + else { QFileInfo fileInfo( mProcessLocalPath + item.path().mid( mProcessRemotePath.size() ) ); if ( fileInfo.exists() ) @@ -289,8 +294,25 @@ void WebdavConnection::processDirParserFinished() } } + mWebdavMkDirs.clear(); for ( const QFileInfo &fileInfo : mLocalItems ) { + // Insure the path exists remotely + QString remoteDir = mProcessRemotePath + fileInfo.absolutePath().mid( mProcessLocalPath.size() ).replace( QDir::separator(), "/" ); + if ( !remoteDirs.contains( remoteDir ) && !mWebdavMkDirs.contains( remoteDir ) ) + { + const QStringList remoteDirParts = remoteDir.mid( mProcessRemotePath.size() ).split( "/", Qt::SkipEmptyParts ); + remoteDir = mProcessRemotePath; + for ( const QString &part : remoteDirParts ) + { + remoteDir += part + "/"; + if ( !remoteDirs.contains( remoteDir ) && !mWebdavMkDirs.contains( remoteDir ) ) + { + mWebdavMkDirs << remoteDir; + } + } + } + mBytesTotal += fileInfo.size(); } emit progressChanged(); @@ -345,7 +367,7 @@ void WebdavConnection::getWebdavItems() } else { - mLastError = tr( "Failed to download file %1 due to network error (%1)" ).arg( reply->error() ); + mLastError = tr( "Failed to download file %1 due to network error (%2)" ).arg( itemPath ).arg( reply->error() ); } mWebdavItems.removeFirst(); @@ -381,12 +403,29 @@ void WebdavConnection::getWebdavItems() void WebdavConnection::putLocalItems() { - if ( !mLocalItems.isEmpty() ) + if ( !mWebdavMkDirs.isEmpty() ) + { + const QString dirPath = mWebdavMkDirs.first(); + + QNetworkReply *reply = mWebdavConnection.mkdir( dirPath ); + + connect( reply, &QNetworkReply::finished, this, [=]() { + mBytesProcessed += mCurrentBytesProcessed; + mCurrentBytesProcessed = 0; + emit progressChanged(); + if ( reply->error() != QNetworkReply::NoError ) + { + mLastError = tr( "Failed to upload file %1 due to network error (%2)" ).arg( dirPath ).arg( reply->error() ); + } + + mWebdavMkDirs.removeFirst(); + putLocalItems(); + } ); + } + else if ( !mLocalItems.isEmpty() ) { const QString itemPath = mLocalItems.first().absoluteFilePath(); - qDebug() << "itemPath " << itemPath; const QString remoteItemPath = mProcessRemotePath + itemPath.mid( mProcessLocalPath.size() ).replace( QDir::separator(), "/" ); - qDebug() << "remoteItemPath " << remoteItemPath; QFile *file = new QFile( itemPath ); file->open( QFile::ReadOnly ); @@ -404,7 +443,7 @@ void WebdavConnection::putLocalItems() emit progressChanged(); if ( reply->error() != QNetworkReply::NoError ) { - mLastError = tr( "Failed to upload file %1 due to network error (%1)" ).arg( reply->error() ); + mLastError = tr( "Failed to upload file %1 due to network error (%2)" ).arg( remoteItemPath ).arg( reply->error() ); } mLocalItems.removeFirst(); diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h index 036f519a26..ead76ef0bf 100644 --- a/src/core/webdavconnection.h +++ b/src/core/webdavconnection.h @@ -130,6 +130,7 @@ class WebdavConnection : public QObject bool mIsUploadingPath = false; QList mWebdavItems; + QList mWebdavMkDirs; QList mLocalItems; QString mProcessRemotePath; diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index 771a5a20d4..d589b2d9de 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -804,7 +804,7 @@ Page { } onProgressChanged: { - if (isImportingPath || isDownloadingPath) { + if (isImportingPath || isDownloadingPath || isUploadingPath) { busyOverlay.progress = progress; } } From 906a3f34eab3e22c844c172aea32cf568d5199fa Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 15:53:11 +0700 Subject: [PATCH 26/43] Silence the flood of debug info --- vcpkg/ports/qtwebdav/debug.patch | 28 ++++++++++++++++++++++++++++ vcpkg/ports/qtwebdav/portfile.cmake | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 vcpkg/ports/qtwebdav/debug.patch diff --git a/vcpkg/ports/qtwebdav/debug.patch b/vcpkg/ports/qtwebdav/debug.patch new file mode 100644 index 0000000000..1a9eb8b170 --- /dev/null +++ b/vcpkg/ports/qtwebdav/debug.patch @@ -0,0 +1,28 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 36bbf15..c0b691f 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -7,6 +7,7 @@ include(GenerateExportHeader) + option(BUILD_EXAMPLE "Build with example" OFF) + option(BUILD_WITH_QT6 "Explicitly build with Qt6" OFF) + option(WITH_EXTENDED_PROPERTIES "Build with extended properties for WedDAV items" OFF) ++option(WITH_DEBUG "Build with debug information" OFF) + + # Determine Qt version to use + if (BUILD_WITH_QT6) +@@ -60,10 +61,13 @@ target_include_directories(QtWebDAV PUBLIC + $ + $ + ) +-target_compile_definitions(QtWebDAV PRIVATE DEBUG_WEBDAV) ++ ++if (WITH_DEBUG) ++ target_compile_definitions(QtWebDAV PRIVATE DEBUG_WEBDAV) ++endif() + + if (WITH_EXTENDED_PROPERTIES) +- target_compile_definitions(QtWebDAV PRIVATE -DQWEBDAVITEM_EXTENDED_PROPERTIES) ++ target_compile_definitions(QtWebDAV PRIVATE QWEBDAVITEM_EXTENDED_PROPERTIES) + endif() + + # Link Qt libraries diff --git a/vcpkg/ports/qtwebdav/portfile.cmake b/vcpkg/ports/qtwebdav/portfile.cmake index efd4f2dd8b..2e86612455 100644 --- a/vcpkg/ports/qtwebdav/portfile.cmake +++ b/vcpkg/ports/qtwebdav/portfile.cmake @@ -4,7 +4,7 @@ vcpkg_from_github( REF 6363cabf99368bb8a659af63584ac790f04cc9c6 SHA512 059e99181fdd751b4598986997721b9e0e7debccbad7f3a258cba2745bd4b28896611566017c50a7bb9ace523bca388551a327b17f76968032c8a6e9a6bb2d34 HEAD_REF main - PATCHES fix.patch + PATCHES fix.patch debug.patch ) list(APPEND QTWEBDAV_OPTIONS -DBUILD_WITH_QT6=True) From 81951fe2fb1cd756f11bf6c9e3583c2172761eed Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 16:13:51 +0700 Subject: [PATCH 27/43] Have uploaded files adapt their last modified to match server to avoid infinite re-upload --- src/core/webdavconnection.cpp | 116 ++++++++++++++++++---------- src/core/webdavconnection.h | 1 + vcpkg/ports/qtwebdav/portfile.cmake | 1 + 3 files changed, 78 insertions(+), 40 deletions(-) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index 4c1bbc37bb..c7310e5000 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -265,59 +265,86 @@ void WebdavConnection::processDirParserFinished() } else if ( mIsUploadingPath ) { - applyStoredPassword(); - - QStringList remoteDirs; - for ( const QWebdavItem &item : list ) + if ( !mWebdavLastModified.isEmpty() ) { - if ( item.isDir() ) + // Adjust modified date to match upload files + for ( const QWebdavItem &item : list ) { - remoteDirs << item.path(); + if ( mWebdavLastModified.contains( item.path() ) ) + { + QFile file( mProcessLocalPath + item.path().mid( mProcessRemotePath.size() ) ); + if ( file.exists() ) + { + // The local file should always exist at this stage, just playing safe + file.open( QFile::Append ); + file.setFileTime( item.lastModified(), QFileDevice::FileModificationTime ); + file.setFileTime( item.lastModified(), QFileDevice::FileAccessTime ); + file.close(); + } + } } - else + mWebdavLastModified.clear(); + + mIsUploadingPath = false; + emit isUploadingPathChanged(); + } + else + { + // Filter files to upload + applyStoredPassword(); + + QStringList remoteDirs; + for ( const QWebdavItem &item : list ) { - QFileInfo fileInfo( mProcessLocalPath + item.path().mid( mProcessRemotePath.size() ) ); - if ( fileInfo.exists() ) + if ( item.isDir() ) { - auto localFileInfo = std::find_if( mLocalItems.begin(), mLocalItems.end(), [&fileInfo]( const QFileInfo &entry ) { - return entry.absoluteFilePath() == fileInfo.absoluteFilePath(); - } ); - - if ( localFileInfo != mLocalItems.end() ) + remoteDirs << item.path(); + } + else + { + QFileInfo fileInfo( mProcessLocalPath + item.path().mid( mProcessRemotePath.size() ) ); + if ( fileInfo.exists() ) { - if ( localFileInfo->fileTime( QFileDevice::FileModificationTime ) == item.lastModified() ) + auto localFileInfo = std::find_if( mLocalItems.begin(), mLocalItems.end(), [&fileInfo]( const QFileInfo &entry ) { + return entry.absoluteFilePath() == fileInfo.absoluteFilePath(); + } ); + + if ( localFileInfo != mLocalItems.end() ) { - mLocalItems.remove( localFileInfo - mLocalItems.begin(), 1 ); + if ( localFileInfo->fileTime( QFileDevice::FileModificationTime ) == item.lastModified() ) + { + mLocalItems.remove( localFileInfo - mLocalItems.begin(), 1 ); + } } } } } - } - mWebdavMkDirs.clear(); - for ( const QFileInfo &fileInfo : mLocalItems ) - { - // Insure the path exists remotely - QString remoteDir = mProcessRemotePath + fileInfo.absolutePath().mid( mProcessLocalPath.size() ).replace( QDir::separator(), "/" ); - if ( !remoteDirs.contains( remoteDir ) && !mWebdavMkDirs.contains( remoteDir ) ) + mWebdavMkDirs.clear(); + for ( const QFileInfo &fileInfo : mLocalItems ) { - const QStringList remoteDirParts = remoteDir.mid( mProcessRemotePath.size() ).split( "/", Qt::SkipEmptyParts ); - remoteDir = mProcessRemotePath; - for ( const QString &part : remoteDirParts ) + // Insure the path exists remotely + QString remoteDir = mProcessRemotePath + fileInfo.absolutePath().mid( mProcessLocalPath.size() ).replace( QDir::separator(), "/" ); + if ( !remoteDirs.contains( remoteDir ) && !mWebdavMkDirs.contains( remoteDir ) ) { - remoteDir += part + "/"; - if ( !remoteDirs.contains( remoteDir ) && !mWebdavMkDirs.contains( remoteDir ) ) + const QStringList remoteDirParts = remoteDir.mid( mProcessRemotePath.size() ).split( "/", Qt::SkipEmptyParts ); + remoteDir = mProcessRemotePath; + for ( const QString &part : remoteDirParts ) { - mWebdavMkDirs << remoteDir; + remoteDir += part + "/"; + if ( !remoteDirs.contains( remoteDir ) && !mWebdavMkDirs.contains( remoteDir ) ) + { + mWebdavMkDirs << remoteDir; + } } } + + mBytesTotal += fileInfo.size(); } + emit progressChanged(); - mBytesTotal += fileInfo.size(); + putLocalItems(); } - emit progressChanged(); - - putLocalItems(); } } @@ -362,8 +389,6 @@ void WebdavConnection::getWebdavItems() file.setFileTime( itemLastModified, QFileDevice::FileModificationTime ); file.setFileTime( itemLastModified, QFileDevice::FileAccessTime ); file.close(); - - QFileInfo fi( file ); } else { @@ -446,6 +471,8 @@ void WebdavConnection::putLocalItems() mLastError = tr( "Failed to upload file %1 due to network error (%2)" ).arg( remoteItemPath ).arg( reply->error() ); } + mWebdavLastModified << remoteItemPath; + mLocalItems.removeFirst(); putLocalItems(); reply->deleteLater(); @@ -455,8 +482,15 @@ void WebdavConnection::putLocalItems() { if ( mIsUploadingPath ) { - mIsUploadingPath = false; - emit isUploadingPathChanged(); + if ( !mWebdavLastModified.isEmpty() ) + { + mWebdavDirParser.listDirectory( &mWebdavConnection, mProcessRemotePath, true ); + } + else + { + mIsUploadingPath = false; + emit isUploadingPathChanged(); + } } } } @@ -495,7 +529,7 @@ void WebdavConnection::downloadPath( const QString &localPath ) QStringList remoteChildrenPath; while ( !webdavConfigurationExists ) { - remoteChildrenPath << dir.dirName(); + remoteChildrenPath.prepend( dir.dirName() ); if ( !dir.cdUp() ) break; @@ -544,7 +578,7 @@ void WebdavConnection::uploadPath( const QString &localPath ) QStringList remoteChildrenPath; while ( !webdavConfigurationExists ) { - remoteChildrenPath << dir.dirName(); + remoteChildrenPath.prepend( dir.dirName() ); if ( !dir.cdUp() ) break; @@ -572,6 +606,8 @@ void WebdavConnection::uploadPath( const QString &localPath ) } mProcessLocalPath = QDir::cleanPath( localPath ) + QDir::separator(); + mWebdavLastModified.clear(); + mLocalItems.clear(); QDirIterator it( mProcessLocalPath, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories ); while ( it.hasNext() ) @@ -598,7 +634,7 @@ void WebdavConnection::uploadPath( const QString &localPath ) double WebdavConnection::progress() const { - if ( ( mIsImportingPath || mIsDownloadingPath ) && mBytesTotal > 0 ) + if ( ( mIsImportingPath || mIsDownloadingPath || mIsUploadingPath ) && mBytesTotal > 0 ) { return static_cast( mBytesProcessed + mCurrentBytesProcessed ) / mBytesTotal; } diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h index ead76ef0bf..307b674aca 100644 --- a/src/core/webdavconnection.h +++ b/src/core/webdavconnection.h @@ -132,6 +132,7 @@ class WebdavConnection : public QObject QList mWebdavItems; QList mWebdavMkDirs; QList mLocalItems; + QList mWebdavLastModified; QString mProcessRemotePath; QString mProcessLocalPath; diff --git a/vcpkg/ports/qtwebdav/portfile.cmake b/vcpkg/ports/qtwebdav/portfile.cmake index 2e86612455..7c60277dc4 100644 --- a/vcpkg/ports/qtwebdav/portfile.cmake +++ b/vcpkg/ports/qtwebdav/portfile.cmake @@ -8,6 +8,7 @@ vcpkg_from_github( ) list(APPEND QTWEBDAV_OPTIONS -DBUILD_WITH_QT6=True) +list(APPEND QTWEBDAV_OPTIONS -DWITH_DEBUG=True) if(VCPKG_CROSSCOMPILING) list(APPEND QTWEBDAV_OPTIONS -DQT_HOST_PATH=${CURRENT_HOST_INSTALLED_DIR}) list(APPEND QTWEBDAV_OPTIONS -DQT_HOST_PATH_CMAKE_DIR:PATH=${CURRENT_HOST_INSTALLED_DIR}/share) From 2adb4ab3598e859218c616c0aacd67fb756cd251 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 17:04:45 +0700 Subject: [PATCH 28/43] Implement non-stored webdav download/upload --- src/core/webdavconnection.cpp | 127 ++++++++++++++++-------- src/core/webdavconnection.h | 5 + src/qml/QFieldLocalDataPickerScreen.qml | 100 +++++++++++++++++-- 3 files changed, 178 insertions(+), 54 deletions(-) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index c7310e5000..26f6171670 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -50,7 +50,7 @@ void WebdavConnection::setUrl( const QString &url ) void WebdavConnection::setUsername( const QString &username ) { - if ( mUrl == username ) + if ( mUsername == username ) return; mUsername = username; @@ -68,7 +68,7 @@ void WebdavConnection::setUsername( const QString &username ) void WebdavConnection::setPassword( const QString &password ) { - if ( mUrl == password ) + if ( mPassword == password ) return; mPassword = password; @@ -163,17 +163,21 @@ void WebdavConnection::applyStoredPassword() } else { - for ( const QgsAuthMethodConfig &config : configs ) + for ( QgsAuthMethodConfig &config : configs ) { - if ( config.uri() == mUrl && config.config( QStringLiteral( "username" ) ) == mUsername ) + if ( config.uri() == mUrl ) { - authManager->removeAuthenticationConfig( config.id() ); + authManager->loadAuthenticationConfig( config.id(), config, true ); + if ( config.config( QStringLiteral( "username" ) ) == mUsername ) + { + authManager->removeAuthenticationConfig( config.id() ); + } } } if ( !mStoredPassword.isEmpty() ) { - mStoredPassword = mPassword; + mStoredPassword.clear(); emit isPasswordStoredChanged(); } } @@ -546,25 +550,30 @@ void WebdavConnection::downloadPath( const QString &localPath ) QVariantMap webdavConfiguration = jsonDocument.toVariant().toMap(); setUrl( webdavConfiguration["url"].toString() ); setUsername( webdavConfiguration["username"].toString() ); - if ( isPasswordStored() ) - { - setupConnection(); + setStorePassword( isPasswordStored() ); - mProcessRemotePath = webdavConfiguration["remote_path"].toString(); - if ( !remoteChildrenPath.isEmpty() ) - { - mProcessRemotePath = mProcessRemotePath + remoteChildrenPath.join( "/" ) + QStringLiteral( "/" ); - } - mProcessLocalPath = QDir::cleanPath( localPath ) + QDir::separator(); + mProcessRemotePath = webdavConfiguration["remote_path"].toString(); + if ( !remoteChildrenPath.isEmpty() ) + { + mProcessRemotePath = mProcessRemotePath + remoteChildrenPath.join( "/" ) + QStringLiteral( "/" ); + } + mProcessLocalPath = QDir::cleanPath( localPath ) + QDir::separator(); - mWebdavItems.clear(); - mBytesProcessed = 0; - mBytesTotal = 0; - emit progressChanged(); + mWebdavItems.clear(); + mBytesProcessed = 0; + mBytesTotal = 0; + emit progressChanged(); - mIsDownloadingPath = true; - emit isDownloadingPathChanged(); + mIsDownloadingPath = true; + emit isDownloadingPathChanged(); + if ( !isPasswordStored() ) + { + emit passwordRequested(); + } + else + { + setupConnection(); mWebdavDirParser.listDirectory( &mWebdavConnection, mProcessRemotePath, true ); } } @@ -595,43 +604,73 @@ void WebdavConnection::uploadPath( const QString &localPath ) QVariantMap webdavConfiguration = jsonDocument.toVariant().toMap(); setUrl( webdavConfiguration["url"].toString() ); setUsername( webdavConfiguration["username"].toString() ); - if ( isPasswordStored() ) - { - setupConnection(); + setStorePassword( isPasswordStored() ); - mProcessRemotePath = webdavConfiguration["remote_path"].toString(); - if ( !remoteChildrenPath.isEmpty() ) - { - mProcessRemotePath = mProcessRemotePath + remoteChildrenPath.join( "/" ) + QStringLiteral( "/" ); - } - mProcessLocalPath = QDir::cleanPath( localPath ) + QDir::separator(); + mProcessRemotePath = webdavConfiguration["remote_path"].toString(); + if ( !remoteChildrenPath.isEmpty() ) + { + mProcessRemotePath = mProcessRemotePath + remoteChildrenPath.join( "/" ) + QStringLiteral( "/" ); + } + mProcessLocalPath = QDir::cleanPath( localPath ) + QDir::separator(); - mWebdavLastModified.clear(); + mWebdavLastModified.clear(); - mLocalItems.clear(); - QDirIterator it( mProcessLocalPath, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories ); - while ( it.hasNext() ) + mLocalItems.clear(); + QDirIterator it( mProcessLocalPath, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories ); + while ( it.hasNext() ) + { + it.next(); + if ( it.fileName() != QStringLiteral( "qfield_webdav_configuration.json" ) ) { - it.next(); - if ( it.fileName() != QStringLiteral( "qfield_webdav_configuration.json" ) ) - { - mLocalItems << it.fileInfo(); - } + mLocalItems << it.fileInfo(); } + } - mBytesProcessed = 0; - mBytesTotal = 0; - emit progressChanged(); + mBytesProcessed = 0; + mBytesTotal = 0; + emit progressChanged(); - mIsUploadingPath = true; - emit isUploadingPathChanged(); + mIsUploadingPath = true; + emit isUploadingPathChanged(); + if ( !isPasswordStored() ) + { + emit passwordRequested(); + } + else + { + setupConnection(); mWebdavDirParser.listDirectory( &mWebdavConnection, mProcessRemotePath, true ); } } } } +void WebdavConnection::answerPasswordRequest( const QString &password ) +{ + setPassword( password ); + + if ( mIsDownloadingPath || mIsUploadingPath ) + { + setupConnection(); + mWebdavDirParser.listDirectory( &mWebdavConnection, mProcessRemotePath, true ); + } +} + +void WebdavConnection::cancelPasswordRequest() +{ + if ( mIsDownloadingPath ) + { + mIsDownloadingPath = false; + emit isDownloadingPathChanged(); + } + else if ( mIsUploadingPath ) + { + mIsUploadingPath = false; + emit isUploadingPathChanged(); + } +} + double WebdavConnection::progress() const { if ( ( mIsImportingPath || mIsDownloadingPath || mIsUploadingPath ) && mBytesTotal > 0 ) diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h index 307b674aca..9f83aa532d 100644 --- a/src/core/webdavconnection.h +++ b/src/core/webdavconnection.h @@ -89,6 +89,9 @@ class WebdavConnection : public QObject Q_INVOKABLE void downloadPath( const QString &localPath ); Q_INVOKABLE void uploadPath( const QString &localPath ); + Q_INVOKABLE void answerPasswordRequest( const QString &password ); + Q_INVOKABLE void cancelPasswordRequest(); + signals: void urlChanged(); void usernameChanged(); @@ -103,6 +106,8 @@ class WebdavConnection : public QObject void progressChanged(); void lastErrorChanged(); + void passwordRequested(); + private slots: void processDirParserFinished(); void processConnectionError( const QString &error ); diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index d589b2d9de..ae1d349c85 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -724,7 +724,7 @@ Page { QfDialog { id: importUrlDialog title: "Import URL" - focus: true + focus: visible y: (mainWindow.height - height - 80) / 2 onAboutToShow: { @@ -768,11 +768,6 @@ Page { WebdavConnection { id: webdavConnection - url: importWebdavUrlInput.text - username: importWebdavUserInput.text - password: importWebdavPasswordInput.text - storePassword: importWebdavStorePasswordCheck.checked - onIsImportingPathChanged: { if (isImportingPath) { busyOverlay.text = qsTr("Importing WebDAV folder"); @@ -813,8 +808,9 @@ Page { displayToast(qsTr("WebDAV error: ") + lastError); } - onIsPasswordStoredChanged: { - console.log(isPasswordStored ? "stored" : "not stored"); + onPasswordRequested: { + enterWebdavPasswordDialog.open(); + enterWebdavPasswordInput.focus = true; } } } @@ -825,12 +821,69 @@ Page { sourceComponent: webdavConnectionComponent } + QfDialog { + id: enterWebdavPasswordDialog + title: "Enter WebDAV password" + focus: true + y: (mainWindow.height - height - 80) / 2 + + Column { + width: childrenRect.width + height: childrenRect.height + spacing: 10 + + TextMetrics { + id: enterWebdavPasswordLabelMetrics + font: enterWebdavPasswordLabel.font + text: enterWebdavPasswordLabel.text + } + + Label { + id: enterWebdavPasswordLabel + width: mainWindow.width - 60 < enterWebdavPasswordLabelMetrics.width ? mainWindow.width - 60 : enterWebdavPasswordLabelMetrics.width + text: qsTr("Type the password for user %1 on server %2:").arg((webdavConnectionLoader.item ? webdavConnectionLoader.item.username : '')).arg((webdavConnectionLoader.item ? webdavConnectionLoader.item.url : '')) + wrapMode: Text.WordWrap + font: Theme.defaultFont + color: Theme.mainTextColor + } + + TextField { + id: enterWebdavPasswordInput + enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths + width: importWebdavUrlLabel.width + placeholderText: qsTr("Password") + echoMode: TextInput.Password + } + } + + onAccepted: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.answerPasswordRequest(enterWebdavPasswordInput.text); + } + } + + onDiscarded: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.cancelPasswordRequest(); + } + } + } + QfDialog { id: importWebdavDialog title: "Import WebDAV folder" - focus: true + focus: visible y: (mainWindow.height - height - 80) / 2 + onAboutToShow: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.url = importWebdavUrlInput.text; + webdavConnectionLoader.item.username = importWebdavUserInput.text; + webdavConnectionLoader.item.password = importWebdavPasswordInput.text; + webdavConnectionLoader.item.storePassword = importWebdavStorePasswordCheck.checked; + } + } + Column { width: childrenRect.width height: childrenRect.height @@ -856,6 +909,12 @@ Page { enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width placeholderText: qsTr("WebDAV server URL") + + onDisplayTextChanged: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.url = displayText; + } + } } TextField { @@ -863,6 +922,12 @@ Page { enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width placeholderText: qsTr("User") + + onDisplayTextChanged: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.username = displayText; + } + } } TextField { @@ -871,6 +936,12 @@ Page { width: importWebdavUrlLabel.width placeholderText: text === "" && webdavConnectionLoader.item && webdavConnectionLoader.item.isPasswordStored ? qsTr("Password (leave empty to use remembered)") : qsTr("Password") echoMode: TextInput.Password + + onDisplayTextChanged: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.password = text; + } + } } CheckBox { @@ -880,6 +951,11 @@ Page { text: qsTr('Remember password') font: Theme.defaultFont checked: true + + onCheckedChanged: { + if (webdavConnectionLoader.item) { + } + } } Row { @@ -925,7 +1001,11 @@ Page { } onAccepted: { - if (importWebdavPathInput.displayText !== '') { + if (importWebdavPathInput.displayText !== '' && webdavConnectionLoader.item) { + webdavConnectionLoader.item.url = importWebdavUrlInput.text; + webdavConnectionLoader.item.username = importWebdavUserInput.text; + webdavConnectionLoader.item.password = importWebdavPasswordInput.text; + webdavConnectionLoader.item.storePassword = importWebdavStorePasswordCheck.checked; webdavConnectionLoader.item.importPath(importWebdavPathInput.displayText, platformUtilities.applicationDirectory() + "Imported Projects/"); } } From 84c4e8a35b2432e1fca54922b9c9ad6480068dbf Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 17:09:44 +0700 Subject: [PATCH 29/43] Cleanup QtWebDAV port by updating to 2.1 --- vcpkg/ports/qtwebdav/debug.patch | 28 ---------------------------- vcpkg/ports/qtwebdav/fix.patch | 12 ------------ vcpkg/ports/qtwebdav/portfile.cmake | 6 ++---- vcpkg/ports/qtwebdav/vcpkg.json | 2 +- 4 files changed, 3 insertions(+), 45 deletions(-) delete mode 100644 vcpkg/ports/qtwebdav/debug.patch delete mode 100644 vcpkg/ports/qtwebdav/fix.patch diff --git a/vcpkg/ports/qtwebdav/debug.patch b/vcpkg/ports/qtwebdav/debug.patch deleted file mode 100644 index 1a9eb8b170..0000000000 --- a/vcpkg/ports/qtwebdav/debug.patch +++ /dev/null @@ -1,28 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 36bbf15..c0b691f 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -7,6 +7,7 @@ include(GenerateExportHeader) - option(BUILD_EXAMPLE "Build with example" OFF) - option(BUILD_WITH_QT6 "Explicitly build with Qt6" OFF) - option(WITH_EXTENDED_PROPERTIES "Build with extended properties for WedDAV items" OFF) -+option(WITH_DEBUG "Build with debug information" OFF) - - # Determine Qt version to use - if (BUILD_WITH_QT6) -@@ -60,10 +61,13 @@ target_include_directories(QtWebDAV PUBLIC - $ - $ - ) --target_compile_definitions(QtWebDAV PRIVATE DEBUG_WEBDAV) -+ -+if (WITH_DEBUG) -+ target_compile_definitions(QtWebDAV PRIVATE DEBUG_WEBDAV) -+endif() - - if (WITH_EXTENDED_PROPERTIES) -- target_compile_definitions(QtWebDAV PRIVATE -DQWEBDAVITEM_EXTENDED_PROPERTIES) -+ target_compile_definitions(QtWebDAV PRIVATE QWEBDAVITEM_EXTENDED_PROPERTIES) - endif() - - # Link Qt libraries diff --git a/vcpkg/ports/qtwebdav/fix.patch b/vcpkg/ports/qtwebdav/fix.patch deleted file mode 100644 index 232f73014a..0000000000 --- a/vcpkg/ports/qtwebdav/fix.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 6164046..36bbf15 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -28,7 +28,6 @@ set(QtWebDAV_SOURCES - set(QtWebDAV_HEADERS - qnaturalsort.h - qwebdav.h -- qwebdav_global.h - qwebdavdirparser.h - qwebdavitem.h - ) diff --git a/vcpkg/ports/qtwebdav/portfile.cmake b/vcpkg/ports/qtwebdav/portfile.cmake index 7c60277dc4..71bd6d8a1a 100644 --- a/vcpkg/ports/qtwebdav/portfile.cmake +++ b/vcpkg/ports/qtwebdav/portfile.cmake @@ -1,14 +1,12 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO m-kuhn/QtWebDAV - REF 6363cabf99368bb8a659af63584ac790f04cc9c6 - SHA512 059e99181fdd751b4598986997721b9e0e7debccbad7f3a258cba2745bd4b28896611566017c50a7bb9ace523bca388551a327b17f76968032c8a6e9a6bb2d34 + REF "v${VERSION}" + SHA512 a4025a36090fb84647eae8acea83df82cdb46a5254f7cd0dcbbf9dccbf9bdeb68c4cec47ae3c34f5a67590553a7c6c05bf0e632e44ab6a6d16435a6f6b0d6a74 HEAD_REF main - PATCHES fix.patch debug.patch ) list(APPEND QTWEBDAV_OPTIONS -DBUILD_WITH_QT6=True) -list(APPEND QTWEBDAV_OPTIONS -DWITH_DEBUG=True) if(VCPKG_CROSSCOMPILING) list(APPEND QTWEBDAV_OPTIONS -DQT_HOST_PATH=${CURRENT_HOST_INSTALLED_DIR}) list(APPEND QTWEBDAV_OPTIONS -DQT_HOST_PATH_CMAKE_DIR:PATH=${CURRENT_HOST_INSTALLED_DIR}/share) diff --git a/vcpkg/ports/qtwebdav/vcpkg.json b/vcpkg/ports/qtwebdav/vcpkg.json index f55102ac0c..869da951fa 100644 --- a/vcpkg/ports/qtwebdav/vcpkg.json +++ b/vcpkg/ports/qtwebdav/vcpkg.json @@ -1,6 +1,6 @@ { "name": "qtwebdav", - "version-string": "dev", + "version-string": "2.1", "description": " Qt library for WebDAV with support for HTTP/HTTPS.", "homepage": "https://github.com/PikachuHy/QtWebDAV", "dependencies": [ From 3d03800f42919e4d29b3971b8842b177cbbdff89 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 17:19:14 +0700 Subject: [PATCH 30/43] Relocate the component into the loader --- src/qml/QFieldLocalDataPickerScreen.qml | 88 ++++++++++++------------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index ae1d349c85..2828b95914 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -762,65 +762,61 @@ Page { } } - Component { - id: webdavConnectionComponent - - WebdavConnection { - id: webdavConnection - - onIsImportingPathChanged: { - if (isImportingPath) { - busyOverlay.text = qsTr("Importing WebDAV folder"); - busyOverlay.progress = 0; - busyOverlay.state = "visible"; - } else { - busyOverlay.state = "hidden"; + Loader { + id: webdavConnectionLoader + active: qfieldLocalDataPickerScreen.openedOnce + sourceComponent: Component { + WebdavConnection { + id: webdavConnection + + onIsImportingPathChanged: { + if (isImportingPath) { + busyOverlay.text = qsTr("Importing WebDAV folder"); + busyOverlay.progress = 0; + busyOverlay.state = "visible"; + } else { + busyOverlay.state = "hidden"; + } } - } - onIsDownloadingPathChanged: { - if (isDownloadingPath) { - busyOverlay.text = qsTr("Downloading WebDAV folder"); - busyOverlay.progress = 0; - busyOverlay.state = "visible"; - } else { - busyOverlay.state = "hidden"; + onIsDownloadingPathChanged: { + if (isDownloadingPath) { + busyOverlay.text = qsTr("Downloading WebDAV folder"); + busyOverlay.progress = 0; + busyOverlay.state = "visible"; + } else { + busyOverlay.state = "hidden"; + } } - } - onIsUploadingPathChanged: { - if (isUploadingPath) { - busyOverlay.text = qsTr("Uploading WebDAV folder"); - busyOverlay.progress = 0; - busyOverlay.state = "visible"; - } else { - busyOverlay.state = "hidden"; + onIsUploadingPathChanged: { + if (isUploadingPath) { + busyOverlay.text = qsTr("Uploading WebDAV folder"); + busyOverlay.progress = 0; + busyOverlay.state = "visible"; + } else { + busyOverlay.state = "hidden"; + } } - } - onProgressChanged: { - if (isImportingPath || isDownloadingPath || isUploadingPath) { - busyOverlay.progress = progress; + onProgressChanged: { + if (isImportingPath || isDownloadingPath || isUploadingPath) { + busyOverlay.progress = progress; + } } - } - onLastErrorChanged: { - displayToast(qsTr("WebDAV error: ") + lastError); - } + onLastErrorChanged: { + displayToast(qsTr("WebDAV error: ") + lastError); + } - onPasswordRequested: { - enterWebdavPasswordDialog.open(); - enterWebdavPasswordInput.focus = true; + onPasswordRequested: { + enterWebdavPasswordDialog.open(); + enterWebdavPasswordInput.focus = true; + } } } } - Loader { - id: webdavConnectionLoader - active: qfieldLocalDataPickerScreen.openedOnce - sourceComponent: webdavConnectionComponent - } - QfDialog { id: enterWebdavPasswordDialog title: "Enter WebDAV password" From 8da781457f7ff557154976b557c0b6e14f203496 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 17:52:00 +0700 Subject: [PATCH 31/43] Implement a webdav upload/download of project content --- src/core/localfilesmodel.cpp | 10 ++---- src/core/utils/fileutils.cpp | 6 ++++ src/core/utils/fileutils.h | 6 ++-- src/core/webdavconnection.cpp | 14 ++++++++ src/core/webdavconnection.h | 2 ++ src/qml/QFieldLocalDataPickerScreen.qml | 47 ++++++++++++++++++++++++- 6 files changed, 74 insertions(+), 11 deletions(-) diff --git a/src/core/localfilesmodel.cpp b/src/core/localfilesmodel.cpp index 24a932fe39..a57df499b6 100644 --- a/src/core/localfilesmodel.cpp +++ b/src/core/localfilesmodel.cpp @@ -18,6 +18,7 @@ #include "platformutilities.h" #include "qfieldcloudutils.h" #include "qgismobileapp.h" +#include "webdavconnection.h" #include #include @@ -336,14 +337,7 @@ QVariant LocalFilesModel::data( const QModelIndex &index, int role ) const case ItemHasWebdavConfigurationRole: { - const QFileInfo fileInfo( mItems[index.row()].path ); - QDir dir( fileInfo.isFile() ? fileInfo.absolutePath() : fileInfo.absoluteFilePath() ); - bool webdavConfigurationExists = dir.exists( "qfield_webdav_configuration.json" ); - while ( !webdavConfigurationExists && dir.cdUp() ) - { - webdavConfigurationExists = dir.exists( "qfield_webdav_configuration.json" ); - } - return webdavConfigurationExists; + return WebdavConnection::hasWebdavConfiguration( mItems[index.row()].path ); } } diff --git a/src/core/utils/fileutils.cpp b/src/core/utils/fileutils.cpp index e2908e2f19..072e68a341 100644 --- a/src/core/utils/fileutils.cpp +++ b/src/core/utils/fileutils.cpp @@ -50,6 +50,12 @@ bool FileUtils::isImageMimeTypeSupported( const QString &mimeType ) return QImageReader::supportedMimeTypes().contains( mimeType.toLatin1() ); } +QString FileUtils::absolutePath( const QString &filePath ) +{ + QFileInfo fileInfo( filePath ); + return fileInfo.absolutePath(); +} + QString FileUtils::fileName( const QString &filePath ) { QFileInfo fileInfo( filePath ); diff --git a/src/core/utils/fileutils.h b/src/core/utils/fileutils.h index bb979dfb98..248ae8c9e8 100644 --- a/src/core/utils/fileutils.h +++ b/src/core/utils/fileutils.h @@ -40,14 +40,16 @@ class QFIELD_CORE_EXPORT FileUtils : public QObject Q_INVOKABLE static QString mimeTypeName( const QString &filePath ); //! Returns TRUE if the provided mimetype is a supported image Q_INVOKABLE static bool isImageMimeTypeSupported( const QString &mimeType ); - //! Returns the filename of a filepath - if no file name exists it's empty + //! Returns the filename of a \a filePath - if no file name exists it's empty Q_INVOKABLE static QString fileName( const QString &filePath ); - //! Returns true if the file exists (false if it's a directory) + //! Returns true if the \a filePath exists (false if it's a directory) Q_INVOKABLE static bool fileExists( const QString &filePath ); //! Returns the suffix (extension) Q_INVOKABLE static QString fileSuffix( const QString &filePath ); //! Returns a human-friendly size from bytes Q_INVOKABLE static QString representFileSize( qint64 bytes ); + //! Returns the absolute path of tghe folder containing the \a filePath. + Q_INVOKABLE static QString absolutePath( const QString &filePath ); /** * Insures that a given image's width and height are restricted to a maximum size. diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index 26f6171670..faea6aa379 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -616,6 +616,7 @@ void WebdavConnection::uploadPath( const QString &localPath ) mWebdavLastModified.clear(); mLocalItems.clear(); + qDebug() << mProcessLocalPath; QDirIterator it( mProcessLocalPath, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories ); while ( it.hasNext() ) { @@ -639,6 +640,7 @@ void WebdavConnection::uploadPath( const QString &localPath ) } else { + qDebug() << mLocalItems; setupConnection(); mWebdavDirParser.listDirectory( &mWebdavConnection, mProcessRemotePath, true ); } @@ -692,3 +694,15 @@ void WebdavConnection::processDirParserError( const QString &error ) mLastError = error; emit lastErrorChanged(); } + +bool WebdavConnection::hasWebdavConfiguration( const QString &path ) +{ + const QFileInfo fileInfo( path ); + QDir dir( fileInfo.isFile() ? fileInfo.absolutePath() : fileInfo.absoluteFilePath() ); + bool webdavConfigurationExists = dir.exists( "qfield_webdav_configuration.json" ); + while ( !webdavConfigurationExists && dir.cdUp() ) + { + webdavConfigurationExists = dir.exists( "qfield_webdav_configuration.json" ); + } + return webdavConfigurationExists; +} diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h index 9f83aa532d..1a67b8511a 100644 --- a/src/core/webdavconnection.h +++ b/src/core/webdavconnection.h @@ -92,6 +92,8 @@ class WebdavConnection : public QObject Q_INVOKABLE void answerPasswordRequest( const QString &password ); Q_INVOKABLE void cancelPasswordRequest(); + Q_INVOKABLE static bool hasWebdavConfiguration( const QString &path ); + signals: void urlChanged(); void usernameChanged(); diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index 2828b95914..45e1a9569b 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -330,7 +330,8 @@ Page { round: true // Since the project menu only has one action for now, hide if PlatformUtilities.UpdateProjectFromArchive is missing - property bool isLocalProject: qgisProject && QFieldCloudUtils.getProjectId(qgisProject.fileName) === '' && (projectInfo.filePath.endsWith('.qgs') || projectInfo.filePath.endsWith('.qgz')) && platformUtilities.capabilities & PlatformUtilities.UpdateProjectFromArchive + property bool isLocalProject: qgisProject && QFieldCloudUtils.getProjectId(qgisProject.fileName) === '' && (projectInfo.filePath.endsWith('.qgs') || projectInfo.filePath.endsWith('.qgz')) + property bool isLocalProjectActionAvailable: updateProjectFromArchive.enabled || uploadProjectToWebdav.enabled visible: (projectFolderView && isLocalProject && table.model.currentDepth === 1) || table.model.currentPath === 'root' anchors.bottom: parent.bottom @@ -718,6 +719,44 @@ Page { platformUtilities.updateProjectFromArchive(projectInfo.filePath); } } + + MenuItem { + id: uploadProjectToWebdav + + enabled: webdavConnectionLoader.item ? webdavConnectionLoader.item.hasWebdavConfiguration(FileUtils.absolutePath(projectInfo.filePath)) : false + visible: enabled + font: Theme.defaultFont + width: parent.width + height: enabled ? undefined : 0 + leftPadding: Theme.menuItemLeftPadding + + text: qsTr("Upload project to WebDAV") + onTriggered: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.uploadPath(FileUtils.absolutePath(projectInfo.filePath)); + } + } + } + + MenuItem { + id: downloadProjectToWebdav + + enabled: uploadProjectToWebdav.enabled + visible: enabled + font: Theme.defaultFont + width: parent.width + height: enabled ? undefined : 0 + leftPadding: Theme.menuItemLeftPadding + + text: qsTr("Download project from WebDAV") + onTriggered: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.openedProjectPath = projectInfo.filePath; + iface.clearProject(); + webdavConnectionLoader.item.downloadPath(FileUtils.absolutePath(projectInfo.filePath)); + } + } + } } } @@ -769,6 +808,8 @@ Page { WebdavConnection { id: webdavConnection + property string openedProjectPath: "" + onIsImportingPathChanged: { if (isImportingPath) { busyOverlay.text = qsTr("Importing WebDAV folder"); @@ -786,6 +827,10 @@ Page { busyOverlay.state = "visible"; } else { busyOverlay.state = "hidden"; + if (openedProjectPath) { + iface.loadFile(openedProjectPath); + openedProjectPath = ""; + } } } From ea5772dc7940f3733c70cf7e063ba514ae77828c Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 17:54:07 +0700 Subject: [PATCH 32/43] Remove some ... when not relevant --- src/qml/QFieldLocalDataPickerScreen.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index 45e1a9569b..6e4851f6ed 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -405,7 +405,7 @@ Page { height: enabled ? undefined : 0 leftPadding: Theme.menuItemLeftPadding - text: qsTr("Push to QFieldCloud...") + text: qsTr("Push to QFieldCloud") onTriggered: { QFieldCloudUtils.addPendingAttachment(cloudProjectsModel.currentProjectId, itemMenu.itemPath); platformUtilities.uploadPendingAttachments(cloudConnection); @@ -499,7 +499,7 @@ Page { height: enabled ? undefined : 0 leftPadding: Theme.menuItemLeftPadding - text: qsTr("Upload folder to WebDAV server...") + text: qsTr("Upload folder to WebDAV server") onTriggered: { if (webdavConnectionLoader.item) { webdavConnectionLoader.item.uploadPath(itemMenu.itemPath); @@ -517,7 +517,7 @@ Page { height: enabled ? undefined : 0 leftPadding: Theme.menuItemLeftPadding - text: qsTr("Download folder from WebDAV server...") + text: qsTr("Download folder from WebDAV server") onTriggered: { if (webdavConnectionLoader.item) { webdavConnectionLoader.item.downloadPath(itemMenu.itemPath); From a43e1c9dd0f1b13efd7652ebc533385dbea0f786 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 5 Jan 2025 18:31:53 +0700 Subject: [PATCH 33/43] Let's be conservative and ask for people to confirm download/upload operation, makes for a nicer password UX too --- src/core/webdavconnection.cpp | 31 +++--------- src/core/webdavconnection.h | 6 +-- src/qml/QFieldLocalDataPickerScreen.qml | 65 +++++++++++++++++-------- 3 files changed, 53 insertions(+), 49 deletions(-) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index faea6aa379..66cc33b9af 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -566,16 +566,8 @@ void WebdavConnection::downloadPath( const QString &localPath ) mIsDownloadingPath = true; emit isDownloadingPathChanged(); - - if ( !isPasswordStored() ) - { - emit passwordRequested(); - } - else - { - setupConnection(); - mWebdavDirParser.listDirectory( &mWebdavConnection, mProcessRemotePath, true ); - } + const QUrl url( mUrl ); + emit confirmationRequested( url.host(), mUsername ); } } } @@ -633,25 +625,14 @@ void WebdavConnection::uploadPath( const QString &localPath ) mIsUploadingPath = true; emit isUploadingPathChanged(); - - if ( !isPasswordStored() ) - { - emit passwordRequested(); - } - else - { - qDebug() << mLocalItems; - setupConnection(); - mWebdavDirParser.listDirectory( &mWebdavConnection, mProcessRemotePath, true ); - } + const QUrl url( mUrl ); + emit confirmationRequested( url.host(), mUsername ); } } } -void WebdavConnection::answerPasswordRequest( const QString &password ) +void WebdavConnection::confirmRequest() { - setPassword( password ); - if ( mIsDownloadingPath || mIsUploadingPath ) { setupConnection(); @@ -659,7 +640,7 @@ void WebdavConnection::answerPasswordRequest( const QString &password ) } } -void WebdavConnection::cancelPasswordRequest() +void WebdavConnection::cancelRequest() { if ( mIsDownloadingPath ) { diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h index 1a67b8511a..4760007364 100644 --- a/src/core/webdavconnection.h +++ b/src/core/webdavconnection.h @@ -89,8 +89,8 @@ class WebdavConnection : public QObject Q_INVOKABLE void downloadPath( const QString &localPath ); Q_INVOKABLE void uploadPath( const QString &localPath ); - Q_INVOKABLE void answerPasswordRequest( const QString &password ); - Q_INVOKABLE void cancelPasswordRequest(); + Q_INVOKABLE void confirmRequest(); + Q_INVOKABLE void cancelRequest(); Q_INVOKABLE static bool hasWebdavConfiguration( const QString &path ); @@ -108,7 +108,7 @@ class WebdavConnection : public QObject void progressChanged(); void lastErrorChanged(); - void passwordRequested(); + void confirmationRequested( const QString &host, const QString &username ); private slots: void processDirParserFinished(); diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index 6e4851f6ed..d3e4b3eb34 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -854,58 +854,86 @@ Page { displayToast(qsTr("WebDAV error: ") + lastError); } - onPasswordRequested: { - enterWebdavPasswordDialog.open(); - enterWebdavPasswordInput.focus = true; + onConfirmationRequested: (host, username) => { + downloadUploadWebdavDialog.isUploadingPath = isUploadingPath; + downloadUploadWebdavDialog.host = host; + downloadUploadWebdavDialog.username = username; + downloadUploadWebdavDialog.open(); } } } } QfDialog { - id: enterWebdavPasswordDialog - title: "Enter WebDAV password" + id: downloadUploadWebdavDialog + title: isUploadingPath ? qsTr("WebDAV upload") : qsTr("WebDAV download") focus: true y: (mainWindow.height - height - 80) / 2 + property bool isUploadingPath: false + property string host: "" + property string username: "" + + onAboutToShow: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.password = downloadUploadWebdavPasswordInput.text; + webdavConnectionLoader.item.storePassword = downloadUploadWebdavPasswordCheck.checked; + } + } + Column { width: childrenRect.width height: childrenRect.height spacing: 10 TextMetrics { - id: enterWebdavPasswordLabelMetrics - font: enterWebdavPasswordLabel.font - text: enterWebdavPasswordLabel.text + id: downloadUploadWebdavIntroMetrics + font: downloadUploadWebdavIntroLabel.font + text: downloadUploadWebdavIntroLabel.text } Label { - id: enterWebdavPasswordLabel - width: mainWindow.width - 60 < enterWebdavPasswordLabelMetrics.width ? mainWindow.width - 60 : enterWebdavPasswordLabelMetrics.width - text: qsTr("Type the password for user %1 on server %2:").arg((webdavConnectionLoader.item ? webdavConnectionLoader.item.username : '')).arg((webdavConnectionLoader.item ? webdavConnectionLoader.item.url : '')) + id: downloadUploadWebdavIntroLabel + width: mainWindow.width - 60 < downloadUploadWebdavIntroMetrics.width ? mainWindow.width - 60 : downloadUploadWebdavIntroMetrics.width + text: downloadUploadWebdavDialog.isUploadingPath ? qsTr("You are about to upload modified content into %1 using user %2.

This operation will overwrite data stored remotely, make sure this is what you want to do.").arg(downloadUploadWebdavDialog.host).arg(downloadUploadWebdavDialog.username) : qsTr("You are about to download modified content from %1 using user %2.

This operation will overwrite data stored locally, make sure this is what you want to do.").arg(downloadUploadWebdavDialog.host).arg(downloadUploadWebdavDialog.username) wrapMode: Text.WordWrap font: Theme.defaultFont color: Theme.mainTextColor } TextField { - id: enterWebdavPasswordInput + id: downloadUploadWebdavPasswordInput enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width - placeholderText: qsTr("Password") + placeholderText: text === "" && webdavConnectionLoader.item && webdavConnectionLoader.item.isPasswordStored ? qsTr("Password (leave empty to use remembered)") : qsTr("Password") echoMode: TextInput.Password + + onDisplayTextChanged: { + if (webdavConnectionLoader.item) { + webdavConnectionLoader.item.password = text; + } + } + } + + CheckBox { + id: downloadUploadWebdavPasswordCheck + width: importWebdavUrlLabel.width + enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths + text: qsTr('Remember password') + font: Theme.defaultFont + checked: true } } onAccepted: { if (webdavConnectionLoader.item) { - webdavConnectionLoader.item.answerPasswordRequest(enterWebdavPasswordInput.text); + webdavConnectionLoader.item.confirmRequest(); } } - onDiscarded: { + onRejected: { if (webdavConnectionLoader.item) { - webdavConnectionLoader.item.cancelPasswordRequest(); + webdavConnectionLoader.item.cancelRequest(); } } } @@ -992,11 +1020,6 @@ Page { text: qsTr('Remember password') font: Theme.defaultFont checked: true - - onCheckedChanged: { - if (webdavConnectionLoader.item) { - } - } } Row { From 4527232c8143010701c6ff6202c17d6f21719528 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Fri, 10 Jan 2025 18:20:20 +0700 Subject: [PATCH 34/43] Papercut fix: open the imported webdav folder when finished for a much nicer UX --- src/core/webdavconnection.cpp | 1 + src/core/webdavconnection.h | 2 ++ src/qml/QFieldLocalDataPickerScreen.qml | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index 66cc33b9af..738fb569aa 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -421,6 +421,7 @@ void WebdavConnection::getWebdavItems() mIsImportingPath = false; emit isImportingPathChanged(); + emit importSuccessful( mProcessLocalPath ); } else if ( mIsDownloadingPath ) { diff --git a/src/core/webdavconnection.h b/src/core/webdavconnection.h index 4760007364..601aa10aae 100644 --- a/src/core/webdavconnection.h +++ b/src/core/webdavconnection.h @@ -110,6 +110,8 @@ class WebdavConnection : public QObject void confirmationRequested( const QString &host, const QString &username ); + void importSuccessful( const QString &path ); + private slots: void processDirParserFinished(); void processConnectionError( const QString &error ); diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index d3e4b3eb34..ac20c1c07e 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -860,6 +860,10 @@ Page { downloadUploadWebdavDialog.username = username; downloadUploadWebdavDialog.open(); } + + onImportSuccessful: path => { + table.model.currentPath = path; + } } } } From 13796a44339de0569dc4c881c59beafe3c2bec6d Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 11 Jan 2025 11:30:09 +0700 Subject: [PATCH 35/43] Add a refresh folders button within the import webdav folder dialog --- src/qml/QFieldLocalDataPickerScreen.qml | 52 ++++++++++++++++--------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index ac20c1c07e..cd1d3d60bf 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -1026,12 +1026,22 @@ Page { checked: true } + Label { + width: importWebdavUrlLabel.width + visible: importWebdavPathInput.visible + text: qsTr("Select the remote folder to import:") + wrapMode: Text.WordWrap + font: Theme.defaultFont + color: Theme.mainTextColor + } + Row { - visible: !webdavConnectionLoader.item || webdavConnectionLoader.item.availablePaths.length === 0 spacing: 5 QfButton { id: importWebdavFetchFoldersButton + anchors.verticalCenter: importWebdavPathInput.verticalCenter + visible: !webdavConnectionLoader.item || webdavConnectionLoader.item.availablePaths.length === 0 enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width - (importWebdavFetchFoldersIndicator.visible ? importWebdavFetchFoldersIndicator.width + 5 : 0) text: !enabled ? qsTr("Fetching remote folders") : qsTr("Fetch remote folders") @@ -1041,31 +1051,37 @@ Page { } } + ComboBox { + id: importWebdavPathInput + width: importWebdavUrlLabel.width - (importWebdavRefetchFoldersButton.width + 5) - (importWebdavFetchFoldersIndicator.visible ? importWebdavFetchFoldersIndicator.width + 5 : 0) + visible: webdavConnectionLoader.item && webdavConnectionLoader.item.availablePaths.length > 0 + enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths + model: [''].concat(webdavConnectionLoader.item ? webdavConnectionLoader.item.availablePaths : []) + } + + QfToolButton { + id: importWebdavRefetchFoldersButton + anchors.verticalCenter: importWebdavPathInput.verticalCenter + visible: importWebdavPathInput.visible + enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths + bgcolor: "transparent" + iconSource: Theme.getThemeVectorIcon("refresh_24dp") + iconColor: enabled ? Theme.mainTextColor : Theme.mainTextDisabledColor + + onClicked: { + webdavConnectionLoader.item.fetchAvailablePaths(); + } + } + BusyIndicator { id: importWebdavFetchFoldersIndicator - anchors.verticalCenter: importWebdavFetchFoldersButton.verticalCenter + anchors.verticalCenter: importWebdavPathInput.verticalCenter width: 48 height: 48 visible: webdavConnectionLoader.item && webdavConnectionLoader.item.isFetchingAvailablePaths running: visible } } - - Label { - width: importWebdavUrlLabel.width - visible: importWebdavPathInput.visible - text: qsTr("Select the remote folder to import:") - wrapMode: Text.WordWrap - font: Theme.defaultFont - color: Theme.mainTextColor - } - - ComboBox { - id: importWebdavPathInput - width: importWebdavUrlLabel.width - visible: webdavConnectionLoader.item && webdavConnectionLoader.item.availablePaths.length > 0 - model: [''].concat(webdavConnectionLoader.item ? webdavConnectionLoader.item.availablePaths : []) - } } onAccepted: { From 8607ed72388f30391607b70fe99c012183df96b4 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 11 Jan 2025 11:43:15 +0700 Subject: [PATCH 36/43] Add a show pasword button in the import webdav folder and download/upload folder dialogs --- src/qml/QFieldLocalDataPickerScreen.qml | 52 ++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index cd1d3d60bf..e13c7c8b05 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -908,7 +908,8 @@ Page { TextField { id: downloadUploadWebdavPasswordInput enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths - width: importWebdavUrlLabel.width + width: downloadUploadWebdavIntroLabel.width + rightPadding: leftPadding + (downloadUploadWebdavShowPasswordInput.width - leftPadding) placeholderText: text === "" && webdavConnectionLoader.item && webdavConnectionLoader.item.isPasswordStored ? qsTr("Password (leave empty to use remembered)") : qsTr("Password") echoMode: TextInput.Password @@ -917,11 +918,34 @@ Page { webdavConnectionLoader.item.password = text; } } + + QfToolButton { + id: downloadUploadWebdavShowPasswordInput + + property int originalEchoMode: TextInput.Normal + + visible: (!!parent.echoMode && parent.echoMode !== TextInput.Normal) || originalEchoMode !== TextInput.Normal + iconSource: parent.echoMode === TextInput.Normal ? Theme.getThemeVectorIcon('ic_hide_green_48dp') : Theme.getThemeVectorIcon('ic_show_green_48dp') + iconColor: Theme.mainColor + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + opacity: parent.text.length > 0 ? 1 : 0.25 + z: 1 + + onClicked: { + if (parent.echoMode !== TextInput.Normal) { + originalEchoMode = parent.echoMode; + parent.echoMode = TextInput.Normal; + } else { + parent.echoMode = originalEchoMode; + } + } + } } CheckBox { id: downloadUploadWebdavPasswordCheck - width: importWebdavUrlLabel.width + width: downloadUploadWebdavIntroLabel.width enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths text: qsTr('Remember password') font: Theme.defaultFont @@ -1007,6 +1031,7 @@ Page { id: importWebdavPasswordInput enabled: !webdavConnectionLoader.item || !webdavConnectionLoader.item.isFetchingAvailablePaths width: importWebdavUrlLabel.width + rightPadding: leftPadding + (importWebdavShowPasswordInput.width - leftPadding) placeholderText: text === "" && webdavConnectionLoader.item && webdavConnectionLoader.item.isPasswordStored ? qsTr("Password (leave empty to use remembered)") : qsTr("Password") echoMode: TextInput.Password @@ -1015,6 +1040,29 @@ Page { webdavConnectionLoader.item.password = text; } } + + QfToolButton { + id: importWebdavShowPasswordInput + + property int originalEchoMode: TextInput.Normal + + visible: (!!parent.echoMode && parent.echoMode !== TextInput.Normal) || originalEchoMode !== TextInput.Normal + iconSource: parent.echoMode === TextInput.Normal ? Theme.getThemeVectorIcon('ic_hide_green_48dp') : Theme.getThemeVectorIcon('ic_show_green_48dp') + iconColor: Theme.mainColor + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + opacity: parent.text.length > 0 ? 1 : 0.25 + z: 1 + + onClicked: { + if (parent.echoMode !== TextInput.Normal) { + originalEchoMode = parent.echoMode; + parent.echoMode = TextInput.Normal; + } else { + parent.echoMode = originalEchoMode; + } + } + } } CheckBox { From 8acf56485db3548288015e8b02383a9040b96a4a Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 11 Jan 2025 12:02:05 +0700 Subject: [PATCH 37/43] Only show favorites that exists, sort them by display name --- src/core/localfilesmodel.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/core/localfilesmodel.cpp b/src/core/localfilesmodel.cpp index a57df499b6..d3632f2d48 100644 --- a/src/core/localfilesmodel.cpp +++ b/src/core/localfilesmodel.cpp @@ -239,10 +239,17 @@ void LocalFilesModel::reloadModel() } const QStringList favorites = QSettings().value( QStringLiteral( "qfieldFavorites" ), QStringList() ).toStringList(); + QList favoriteItems; for ( const QString &item : favorites ) { - mItems << Item( ItemMetaType::Favorite, ItemType::SimpleFolder, getCurrentTitleFromPath( item ), QString(), item ); + if ( QFileInfo::exists( item ) ) + { + favoriteItems << Item( ItemMetaType::Favorite, ItemType::SimpleFolder, getCurrentTitleFromPath( item ), QString(), item ); + } } + + std::sort( favoriteItems.begin(), favoriteItems.end(), []( const Item &a, const Item &b ) { return a.title < b.title; } ); + mItems.append( favoriteItems ); } else { From 5e3e6121d47515f76206fab9f811e1f694041eb0 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 11 Jan 2025 12:05:57 +0700 Subject: [PATCH 38/43] Avoid complex showing/hiding panels, just stack them properly --- src/qml/qgismobileapp.qml | 62 ++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index 52047f3f5d..4a63595de4 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -3883,6 +3883,31 @@ ApplicationWindow { parent: Overlay.overlay } + WelcomeScreen { + id: welcomeScreen + objectName: "welcomeScreen" + visible: !iface.hasProjectOnLaunch() + + model: RecentProjectListModel { + id: recentProjectListModel + } + property ProjectSource __projectSource + + anchors.fill: parent + + onOpenLocalDataPicker: { + qfieldLocalDataPickerScreen.projectFolderView = false; + qfieldLocalDataPickerScreen.model.resetToRoot(); + qfieldLocalDataPickerScreen.visible = true; + } + + onShowQFieldCloudScreen: { + qfieldCloudScreen.visible = true; + } + + Component.onCompleted: focusstack.addFocusTaker(this) + } + QFieldCloudScreen { id: qfieldCloudScreen @@ -3892,7 +3917,6 @@ ApplicationWindow { onFinished: { visible = false; - welcomeScreen.visible = true; } Component.onCompleted: focusstack.addFocusTaker(this) @@ -3924,42 +3948,8 @@ ApplicationWindow { visible: false focus: visible - onFinished: { + onFinished: loading => { visible = false; - if (model.currentPath === 'root') { - welcomeScreen.visible = loading ? false : true; - } - } - - Component.onCompleted: focusstack.addFocusTaker(this) - } - - WelcomeScreen { - id: welcomeScreen - objectName: "welcomeScreen" - visible: !iface.hasProjectOnLaunch() - - model: RecentProjectListModel { - id: recentProjectListModel - } - property ProjectSource __projectSource - - anchors.fill: parent - - onOpenLocalDataPicker: { - //if (platformUtilities.capabilities & PlatformUtilities.CustomLocalDataPicker) { - welcomeScreen.visible = false; - qfieldLocalDataPickerScreen.projectFolderView = false; - qfieldLocalDataPickerScreen.model.resetToRoot(); - qfieldLocalDataPickerScreen.visible = true; - //} else { - // __projectSource = platformUtilities.openProject(this); - //} - } - - onShowQFieldCloudScreen: { - welcomeScreen.visible = false; - qfieldCloudScreen.visible = true; } onShowSettings: { From 5ea14b400dd11a9ba08bea802a2cfd9061dba26f Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 11 Jan 2025 12:12:35 +0700 Subject: [PATCH 39/43] Another tiny focus stack fix --- src/core/focusstack.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/focusstack.cpp b/src/core/focusstack.cpp index a0ab75ae30..e4f2f4cd2c 100644 --- a/src/core/focusstack.cpp +++ b/src/core/focusstack.cpp @@ -61,6 +61,7 @@ void FocusStack::setFocused( QObject *object ) { mStackList.removeAll( object ); mStackList.append( object ); + QMetaObject::invokeMethod( object, "forceActiveFocus", Qt::DirectConnection ); } void FocusStack::setUnfocused( QObject *object ) From a6f50b33ce573dbbbb3ff01ba468138fc9ea17f2 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 11 Jan 2025 12:57:35 +0700 Subject: [PATCH 40/43] Remove last qDebug() call --- src/core/webdavconnection.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/webdavconnection.cpp b/src/core/webdavconnection.cpp index 738fb569aa..9310b15d39 100644 --- a/src/core/webdavconnection.cpp +++ b/src/core/webdavconnection.cpp @@ -609,7 +609,6 @@ void WebdavConnection::uploadPath( const QString &localPath ) mWebdavLastModified.clear(); mLocalItems.clear(); - qDebug() << mProcessLocalPath; QDirIterator it( mProcessLocalPath, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories ); while ( it.hasNext() ) { From 7448bc3199c51d80a2eac273d3b478e372ac0e06 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 12 Jan 2025 09:31:26 +0700 Subject: [PATCH 41/43] Fix rebase gone wrong --- src/qml/qgismobileapp.qml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index 4a63595de4..ecc8a0bfee 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -3905,6 +3905,11 @@ ApplicationWindow { qfieldCloudScreen.visible = true; } + onShowSettings: { + qfieldSettings.reset(); + qfieldSettings.visible = true; + } + Component.onCompleted: focusstack.addFocusTaker(this) } @@ -3952,11 +3957,6 @@ ApplicationWindow { visible = false; } - onShowSettings: { - qfieldSettings.reset(); - qfieldSettings.visible = true; - } - Component.onCompleted: focusstack.addFocusTaker(this) } From 492feaa9b12576283f5864df07e7aa12bcd1dc62 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 13 Jan 2025 09:09:59 +0700 Subject: [PATCH 42/43] Fix main menu location when opening from dashboard --- src/qml/DashBoard.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qml/DashBoard.qml b/src/qml/DashBoard.qml index cfbdc7f900..fc70d48439 100644 --- a/src/qml/DashBoard.qml +++ b/src/qml/DashBoard.qml @@ -47,7 +47,7 @@ Drawer { focus: visible clip: true - onShowMenu: mainMenu.popup(menuButton.x + menuButton.width - mainMenu.width - 2, mainWindow.sceneTopMargin + menuButton.y - 2) + onShowMenu: mainMenu.popup(menuButton.x + menuButton.width - mainMenu.width - 2, menuButton.y - 2) onShowCloudMenu: qfieldCloudPopup.show() onActiveLayerChanged: { From e4cfc0964de22b03de1fc1028099db96d4c20862 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 13 Jan 2025 09:13:01 +0700 Subject: [PATCH 43/43] Fix missing translation string --- src/qml/QFieldLocalDataPickerScreen.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index e13c7c8b05..ba20851aaa 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -762,7 +762,7 @@ Page { QfDialog { id: importUrlDialog - title: "Import URL" + title: qsTr("Import URL") focus: visible y: (mainWindow.height - height - 80) / 2 @@ -968,7 +968,7 @@ Page { QfDialog { id: importWebdavDialog - title: "Import WebDAV folder" + title: qsTr("Import WebDAV folder") focus: visible y: (mainWindow.height - height - 80) / 2