diff --git a/resources.qrc b/resources.qrc index fc65956940bf..79a38f1e6137 100644 --- a/resources.qrc +++ b/resources.qrc @@ -12,6 +12,7 @@ src/gui/filedetails/FileDetailsView.qml src/gui/filedetails/FileDetailsWindow.qml src/gui/filedetails/FileTag.qml + src/gui/filedetails/NCInputDateField.qml src/gui/filedetails/NCInputTextEdit.qml src/gui/filedetails/NCInputTextField.qml src/gui/filedetails/NCTabButton.qml @@ -61,6 +62,10 @@ src/gui/ResolveConflictsDialog.qml src/gui/ConflictDelegate.qml src/gui/ConflictItemFileInfo.qml - src/gui/filedetails/NCInputDateField.qml + src/gui/macOS/ui/FileProviderSettings.qml + src/gui/macOS/ui/FileProviderFileDelegate.qml + src/gui/macOS/ui/FileProviderEvictionDialog.qml + src/gui/macOS/ui/FileProviderSyncStatus.qml + src/gui/macOS/ui/FileProviderStorageInfo.qml diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Extensions/Logger+Extensions.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Extensions/Logger+Extensions.swift index f58ff0e90887..6edb6ab4be9f 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Extensions/Logger+Extensions.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Extensions/Logger+Extensions.swift @@ -26,5 +26,27 @@ extension Logger { static let localFileOps = Logger(subsystem: subsystem, category: "localfileoperations") static let ncFilesDatabase = Logger(subsystem: subsystem, category: "nextcloudfilesdatabase") static let materialisedFileHandling = Logger( - subsystem: subsystem, category: "materialisedfilehandling") + subsystem: subsystem, category: "materialisedfilehandling" + ) + static let logger = Logger(subsystem: subsystem, category: "logger") + + @available(macOSApplicationExtension 12.0, *) + static func logEntries(interval: TimeInterval = -3600) -> (Array?, Error?) { + do { + let logStore = try OSLogStore(scope: .currentProcessIdentifier) + let timeDate = Date().addingTimeInterval(interval) + let logPosition = logStore.position(date: timeDate) + let entries = try logStore.getEntries(at: logPosition) + + return (entries + .compactMap { $0 as? OSLogEntryLog } + .filter { $0.subsystem == Logger.subsystem } + .map { $0.composedMessage }, nil) + + } catch let error { + Logger.logger.error("Could not acquire os log store: \(error)"); + return (nil, error) + } + } } + diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationProtocol.h b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationProtocol.h index 766eab1f6778..47eb6c95bfec 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationProtocol.h +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationProtocol.h @@ -24,6 +24,7 @@ serverUrl:(NSString *)serverUrl password:(NSString *)password; - (void)removeAccountConfig; +- (void)createDebugLogStringWithCompletionHandler:(void(^)(NSString *debugLogString, NSError *error))completionHandler; @end diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift index 83d26ae3a84c..ccdc1dc90209 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift @@ -60,4 +60,21 @@ class ClientCommunicationService: NSObject, NSFileProviderServiceSource, NSXPCLi func removeAccountConfig() { self.fpExtension.removeAccountConfig() } + + func createDebugLogString(completionHandler: ((String?, Error?) -> Void)!) { + if #available(macOSApplicationExtension 12.0, *) { + let (logs, error) = Logger.logEntries() + guard error == nil else { + Logger.logger.error("Cannot create debug archive, received error: \(error, privacy: .public)") + completionHandler(nil, error) + return + } + guard let logs = logs else { + Logger.logger.error("Canot create debug archive with nil logs.") + completionHandler(nil, nil) + return + } + completionHandler(logs.joined(separator: "\n"), nil) + } + } } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a2a595faab2f..cf1e13032f2e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -26,6 +26,12 @@ set_package_properties(Qt5Concurrent PROPERTIES TYPE REQUIRED ) +find_package(Qt5QuickWidgets ${REQUIRED_QT_VERSION} CONFIG QUIET) +set_package_properties(Qt5QuickWidgets PROPERTIES + DESCRIPTION "Qt5 QuickWidgets component." + TYPE REQUIRED +) + find_package(Qt5WebEngineWidgets ${REQUIRED_QT_VERSION} CONFIG QUIET) if(NOT BUILD_WITH_WEBENGINE) set_package_properties(Qt5WebEngineWidgets PROPERTIES diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 4a6e4d27fb1b..097e4df7909e 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1,5 +1,5 @@ project(gui) -find_package(Qt5 REQUIRED COMPONENTS Widgets Svg Qml Quick QuickControls2 Xml Network) +find_package(Qt5 REQUIRED COMPONENTS Widgets Svg Qml Quick QuickControls2 QuickWidgets Xml Network) find_package(KF5Archive REQUIRED) if(QUICK_COMPILER) @@ -293,15 +293,31 @@ IF( APPLE ) macOS/fileprovider_mac.mm macOS/fileproviderdomainmanager.h macOS/fileproviderdomainmanager_mac.mm + macOS/fileproviderdomainsyncstatus.h + macOS/fileproviderdomainsyncstatus_mac.mm + macOS/fileprovideritemmetadata.h + macOS/fileprovideritemmetadata.cpp + macOS/fileprovideritemmetadata_mac.mm + macOS/fileprovidermaterialiseditemsmodel.h + macOS/fileprovidermaterialiseditemsmodel.cpp + macOS/fileprovidermaterialiseditemsmodel_mac.mm + macOS/fileprovidersettingscontroller.h + macOS/fileprovidersettingscontroller_mac.mm macOS/fileprovidersocketcontroller.h macOS/fileprovidersocketcontroller.cpp macOS/fileprovidersocketserver.h macOS/fileprovidersocketserver.cpp macOS/fileprovidersocketserver_mac.mm + macOS/fileproviderstorageuseenumerationobserver.h + macOS/fileproviderstorageuseenumerationobserver.m + macOS/fileproviderutils.h + macOS/fileproviderutils_mac.mm macOS/fileproviderxpc.h macOS/fileproviderxpc_mac.mm macOS/fileproviderxpc_mac_utils.h - macOS/fileproviderxpc_mac_utils.mm) + macOS/fileproviderxpc_mac_utils.mm + macOS/progressobserver.h + macOS/progressobserver.m) endif() if(SPARKLE_FOUND AND BUILD_UPDATER) @@ -543,6 +559,7 @@ target_link_libraries(nextcloudCore Qt5::Qml Qt5::Quick Qt5::QuickControls2 + Qt5::QuickWidgets KF5::Archive ) diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 8387c25c0bdc..e61043feda1e 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -60,6 +60,10 @@ #include #include +#ifdef BUILD_FILE_PROVIDER_MODULE +#include "macOS/fileprovider.h" +#endif + #include "account.h" namespace { @@ -193,6 +197,24 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) #endif new ToolTipUpdater(_ui->_folderList); +#if defined(BUILD_FILE_PROVIDER_MODULE) + if (Mac::FileProvider::fileProviderAvailable()) { + const auto fileProviderTab = _ui->fileProviderTab; + const auto fpSettingsLayout = new QVBoxLayout(fileProviderTab); + const auto fpAccountUserIdAtHost = _accountState->account()->userIdAtHostWithPort(); + const auto fpSettingsController = Mac::FileProviderSettingsController::instance(); + const auto fpSettingsWidget = fpSettingsController->settingsViewWidget(fpAccountUserIdAtHost, fileProviderTab); + fpSettingsLayout->setMargin(0); + fpSettingsLayout->addWidget(fpSettingsWidget); + fileProviderTab->setLayout(fpSettingsLayout); + } else { + disguiseTabWidget(); + } +#else + disguiseTabWidget(); + _ui->tabWidget->setCurrentIndex(0); +#endif + const auto mouseCursorChanger = new MouseCursorChanger(this); mouseCursorChanger->folderList = _ui->_folderList; mouseCursorChanger->model = _model; @@ -1688,6 +1710,14 @@ void AccountSettings::initializeE2eEncryptionSettingsMessage() connect(actionEnableE2e, &QAction::triggered, this, &AccountSettings::slotE2eEncryptionGenerateKeys); } +void AccountSettings::disguiseTabWidget() const +{ + // Ensure all elements of the tab widget are hidden. + // Document mode lets the child view take up the whole view. + _ui->tabWidget->setDocumentMode(true); + _ui->tabWidget->tabBar()->hide(); +} + } // namespace OCC #include "accountsettings.moc" diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index 1ddbc097d413..117a3aa536a0 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -27,6 +27,10 @@ #include "owncloudgui.h" #include "folderstatusmodel.h" +#ifdef BUILD_FILE_PROVIDER_MODULE +#include "macOS/fileprovidersettingscontroller.h" +#endif + class QModelIndex; class QNetworkReply; class QListWidgetItem; @@ -137,6 +141,8 @@ private slots: /// Returns the alias of the selected folder, empty string if none [[nodiscard]] QString selectedFolderAlias() const; + void disguiseTabWidget() const; + Ui::AccountSettings *_ui; FolderStatusModel *_model; diff --git a/src/gui/accountsettings.ui b/src/gui/accountsettings.ui index 77224160c7dc..bc1594e8f318 100644 --- a/src/gui/accountsettings.ui +++ b/src/gui/accountsettings.ui @@ -6,14 +6,68 @@ 0 0 - 588 - 557 + 1028 + 871 Form + + + + + + + 0 + 0 + + + + + + + Storage space: … + + + Qt::PlainText + + + false + + + + + + + false + + + + 0 + 0 + + + + + 16777215 + 7 + + + + 100 + + + -1 + + + false + + + + + @@ -131,82 +185,9 @@ - - - - - - - 0 - 0 - - - - - - - Storage space: … - - - Qt::PlainText - - - false - - - - - - - false - - - - 0 - 0 - - - - - 16777215 - 7 - - - - 100 - - - -1 - - - false - - - - - - - - - - 0 - 5 - - - - Qt::CustomContextMenu - - - QAbstractItemView::NoEditTriggers - - - true - - - @@ -276,6 +257,59 @@ + + + + 1 + + + + Standard file sync + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Qt::CustomContextMenu + + + false + + + QAbstractItemView::NoEditTriggers + + + true + + + + + + + + Virtual file sync + + + + diff --git a/src/gui/macOS/fileprovider.h b/src/gui/macOS/fileprovider.h index e9d89b7a5ba8..215a62dfb253 100644 --- a/src/gui/macOS/fileprovider.h +++ b/src/gui/macOS/fileprovider.h @@ -39,6 +39,9 @@ class FileProvider : public QObject static bool fileProviderAvailable(); +public slots: + void createDebugArchiveForDomain(const QString &domainIdentifier, const QString &filename) const; + private slots: void configureXPC(); diff --git a/src/gui/macOS/fileprovider_mac.mm b/src/gui/macOS/fileprovider_mac.mm index f45b91556946..94b1d9f0190e 100644 --- a/src/gui/macOS/fileprovider_mac.mm +++ b/src/gui/macOS/fileprovider_mac.mm @@ -12,13 +12,15 @@ * for more details. */ -#import + +#include "fileprovider.h" #include -#include "configfile.h" +#include "libsync/configfile.h" +#include "gui/macOS/fileproviderxpc.h" -#include "fileprovider.h" +#import namespace OCC { @@ -37,10 +39,6 @@ qCInfo(lcMacFileProvider) << "File provider system is not available on this version of macOS."; deleteLater(); return; - } else if (!ConfigFile().macFileProviderModuleEnabled()) { - qCInfo(lcMacFileProvider) << "File provider module is not enabled in application config."; - deleteLater(); - return; } qCInfo(lcMacFileProvider) << "Initialising file provider domain manager."; @@ -65,9 +63,6 @@ if (!fileProviderAvailable()) { qCInfo(lcMacFileProvider) << "File provider system is not available on this version of macOS."; return nullptr; - } else if (!ConfigFile().macFileProviderModuleEnabled()) { - qCInfo(lcMacFileProvider) << "File provider module is not enabled in application config."; - return nullptr; } if (!_instance) { @@ -102,5 +97,10 @@ } } +void FileProvider::createDebugArchiveForDomain(const QString &domainIdentifier, const QString &filename) const +{ + _xpc->createDebugArchiveForExtension(domainIdentifier, filename); +} + } // namespace Mac } // namespace OCC diff --git a/src/gui/macOS/fileproviderdomainmanager.h b/src/gui/macOS/fileproviderdomainmanager.h index 33347dc5e1a3..8de3fc1f2493 100644 --- a/src/gui/macOS/fileproviderdomainmanager.h +++ b/src/gui/macOS/fileproviderdomainmanager.h @@ -33,6 +33,8 @@ class FileProviderDomainManager : public QObject ~FileProviderDomainManager() override; static AccountStatePtr accountStateFromFileProviderDomainIdentifier(const QString &domainIdentifier); + static QString fileProviderDomainIdentifierFromAccountState(const AccountStatePtr &accountState); + void start(); signals: @@ -40,6 +42,7 @@ class FileProviderDomainManager : public QObject private slots: void setupFileProviderDomains(); + void updateFileProviderDomains(); void addFileProviderDomainForAccount(const OCC::AccountState * const accountState); void removeFileProviderDomainForAccount(const OCC::AccountState * const accountState); diff --git a/src/gui/macOS/fileproviderdomainmanager_mac.mm b/src/gui/macOS/fileproviderdomainmanager_mac.mm index d6c9acda6dc4..331c44df54db 100644 --- a/src/gui/macOS/fileproviderdomainmanager_mac.mm +++ b/src/gui/macOS/fileproviderdomainmanager_mac.mm @@ -19,6 +19,7 @@ #include "config.h" #include "fileproviderdomainmanager.h" +#include "fileprovidersettingscontroller.h" #include "pushnotifications.h" #include "gui/accountmanager.h" @@ -223,6 +224,8 @@ void removeFileProviderDomain(const AccountState * const accountState) NSFileProviderDomain * const domain = _registeredDomains.take(domainId); [domain release]; + + _registeredDomains.remove(domainId); }]; } } @@ -437,6 +440,9 @@ QStringList configuredDomainIds() const const auto trReason = tr("%1 application has been closed. Reopen to reconnect.").arg(APPLICATION_NAME); disconnectFileProviderDomainForAccount(accountState, trReason); }); + + connect(FileProviderSettingsController::instance(), &FileProviderSettingsController::vfsEnabledAccountsChanged, + this, &FileProviderDomainManager::updateFileProviderDomains); } void FileProviderDomainManager::setupFileProviderDomains() @@ -446,11 +452,33 @@ QStringList configuredDomainIds() const } d->findExistingFileProviderDomains(); + updateFileProviderDomains(); +} + +void FileProviderDomainManager::updateFileProviderDomains() +{ + if (!d) { + return; + } - for(auto &accountState : AccountManager::instance()->accounts()) { + const auto vfsEnabledAccounts = FileProviderSettingsController::instance()->vfsEnabledAccounts(); + auto configuredDomains = d->configuredDomainIds(); + + for (const auto &accountUserIdAtHost : vfsEnabledAccounts) { + if (configuredDomains.contains(accountUserIdAtHost)) { + configuredDomains.removeAll(accountUserIdAtHost); + continue; + } + + const auto accountState = AccountManager::instance()->accountFromUserId(accountUserIdAtHost); addFileProviderDomainForAccount(accountState.data()); } + for (const auto &remainingDomainUserId : configuredDomains) { + const auto accountState = AccountManager::instance()->accountFromUserId(remainingDomainUserId); + removeFileProviderDomainForAccount(accountState.data()); + } + emit domainSetupComplete(); } @@ -649,6 +677,11 @@ QStringList configuredDomainIds() const return accountForReceivedDomainIdentifier; } +QString FileProviderDomainManager::fileProviderDomainIdentifierFromAccountState(const AccountStatePtr &accountState) +{ + return domainIdentifierForAccount(accountState->account()); +} + } // namespace Mac } // namespace OCC diff --git a/src/gui/macOS/fileproviderdomainsyncstatus.h b/src/gui/macOS/fileproviderdomainsyncstatus.h new file mode 100644 index 000000000000..b6b1256acf9e --- /dev/null +++ b/src/gui/macOS/fileproviderdomainsyncstatus.h @@ -0,0 +1,98 @@ +/* +* Copyright (C) 2024 by Claudio Cambra +* +* 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. +* +* This program is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +* for more details. +*/ + +#include +#include + +#pragma once + +namespace OCC::Mac +{ + +class FileProviderDomainSyncStatus : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("FileProviderDomainSyncStatus cannot be instantiated from QML") + Q_PROPERTY(bool syncing READ syncing NOTIFY syncingChanged) + Q_PROPERTY(bool downloading READ downloading NOTIFY downloadingChanged) + Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged) + Q_PROPERTY(double fractionCompleted READ fractionCompleted NOTIFY fractionCompletedChanged) + Q_PROPERTY(double downloadFractionCompleted READ downloadFractionCompleted NOTIFY downloadFractionCompletedChanged) + Q_PROPERTY(double uploadFractionCompleted READ uploadFractionCompleted NOTIFY uploadFractionCompletedChanged) + Q_PROPERTY(int downloadFileTotalCount READ downloadFileTotalCount NOTIFY downloadFileTotalCountChanged) + Q_PROPERTY(int downloadFileCompletedCount READ downloadFileCompletedCount NOTIFY downloadFileCompletedCountChanged) + Q_PROPERTY(int uploadFileTotalCount READ uploadFileTotalCount NOTIFY uploadFileTotalCountChanged) + Q_PROPERTY(int uploadFileCompletedCount READ uploadFileCompletedCount NOTIFY uploadFileCompletedCountChanged) + // TODO: more detailed reporting (time remaining, megabytes, etc.) + Q_PROPERTY(QUrl icon READ icon NOTIFY iconChanged) + +public: + explicit FileProviderDomainSyncStatus(const QString &domainIdentifier, QObject *parent = nullptr); + ~FileProviderDomainSyncStatus() override; + + [[nodiscard]] bool syncing() const; + [[nodiscard]] bool downloading() const; + [[nodiscard]] bool uploading() const; + [[nodiscard]] double fractionCompleted() const; + [[nodiscard]] double downloadFractionCompleted() const; + [[nodiscard]] double uploadFractionCompleted() const; + [[nodiscard]] int downloadFileTotalCount() const; + [[nodiscard]] int downloadFileCompletedCount() const; + [[nodiscard]] int uploadFileTotalCount() const; + [[nodiscard]] int uploadFileCompletedCount() const; + [[nodiscard]] QUrl icon() const; + +signals: + void syncingChanged(bool syncing); + void downloadingChanged(bool downloading); + void uploadingChanged(bool uploading); + void fractionCompletedChanged(double fractionCompleted); + void downloadFractionCompletedChanged(double downloadFractionCompleted); + void uploadFractionCompletedChanged(double uploadFractionCompleted); + void downloadFileTotalCountChanged(int downloadFileTotalCount); + void downloadFileCompletedCountChanged(int downloadFileCompletedCount); + void uploadFileTotalCountChanged(int uploadFileTotalCount); + void uploadFileCompletedCountChanged(int uploadFileCompletedCount); + void iconChanged(const QUrl &icon); + +private: + void setDownloading(const bool syncing); + void setUploading(const bool syncing); + void setDownloadFractionCompleted(const double fractionCompleted); + void setUploadFractionCompleted(const double fractionCompleted); + void setDownloadFileTotalCount(const int fileTotalCount); + void setDownloadFileCompletedCount(const int fileCompletedCount); + void setUploadFileTotalCount(const int fileTotalCount); + void setUploadFileCompletedCount(const int fileCompletedCount); + void setIcon(const QUrl &icon); + void updateIcon(); + + bool _downloading = false; + bool _uploading = false; + double _downloadFractionCompleted = 0.0; + double _uploadFractionCompleted = 0.0; + int _downloadFileTotalCount = 0; + int _downloadFileCompletedCount = 0; + int _uploadFileTotalCount = 0; + int _uploadFileCompletedCount = 0; + QUrl _icon; + + class MacImplementation; + std::unique_ptr d; +}; + +} // OCC::Mac + +Q_DECLARE_METATYPE(OCC::Mac::FileProviderDomainSyncStatus*) diff --git a/src/gui/macOS/fileproviderdomainsyncstatus_mac.mm b/src/gui/macOS/fileproviderdomainsyncstatus_mac.mm new file mode 100644 index 000000000000..6cc7efd42ae4 --- /dev/null +++ b/src/gui/macOS/fileproviderdomainsyncstatus_mac.mm @@ -0,0 +1,261 @@ +/* +* Copyright (C) 2024 by Claudio Cambra +* +* 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. +* +* This program is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +* for more details. +*/ + +#include "fileproviderdomainsyncstatus.h" + +#include + +#include "gui/macOS/fileproviderutils.h" +#include "libsync/theme.h" + +#import + +#import "gui/macOS/progressobserver.h" + +namespace OCC::Mac +{ + +Q_LOGGING_CATEGORY(lcMacFileProviderDomainSyncStatus, "nextcloud.gui.macfileproviderdomainsyncstatus", QtInfoMsg) + +class FileProviderDomainSyncStatus::MacImplementation +{ +public: + explicit MacImplementation(const QString &domainIdentifier, FileProviderDomainSyncStatus *parent) + : q(parent) + { + _domain = FileProviderUtils::domainForIdentifier(domainIdentifier); + _manager = [NSFileProviderManager managerForDomain:_domain]; + + if (_manager == nil) { + qCWarning(lcMacFileProviderDomainSyncStatus) << "Could not get manager for domain" << domainIdentifier; + return; + } + + NSProgress *const downloadProgress = [_manager globalProgressForKind:NSProgressFileOperationKindDownloading]; + NSProgress *const uploadProgress = [_manager globalProgressForKind:NSProgressFileOperationKindUploading]; + _downloadProgressObserver = [[ProgressObserver alloc] initWithProgress:downloadProgress]; + _uploadProgressObserver = [[ProgressObserver alloc] initWithProgress:uploadProgress]; + + _downloadProgressObserver.progressKVOChangeHandler = ^(NSProgress *const progress){ + updateDownload(progress); + }; + _uploadProgressObserver.progressKVOChangeHandler = ^(NSProgress *const progress){ + updateUpload(progress); + }; + } + + ~MacImplementation() = default; + + void updateDownload(NSProgress *const progress) const + { + qCInfo(lcMacFileProviderDomainSyncStatus) << "Download progress changed" << progress.localizedDescription; + if (progress == nil || q == nullptr) { + return; + } + + q->setDownloading(!progress.paused && !progress.cancelled && !progress.finished); + q->setDownloadFractionCompleted(progress.fractionCompleted); + q->setDownloadFileTotalCount(progress.fileTotalCount.intValue); + q->setDownloadFileCompletedCount(progress.fileCompletedCount.intValue); + q->updateIcon(); + } + + void updateUpload(NSProgress *const progress) const + { + qCInfo(lcMacFileProviderDomainSyncStatus) << "Upload progress changed" << progress.localizedDescription; + if (progress == nil || q == nullptr) { + return; + } + + q->setUploading(!progress.paused && !progress.cancelled && !progress.finished); + q->setUploadFractionCompleted(progress.fractionCompleted); + q->setUploadFileTotalCount(progress.fileTotalCount.intValue); + q->setUploadFileCompletedCount(progress.fileCompletedCount.intValue); + q->updateIcon(); + } + +private: + NSFileProviderDomain *_domain = nil; + NSFileProviderManager *_manager = nil; + ProgressObserver *_downloadProgressObserver = nullptr; + ProgressObserver *_uploadProgressObserver = nullptr; + FileProviderDomainSyncStatus *q = nullptr; +}; + +FileProviderDomainSyncStatus::FileProviderDomainSyncStatus(const QString &domainIdentifier, QObject *parent) + : QObject(parent) + , d(std::make_unique(domainIdentifier, this)) +{ + qRegisterMetaType("FileProviderDomainSyncStatus*"); + updateIcon(); +} + +FileProviderDomainSyncStatus::~FileProviderDomainSyncStatus() = default; + +bool FileProviderDomainSyncStatus::syncing() const +{ + return downloading() || uploading(); +} + +bool FileProviderDomainSyncStatus::downloading() const +{ + return _downloading; +} + +bool FileProviderDomainSyncStatus::uploading() const +{ + return _uploading; +} + +double FileProviderDomainSyncStatus::fractionCompleted() const +{ + return (downloadFractionCompleted() + uploadFractionCompleted()) / 2; +} + +double FileProviderDomainSyncStatus::downloadFractionCompleted() const +{ + return _downloadFractionCompleted; +} + +double FileProviderDomainSyncStatus::uploadFractionCompleted() const +{ + return _uploadFractionCompleted; +} + +int FileProviderDomainSyncStatus::downloadFileTotalCount() const +{ + return _downloadFileTotalCount; +} + +int FileProviderDomainSyncStatus::downloadFileCompletedCount() const +{ + return _downloadFileCompletedCount; +} + +int FileProviderDomainSyncStatus::uploadFileTotalCount() const +{ + return _uploadFileTotalCount; +} + +int FileProviderDomainSyncStatus::uploadFileCompletedCount() const +{ + return _uploadFileCompletedCount; +} + +QUrl FileProviderDomainSyncStatus::icon() const +{ + return _icon; +} + +void FileProviderDomainSyncStatus::setDownloading(const bool downloading) +{ + if (_downloading == downloading) { + return; + } + + _downloading = downloading; + emit downloadingChanged(_downloading); + emit syncingChanged(syncing()); +} + +void FileProviderDomainSyncStatus::setUploading(const bool uploading) +{ + if (_uploading == uploading) { + return; + } + + _uploading = uploading; + emit uploadingChanged(_uploading); + emit syncingChanged(syncing()); +} + +void FileProviderDomainSyncStatus::setDownloadFractionCompleted(const double downloadFractionCompleted) +{ + if (_downloadFractionCompleted == downloadFractionCompleted) { + return; + } + + _downloadFractionCompleted = downloadFractionCompleted; + emit downloadFractionCompletedChanged(_downloadFractionCompleted); + emit fractionCompletedChanged(fractionCompleted()); +} + +void FileProviderDomainSyncStatus::setUploadFractionCompleted(const double uploadFractionCompleted) +{ + if (_uploadFractionCompleted == uploadFractionCompleted) { + return; + } + + _uploadFractionCompleted = uploadFractionCompleted; + emit uploadFractionCompletedChanged(_uploadFractionCompleted); + emit fractionCompletedChanged(fractionCompleted()); +} + +void FileProviderDomainSyncStatus::setDownloadFileTotalCount(const int fileTotalCount) +{ + if (_downloadFileTotalCount == fileTotalCount) { + return; + } + + _downloadFileTotalCount = fileTotalCount; + emit downloadFileTotalCountChanged(_downloadFileTotalCount); +} + +void FileProviderDomainSyncStatus::setDownloadFileCompletedCount(const int fileCompletedCount) +{ + if (_downloadFileCompletedCount == fileCompletedCount) { + return; + } + + _downloadFileCompletedCount = fileCompletedCount; + emit downloadFileCompletedCountChanged(_downloadFileCompletedCount); +} + +void FileProviderDomainSyncStatus::setUploadFileTotalCount(const int fileTotalCount) +{ + if (_uploadFileTotalCount == fileTotalCount) { + return; + } + + _uploadFileTotalCount = fileTotalCount; + emit uploadFileTotalCountChanged(_uploadFileTotalCount); +} + +void FileProviderDomainSyncStatus::setUploadFileCompletedCount(const int fileCompletedCount) +{ + if (_uploadFileCompletedCount == fileCompletedCount) { + return; + } + + _uploadFileCompletedCount = fileCompletedCount; + emit uploadFileCompletedCountChanged(_uploadFileCompletedCount); +} + +void FileProviderDomainSyncStatus::setIcon(const QUrl &icon) +{ + if (_icon == icon) { + return; + } + + _icon = icon; + emit iconChanged(_icon); +} + +void FileProviderDomainSyncStatus::updateIcon() +{ + const auto iconUrl = syncing() ? Theme::instance()->syncStatusRunning() : Theme::instance()->syncStatusOk(); + setIcon(iconUrl); +} + +} // OCC::Mac diff --git a/src/gui/macOS/fileprovideritemmetadata.cpp b/src/gui/macOS/fileprovideritemmetadata.cpp new file mode 100644 index 000000000000..2219a22262d7 --- /dev/null +++ b/src/gui/macOS/fileprovideritemmetadata.cpp @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "fileprovideritemmetadata.h" + +namespace OCC { + +namespace Mac { + +QString FileProviderItemMetadata::identifier() const +{ + return _identifier; +} + +QString FileProviderItemMetadata::parentItemIdentifier() const +{ + return _parentItemIdentifier; +} + +QString FileProviderItemMetadata::domainIdentifier() const +{ + return _domainIdentifier; +} + +QString FileProviderItemMetadata::filename() const +{ + return _filename; +} + +QString FileProviderItemMetadata::typeIdentifier() const +{ + return _typeIdentifier; +} + +QString FileProviderItemMetadata::symlinkTargetPath() const +{ + return _symlinkTargetPath; +} + +QString FileProviderItemMetadata::uploadingError() const +{ + return _uploadingError; +} + +QString FileProviderItemMetadata::downloadingError() const +{ + return _downloadingError; +} + +QString FileProviderItemMetadata::mostRecentEditorName() const +{ + return _mostRecentEditorName; +} + +QString FileProviderItemMetadata::ownerName() const +{ + return _ownerName; +} + +QDateTime FileProviderItemMetadata::contentModificationDate() const +{ + return _contentModificationDate; +} + +QDateTime FileProviderItemMetadata::creationDate() const +{ + return _creationDate; +} + +QDateTime FileProviderItemMetadata::lastUsedDate() const +{ + return _lastUsedDate; +} + +QByteArray FileProviderItemMetadata::contentVersion() const +{ + return _contentVersion; +} + +QByteArray FileProviderItemMetadata::metadataVersion() const +{ + return _metadataVersion; +} + +QByteArray FileProviderItemMetadata::tagData() const +{ + return _tagData; +} + +QHash FileProviderItemMetadata::extendedAttributes() const +{ + return _extendedAttributes; +} + +int FileProviderItemMetadata::capabilities() const +{ + return _capabilities; +} + +int FileProviderItemMetadata::fileSystemFlags() const +{ + return _fileSystemFlags; +} + +unsigned int FileProviderItemMetadata::childItemCount() const +{ + return _childItemCount; +} + +unsigned int FileProviderItemMetadata::typeOsCode() const +{ + return _typeOsCode; +} + +unsigned int FileProviderItemMetadata::creatorOsCode() const +{ + return _creatorOsCode; +} + +unsigned long long FileProviderItemMetadata::documentSize() const +{ + return _documentSize; +} + +bool FileProviderItemMetadata::mostRecentVersionDownloaded() const +{ + return _mostRecentVersionDownloaded; +} + +bool FileProviderItemMetadata::uploading() const +{ + return _uploading; +} + +bool FileProviderItemMetadata::uploaded() const +{ + return _uploaded; +} + +bool FileProviderItemMetadata::downloading() const +{ + return _downloading; +} + +bool FileProviderItemMetadata::downloaded() const +{ + return _downloaded; +} + +bool FileProviderItemMetadata::shared() const +{ + return _shared; +} + +bool FileProviderItemMetadata::sharedByCurrentUser() const +{ + return _sharedByCurrentUser; +} + +QString FileProviderItemMetadata::userVisiblePath() const +{ + return _userVisiblePath; +} + +QString FileProviderItemMetadata::fileTypeString() const +{ + return _fileTypeString; +} + +bool operator==(const FileProviderItemMetadata &lhs, const FileProviderItemMetadata &rhs) +{ + return lhs.identifier() == rhs.identifier() && + lhs.contentVersion() == rhs.contentVersion() && + lhs.metadataVersion() == rhs.metadataVersion(); +} + +} // namespace Mac + +} // namespace OCC diff --git a/src/gui/macOS/fileprovideritemmetadata.h b/src/gui/macOS/fileprovideritemmetadata.h new file mode 100644 index 000000000000..e3e0b0a0cd23 --- /dev/null +++ b/src/gui/macOS/fileprovideritemmetadata.h @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#pragma once + +#include +#include + +namespace OCC { + +namespace Mac { + +class FileProviderItemMetadata +{ + Q_GADGET + + Q_PROPERTY(QString identifier READ identifier CONSTANT) + Q_PROPERTY(QString parentItemIdentifier READ parentItemIdentifier CONSTANT) + Q_PROPERTY(QString domainIdentifier READ domainIdentifier CONSTANT) + Q_PROPERTY(QString filename READ filename CONSTANT) + Q_PROPERTY(QString typeIdentifier READ typeIdentifier CONSTANT) + Q_PROPERTY(QString symlinkTargetPath READ symlinkTargetPath CONSTANT) + Q_PROPERTY(QString uploadingError READ uploadingError CONSTANT) + Q_PROPERTY(QString downloadingError READ downloadingError CONSTANT) + Q_PROPERTY(QString mostRecentEditorName READ mostRecentEditorName CONSTANT) + Q_PROPERTY(QString ownerName READ ownerName CONSTANT) + Q_PROPERTY(QDateTime contentModificationDate READ contentModificationDate CONSTANT) + Q_PROPERTY(QDateTime creationDate READ creationDate CONSTANT) + Q_PROPERTY(QDateTime lastUsedDate READ lastUsedDate CONSTANT) + Q_PROPERTY(QByteArray contentVersion READ contentVersion CONSTANT) + Q_PROPERTY(QByteArray metadataVersion READ metadataVersion CONSTANT) + Q_PROPERTY(QByteArray tagData READ tagData CONSTANT) + Q_PROPERTY(QHash extendedAttributes READ extendedAttributes CONSTANT) + Q_PROPERTY(int capabilities READ capabilities CONSTANT) + Q_PROPERTY(int fileSystemFlags READ fileSystemFlags CONSTANT) + Q_PROPERTY(unsigned int childItemCount READ childItemCount CONSTANT) + Q_PROPERTY(unsigned int typeOsCode READ typeOsCode CONSTANT) + Q_PROPERTY(unsigned int creatorOsCode READ creatorOsCode CONSTANT) + Q_PROPERTY(unsigned long long documentSize READ documentSize CONSTANT) + Q_PROPERTY(bool mostRecentVersionDownloaded READ mostRecentVersionDownloaded CONSTANT) + Q_PROPERTY(bool uploading READ uploading CONSTANT) + Q_PROPERTY(bool uploaded READ uploaded CONSTANT) + Q_PROPERTY(bool downloading READ downloading CONSTANT) + Q_PROPERTY(bool downloaded READ downloaded CONSTANT) + Q_PROPERTY(bool shared READ shared CONSTANT) + Q_PROPERTY(bool sharedByCurrentUser READ sharedByCurrentUser CONSTANT) + + Q_PROPERTY(QString userVisiblePath READ userVisiblePath CONSTANT) + +public: + static FileProviderItemMetadata fromNSFileProviderItem(const void *const nsFileProviderItem, const QString &domainIdentifier); + + [[nodiscard]] QString identifier() const; + [[nodiscard]] QString parentItemIdentifier() const; + [[nodiscard]] QString domainIdentifier() const; + [[nodiscard]] QString filename() const; + [[nodiscard]] QString typeIdentifier() const; + [[nodiscard]] QString symlinkTargetPath() const; + [[nodiscard]] QString uploadingError() const; + [[nodiscard]] QString downloadingError() const; + [[nodiscard]] QString mostRecentEditorName() const; + [[nodiscard]] QString ownerName() const; + [[nodiscard]] QDateTime contentModificationDate() const; + [[nodiscard]] QDateTime creationDate() const; + [[nodiscard]] QDateTime lastUsedDate() const; + [[nodiscard]] QByteArray contentVersion() const; + [[nodiscard]] QByteArray metadataVersion() const; + [[nodiscard]] QByteArray tagData() const; + [[nodiscard]] QHash extendedAttributes() const; + [[nodiscard]] int capabilities() const; + [[nodiscard]] int fileSystemFlags() const; + [[nodiscard]] unsigned int childItemCount() const; + [[nodiscard]] unsigned int typeOsCode() const; + [[nodiscard]] unsigned int creatorOsCode() const; + [[nodiscard]] unsigned long long documentSize() const; + [[nodiscard]] bool mostRecentVersionDownloaded() const; + [[nodiscard]] bool uploading() const; + [[nodiscard]] bool uploaded() const; + [[nodiscard]] bool downloading() const; + [[nodiscard]] bool downloaded() const; + [[nodiscard]] bool shared() const; + [[nodiscard]] bool sharedByCurrentUser() const; + + [[nodiscard]] QString userVisiblePath() const; + [[nodiscard]] QString fileTypeString() const; + + // Check equality via identifier, contentVersion, and metadataVersion + friend bool operator==(const FileProviderItemMetadata &lhs, const FileProviderItemMetadata &rhs); + +private: + [[nodiscard]] QString getUserVisiblePath() const; + + QString _identifier; + QString _parentItemIdentifier; + QString _domainIdentifier; + QString _filename; + QString _typeIdentifier; + QString _symlinkTargetPath; + QString _uploadingError; + QString _downloadingError; + QString _mostRecentEditorName; + QString _ownerName; + QDateTime _contentModificationDate; + QDateTime _creationDate; + QDateTime _lastUsedDate; + QByteArray _contentVersion; + QByteArray _metadataVersion; + QByteArray _tagData; + QHash _extendedAttributes; + quint64 _favoriteRank = 0; + int _capabilities = 0; + int _fileSystemFlags = 0; + unsigned int _childItemCount = 0; + unsigned int _typeOsCode = 0; + unsigned int _creatorOsCode = 0; + unsigned long long _documentSize = 0; + bool _mostRecentVersionDownloaded = false; + bool _uploading = false; + bool _uploaded = false; + bool _downloading = false; + bool _downloaded = false; + bool _shared = false; + bool _sharedByCurrentUser = false; + bool _trashed = false; + + QString _userVisiblePath; + QString _fileTypeString; +}; + +} + +} diff --git a/src/gui/macOS/fileprovideritemmetadata_mac.mm b/src/gui/macOS/fileprovideritemmetadata_mac.mm new file mode 100644 index 000000000000..d8e64abb6883 --- /dev/null +++ b/src/gui/macOS/fileprovideritemmetadata_mac.mm @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "fileprovideritemmetadata.h" + +#include +#include + +#import +#import +#import + +#include "fileproviderutils.h" + +namespace { + +QString nsNameComponentsToLocalisedQString(NSPersonNameComponents *const nameComponents) +{ + if (nameComponents == nil) { + return {}; + } + + NSString *const name = [NSPersonNameComponentsFormatter localizedStringFromPersonNameComponents:nameComponents style:NSPersonNameComponentsFormatterStyleDefault options:0]; + return QString::fromNSString(name); +} + +QHash extendedAttributesToHash(NSDictionary *const extendedAttributes) +{ + QHash hash; + for (NSString *const key in extendedAttributes) { + NSData *const value = [extendedAttributes objectForKey:key]; + hash.insert(QString::fromNSString(key), QByteArray::fromNSData(value)); + } + return hash; +} + +} + +namespace OCC { + +namespace Mac { + +Q_LOGGING_CATEGORY(lcMacImplFileProviderItemMetadata, "nextcloud.gui.macfileprovideritemmetadatamacimpl", QtInfoMsg) + +FileProviderItemMetadata FileProviderItemMetadata::fromNSFileProviderItem(const void *const nsFileProviderItem, const QString &domainIdentifier) +{ + FileProviderItemMetadata metadata; + const id bridgedNsFileProviderItem = (__bridge id)nsFileProviderItem; + if (bridgedNsFileProviderItem == nil) { + return {}; + } + + metadata._identifier = QString::fromNSString(bridgedNsFileProviderItem.itemIdentifier); + metadata._parentItemIdentifier = QString::fromNSString(bridgedNsFileProviderItem.parentItemIdentifier); + metadata._domainIdentifier = domainIdentifier; + metadata._filename = QString::fromNSString(bridgedNsFileProviderItem.filename); + metadata._typeIdentifier = QString::fromNSString(bridgedNsFileProviderItem.contentType.identifier); + metadata._symlinkTargetPath = QString::fromNSString(bridgedNsFileProviderItem.symlinkTargetPath); + metadata._uploadingError = QString::fromNSString(bridgedNsFileProviderItem.uploadingError.localizedDescription); + metadata._downloadingError = QString::fromNSString(bridgedNsFileProviderItem.downloadingError.localizedDescription); + metadata._mostRecentEditorName = nsNameComponentsToLocalisedQString(bridgedNsFileProviderItem.mostRecentEditorNameComponents); + metadata._ownerName = nsNameComponentsToLocalisedQString(bridgedNsFileProviderItem.ownerNameComponents); + metadata._contentModificationDate = QDateTime::fromNSDate(bridgedNsFileProviderItem.contentModificationDate); + metadata._creationDate = QDateTime::fromNSDate(bridgedNsFileProviderItem.creationDate); + metadata._lastUsedDate = QDateTime::fromNSDate(bridgedNsFileProviderItem.lastUsedDate); + metadata._contentVersion = QByteArray::fromNSData(bridgedNsFileProviderItem.itemVersion.contentVersion); + metadata._metadataVersion = QByteArray::fromNSData(bridgedNsFileProviderItem.itemVersion.metadataVersion); + metadata._tagData = QByteArray::fromNSData(bridgedNsFileProviderItem.tagData); + metadata._extendedAttributes = extendedAttributesToHash(bridgedNsFileProviderItem.extendedAttributes); + metadata._capabilities = bridgedNsFileProviderItem.capabilities; + metadata._fileSystemFlags = bridgedNsFileProviderItem.fileSystemFlags; + metadata._childItemCount = bridgedNsFileProviderItem.childItemCount.unsignedIntegerValue; + metadata._typeOsCode = bridgedNsFileProviderItem.typeAndCreator.type; + metadata._creatorOsCode = bridgedNsFileProviderItem.typeAndCreator.creator; + metadata._documentSize = bridgedNsFileProviderItem.documentSize.unsignedLongLongValue; + metadata._mostRecentVersionDownloaded = bridgedNsFileProviderItem.mostRecentVersionDownloaded; + metadata._uploading = bridgedNsFileProviderItem.uploading; + metadata._uploaded = bridgedNsFileProviderItem.uploaded; + metadata._downloading = bridgedNsFileProviderItem.downloading; + metadata._downloaded = bridgedNsFileProviderItem.downloaded; + metadata._shared = bridgedNsFileProviderItem.shared; + metadata._sharedByCurrentUser = bridgedNsFileProviderItem.sharedByCurrentUser; + + metadata._userVisiblePath = metadata.getUserVisiblePath(); + metadata._fileTypeString = QString::fromNSString(bridgedNsFileProviderItem.contentType.localizedDescription); + + if (metadata._documentSize == 0) { + // If the document size is 0, we can try to get the size of the file + // directly from its path. These are all materialised files anyway + // so the size will be properly represented + const auto path = metadata.userVisiblePath(); + const auto fileInfo = QFileInfo(path); + metadata._documentSize = fileInfo.size(); + } + + return metadata; +} + +QString FileProviderItemMetadata::getUserVisiblePath() const +{ + qCDebug(lcMacImplFileProviderItemMetadata) << "Getting user visible path"; + + const auto id = identifier(); + const auto domainId = domainIdentifier(); + + if (id.isEmpty() || domainId.isEmpty()) { + qCWarning(lcMacImplFileProviderItemMetadata) << "Could not fetch user visible path for item, no identifier or domainIdentifier"; + return QStringLiteral("Unknown"); + } + + __block QString returnPath = QObject::tr("Unknown"); + NSFileProviderManager *manager = FileProviderUtils::managerForDomainIdentifier(domainId); + + if (manager == nil) { + qCWarning(lcMacImplFileProviderItemMetadata) << "Null manager, cannot get item path"; + return returnPath; + } + + NSString *const nsItemIdentifier = id.toNSString(); + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + // getUserVisibleUrl is async, so wait here + + [manager getUserVisibleURLForItemIdentifier:nsItemIdentifier + completionHandler:^(NSURL *const userVisibleFile, NSError *const error) { + + if (error != nil) { + qCWarning(lcMacImplFileProviderItemMetadata) << "Error fetching user visible url for item identifier." << error.localizedDescription; + } else { + returnPath = QString::fromNSString(userVisibleFile.path); + } + + dispatch_semaphore_signal(semaphore); + }]; + + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + dispatch_release(semaphore); + + return returnPath; +} + +} + +} diff --git a/src/gui/macOS/fileprovidermaterialiseditemsmodel.cpp b/src/gui/macOS/fileprovidermaterialiseditemsmodel.cpp new file mode 100644 index 000000000000..2e18666c39b6 --- /dev/null +++ b/src/gui/macOS/fileprovidermaterialiseditemsmodel.cpp @@ -0,0 +1,171 @@ +/* + * Copyright 2023 (c) Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "fileprovidermaterialiseditemsmodel.h" + +#include + +namespace OCC { + +namespace Mac { + +FileProviderMaterialisedItemsModel::FileProviderMaterialisedItemsModel(QObject * const parent) + : QAbstractListModel(parent) +{ +} + +int FileProviderMaterialisedItemsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return _items.count(); +} + +QVariant FileProviderMaterialisedItemsModel::data(const QModelIndex &index, int role) const +{ + const auto item = _items.at(index.row()); + + switch (role) { + case Qt::DisplayRole: + case FilenameRole: + return item.filename(); + case IdentifierRole: + return item.identifier(); + case ParentItemIdentifierRole: + return item.parentItemIdentifier(); + case DomainIdentifierRole: + return item.domainIdentifier(); + case TypeIdentifierRole: + return item.typeIdentifier(); + case SymlinkTargetPathRole: + return item.symlinkTargetPath(); + case UploadingErrorRole: + return item.uploadingError(); + case DownloadingErrorRole: + return item.downloadingError(); + case MostRecentEditorNameRole: + return item.mostRecentEditorName(); + case OwnerNameRole: + return item.ownerName(); + case ContentModificationDateRole: + return item.contentModificationDate(); + case CreationDateRole: + return item.creationDate(); + case LastUsedDateRole: + return item.lastUsedDate(); + case ContentVersionRole: + return item.contentVersion(); + case MetadataVersionRole: + return item.metadataVersion(); + case TagDataRole: + return item.tagData(); + case CapabilitiesRole: + return item.capabilities(); + case FileSystemFlagsRole: + return item.fileSystemFlags(); + case ChildItemCountRole: + return item.childItemCount(); + case TypeOsCodeRole: + return item.typeOsCode(); + case CreatorOsCodeRole: + return item.creatorOsCode(); + case DocumentSizeRole: + return item.documentSize(); + case MostRecentVersionDownloadedRole: + return item.mostRecentVersionDownloaded(); + case UploadingRole: + return item.uploading(); + case UploadedRole: + return item.uploaded(); + case DownloadingRole: + return item.downloading(); + case DownloadedRole: + return item.downloaded(); + case SharedRole: + return item.shared(); + case SharedByCurrentUserRole: + return item.sharedByCurrentUser(); + case UserVisiblePathRole: + return item.userVisiblePath(); + case FileTypeStringRole: + return item.fileTypeString(); + case FileSizeStringRole: + return _locale.formattedDataSize(item.documentSize()); + } + return {}; +} + +QHash FileProviderMaterialisedItemsModel::roleNames() const +{ + auto roleNames = QAbstractListModel::roleNames(); + roleNames.insert({ + { IdentifierRole, "identifier" }, + { ParentItemIdentifierRole, "parentItemIdentifier" }, + { DomainIdentifierRole, "domainIdentifier" }, + { FilenameRole, "fileName" }, + { TypeIdentifierRole, "typeIdentifier" }, + { SymlinkTargetPathRole, "symlinkTargetPath" }, + { UploadingErrorRole, "uploadingError" }, + { DownloadingErrorRole, "downloadingError" }, + { MostRecentEditorNameRole, "mostRecentEditorName" }, + { OwnerNameRole, "ownerName" }, + { ContentModificationDateRole, "contentModificationDate" }, + { CreationDateRole, "creationDate" }, + { LastUsedDateRole, "lastUsedDate" }, + { ContentVersionRole, "contentVersion" }, + { MetadataVersionRole, "metadataVersion" }, + { TagDataRole, "tagData" }, + { CapabilitiesRole, "capabilities" }, + { FileSystemFlagsRole, "fileSystemFlags" }, + { ChildItemCountRole, "childItemCount" }, + { TypeOsCodeRole, "typeOsCode" }, + { CreatorOsCodeRole, "creatorOsCode" }, + { DocumentSizeRole, "documentSize" }, + { MostRecentVersionDownloadedRole, "mostRecentVersionDownloaded" }, + { UploadingRole, "uploading" }, + { UploadedRole, "uploaded" }, + { DownloadingRole, "downloading" }, + { DownloadedRole, "downloaded" }, + { SharedRole, "shared" }, + { SharedByCurrentUserRole, "sharedByCurrentUser" }, + { UserVisiblePathRole, "userVisiblePath" }, + { FileTypeStringRole, "fileTypeString" }, + { FileSizeStringRole, "fileSizeString" }, + }); + return roleNames; +} + +QVector FileProviderMaterialisedItemsModel::items() const +{ + return _items; +} + +void FileProviderMaterialisedItemsModel::setItems(const QVector &items) +{ + if (items == _items) { + return; + } + + beginResetModel(); + _items = items; + endResetModel(); + + Q_EMIT itemsChanged(); +} + +} // namespace Mac + +} // namespace OCC diff --git a/src/gui/macOS/fileprovidermaterialiseditemsmodel.h b/src/gui/macOS/fileprovidermaterialiseditemsmodel.h new file mode 100644 index 000000000000..3d34dd9f2b7f --- /dev/null +++ b/src/gui/macOS/fileprovidermaterialiseditemsmodel.h @@ -0,0 +1,90 @@ +/* + * Copyright 2023 (c) Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#pragma once + +#include +#include + +#include "gui/macOS/fileprovideritemmetadata.h" + +namespace OCC { + +namespace Mac { + +class FileProviderMaterialisedItemsModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QVector items READ items WRITE setItems NOTIFY itemsChanged) + +public: + enum Roles { + IdentifierRole = Qt::UserRole + 1, + ParentItemIdentifierRole, + DomainIdentifierRole, + FilenameRole, + TypeIdentifierRole, + SymlinkTargetPathRole, + UploadingErrorRole, + DownloadingErrorRole, + MostRecentEditorNameRole, + OwnerNameRole, + ContentModificationDateRole, + CreationDateRole, + LastUsedDateRole, + ContentVersionRole, + MetadataVersionRole, + TagDataRole, + CapabilitiesRole, + FileSystemFlagsRole, + ChildItemCountRole, + TypeOsCodeRole, + CreatorOsCodeRole, + DocumentSizeRole, + MostRecentVersionDownloadedRole, + UploadingRole, + UploadedRole, + DownloadingRole, + DownloadedRole, + SharedRole, + SharedByCurrentUserRole, + UserVisiblePathRole, + FileTypeStringRole, + FileSizeStringRole, + }; + Q_ENUM(Roles) + + explicit FileProviderMaterialisedItemsModel(QObject *parent = nullptr); + [[nodiscard]] int rowCount(const QModelIndex &parent = {}) const override; + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + [[nodiscard]] QHash roleNames() const override; + + [[nodiscard]] QVector items() const; + +signals: + void itemsChanged(); + +public slots: + void setItems(const QVector &items); + void evictItem(const QString &identifier, const QString &domainIdentifier); + +private: + QVector _items; + QLocale _locale; +}; + +} // namespace Mac + +} // namespace OCC diff --git a/src/gui/macOS/fileprovidermaterialiseditemsmodel_mac.mm b/src/gui/macOS/fileprovidermaterialiseditemsmodel_mac.mm new file mode 100644 index 000000000000..ec1dd2ce0c98 --- /dev/null +++ b/src/gui/macOS/fileprovidermaterialiseditemsmodel_mac.mm @@ -0,0 +1,84 @@ +/* + * Copyright 2023 (c) Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "fileprovidermaterialiseditemsmodel.h" + +#include + +#import + +#include "fileproviderutils.h" + +#include "gui/systray.h" + +namespace OCC { + +namespace Mac { + +Q_LOGGING_CATEGORY(lcMacImplFileProviderMaterialisedItemsModelMac, "nextcloud.gui.macfileprovidermaterialiseditemsmodelmac", QtInfoMsg) + +void FileProviderMaterialisedItemsModel::evictItem(const QString &identifier, const QString &domainIdentifier) +{ + NSFileProviderManager * const manager = FileProviderUtils::managerForDomainIdentifier(domainIdentifier); + if (manager == nil) { + qCWarning(lcMacImplFileProviderMaterialisedItemsModelMac) << "Received null manager for domain" + << domainIdentifier + << "cannot evict item" + << identifier; + Systray::instance()->showMessage(tr("Error"), + tr("An internal error occurred. Please try again later."), + QSystemTrayIcon::Warning); + return; + } + + __block BOOL successfullyDeleted = YES; + + [manager evictItemWithIdentifier:identifier.toNSString() completionHandler:^(NSError *error) { + if (error != nil) { + const auto errorDesc = QString::fromNSString(error.localizedDescription); + qCWarning(lcMacImplFileProviderMaterialisedItemsModelMac) << "Error evicting item:" << errorDesc; + Systray::instance()->showMessage(tr("Error"), + tr("An error occurred while trying to delete the local copy of this item: %1").arg(errorDesc), + QSystemTrayIcon::Warning); + successfullyDeleted = NO; + } + }]; + + if (successfullyDeleted == NO) { + return; + } + + const auto deletedItemIt = std::find_if(_items.cbegin(), + _items.cend(), + [identifier, domainIdentifier](const FileProviderItemMetadata &item) { + return item.identifier() == identifier && item.domainIdentifier() == domainIdentifier; + }); + + if (deletedItemIt == _items.cend()) { + qCWarning(lcMacImplFileProviderMaterialisedItemsModelMac) << "Could not find item" + << identifier + << "in model items."; + return; + } + + const auto deletedItemRow = std::distance(_items.cbegin(), deletedItemIt); + beginRemoveRows({}, deletedItemRow, deletedItemRow); + _items.remove(deletedItemRow); + endRemoveRows(); +} + + +} // namespace OCC + +} // namespace Mac diff --git a/src/gui/macOS/fileprovidersettingscontroller.h b/src/gui/macOS/fileprovidersettingscontroller.h new file mode 100644 index 000000000000..bf980fdf770c --- /dev/null +++ b/src/gui/macOS/fileprovidersettingscontroller.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#pragma once + +#include +#include + +#include "gui/macOS/fileproviderdomainsyncstatus.h" + +class QAbstractListModel; + +namespace OCC { + +class UserInfo; + +namespace Mac { + +class FileProviderSettingsController : public QObject +{ + Q_OBJECT + +public: + static FileProviderSettingsController *instance(); + + [[nodiscard]] QQuickWidget *settingsViewWidget(const QString &accountUserIdAtHost, + QWidget *const parent = nullptr, + const QQuickWidget::ResizeMode resizeMode = QQuickWidget::SizeRootObjectToView); + + [[nodiscard]] QStringList vfsEnabledAccounts() const; + [[nodiscard]] Q_INVOKABLE bool vfsEnabledForAccount(const QString &userIdAtHost) const; + [[nodiscard]] unsigned long long localStorageUsageForAccount(const QString &userIdAtHost) const; + [[nodiscard]] Q_INVOKABLE float localStorageUsageGbForAccount(const QString &userIdAtHost) const; + [[nodiscard]] unsigned long long remoteStorageUsageForAccount(const QString &userIdAtHost) const; + [[nodiscard]] Q_INVOKABLE float remoteStorageUsageGbForAccount(const QString &userIdAtHost) const; + + [[nodiscard]] Q_INVOKABLE QAbstractListModel *materialisedItemsModelForAccount(const QString &userIdAtHost); + [[nodiscard]] Q_INVOKABLE FileProviderDomainSyncStatus *domainSyncStatusForAccount(const QString &userIdAtHost) const; + +public slots: + void setVfsEnabledForAccount(const QString &userIdAtHost, const bool setEnabled); + + void createEvictionWindowForAccount(const QString &userIdAtHost); + void signalFileProviderDomain(const QString &userIdAtHost); + void createDebugArchive(const QString &userIdAtHost); + +signals: + void vfsEnabledAccountsChanged(); + void localStorageUsageForAccountChanged(const QString &userIdAtHost); + void remoteStorageUsageForAccountChanged(const QString &userIdAtHost); + void materialisedItemsForAccountChanged(const QString &userIdAtHost); + +private: + explicit FileProviderSettingsController(QObject *parent = nullptr); + + class MacImplementation; + MacImplementation *d; + + QHash _userInfos; +}; + +} // Mac + +} // OCC diff --git a/src/gui/macOS/fileprovidersettingscontroller_mac.mm b/src/gui/macOS/fileprovidersettingscontroller_mac.mm new file mode 100644 index 000000000000..17d277e5ef1a --- /dev/null +++ b/src/gui/macOS/fileprovidersettingscontroller_mac.mm @@ -0,0 +1,450 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "fileprovidersettingscontroller.h" + +#include +#include + +#include "gui/systray.h" +#include "gui/userinfo.h" +#include "gui/macOS/fileprovider.h" +#include "gui/macOS/fileprovideritemmetadata.h" +#include "gui/macOS/fileprovidermaterialiseditemsmodel.h" +#include "gui/macOS/fileproviderutils.h" + +// Objective-C imports +#import + +#import "fileproviderstorageuseenumerationobserver.h" +// End of Objective-C imports + +namespace { +constexpr auto fpSettingsQmlPath = "qrc:/qml/src/gui/macOS/ui/FileProviderSettings.qml"; +constexpr auto fpEvictionDialogQmlPath = "qrc:/qml/src/gui/macOS/ui/FileProviderEvictionDialog.qml"; + +// QML properties -- make sure they match up in QML file! +constexpr auto fpAccountUserIdAtHostProp = "accountUserIdAtHost"; +constexpr auto fpMaterialisedItemsModelProp = "materialisedItemsModel"; + +// NSUserDefaults entries +constexpr auto enabledAccountsSettingsKey = "enabledAccounts"; + +float gbFromBytesWithOneDecimal(const unsigned long long bytes) +{ + constexpr auto bytesIn100Mb = 1ULL * 1000ULL * 1000ULL * 100ULL; + return ((bytes * 1.0) / bytesIn100Mb) / 10.0; +} +} // namespace + +namespace OCC { + +namespace Mac { + +Q_LOGGING_CATEGORY(lcFileProviderSettingsController, "nextcloud.gui.mac.fileprovider.settingscontroller") + +class FileProviderSettingsController::MacImplementation : public QObject +{ +public: + enum class VfsAccountsAction { + VfsAccountsNoAction, + VfsAccountsEnabledChanged, + }; + + explicit MacImplementation(FileProviderSettingsController *const parent) + { + q = parent; + initialCheck(); + fetchMaterialisedFilesStorageUsage(); + }; + + ~MacImplementation() override = default; + + [[nodiscard]] QStringList enabledAccounts() const + { + QStringList qEnabledAccounts; + NSArray *const enabledAccounts = nsEnabledAccounts(); + for (NSString *const userIdAtHostString in enabledAccounts) { + qEnabledAccounts.append(QString::fromNSString(userIdAtHostString)); + } + return qEnabledAccounts; + } + + [[nodiscard]] bool vfsEnabledForAccount(const QString &userIdAtHost) const + { + NSArray *const vfsEnabledAccounts = nsEnabledAccounts(); + return [vfsEnabledAccounts containsObject:userIdAtHost.toNSString()]; + } + + [[nodiscard]] VfsAccountsAction setVfsEnabledForAccount(const QString &userIdAtHost, const bool setEnabled) const + { + NSArray *vfsEnabledAccounts = nsEnabledAccounts(); + + qCInfo(lcFileProviderSettingsController) << "Setting file provider-based vfs of account" + << userIdAtHost + << "to" + << setEnabled; + + if (vfsEnabledAccounts == nil) { + qCDebug(lcFileProviderSettingsController) << "Received nil array for accounts, creating new array"; + vfsEnabledAccounts = NSArray.array; + } + + NSString *const nsUserIdAtHost = userIdAtHost.toNSString(); + const BOOL accountEnabled = [vfsEnabledAccounts containsObject:nsUserIdAtHost]; + + if (accountEnabled == setEnabled) { + qCDebug(lcFileProviderSettingsController) << "VFS enablement status for" + << userIdAtHost + << "matches config."; + return VfsAccountsAction::VfsAccountsNoAction; + } + + NSMutableArray *const mutableVfsAccounts = vfsEnabledAccounts.mutableCopy; + + if (setEnabled) { + [mutableVfsAccounts addObject:nsUserIdAtHost]; + } else { + [mutableVfsAccounts removeObject:nsUserIdAtHost]; + } + + NSArray *const modifiedVfsAccounts = mutableVfsAccounts.copy; + [_userDefaults setObject:modifiedVfsAccounts forKey:_accountsKey]; + + Q_ASSERT(vfsEnabledForAccount(userIdAtHost) == userIdAtHost); + + return VfsAccountsAction::VfsAccountsEnabledChanged; + } + + [[nodiscard]] VfsAccountsAction enableVfsForAllAccounts() const + { + const auto accManager = AccountManager::instance(); + const auto accountsList = accManager->accounts(); + + if (accountsList.count() == 0) { + return VfsAccountsAction::VfsAccountsNoAction; + } + + auto overallActResult = VfsAccountsAction::VfsAccountsNoAction; + + for (const auto &account : accountsList) { + const auto qAccountUserIdAtHost = account->account()->userIdAtHostWithPort(); + const auto accountActResult = setVfsEnabledForAccount(qAccountUserIdAtHost, true); + + if (accountActResult == VfsAccountsAction::VfsAccountsEnabledChanged) { + overallActResult = accountActResult; + } + } + + return overallActResult; + } + + [[nodiscard]] unsigned long long localStorageUsageForAccount(const QString &userIdAtHost) const + { + // Return cached value as we fetch asynchronously on initialisation of this class. + // We will then emit a signal when the new value is found. + return _storageUsage.value(userIdAtHost); + } + + [[nodiscard]] QVector materialisedItemsForAccount(const QString &userIdAtHost) const + { + return _materialisedFiles.value(userIdAtHost); + } + + void signalFileProviderDomain(const QString &userIdAtHost) const + { + qCInfo(lcFileProviderSettingsController) << "Signalling file provider domain" << userIdAtHost; + NSFileProviderDomain * const domain = FileProviderUtils::domainForIdentifier(userIdAtHost); + NSFileProviderManager * const manager = [NSFileProviderManager managerForDomain:domain]; + [manager signalEnumeratorForContainerItemIdentifier:NSFileProviderRootContainerItemIdentifier + completionHandler:^(NSError *const error) { + if (error != nil) { + qCWarning(lcFileProviderSettingsController) << "Could not signal file provider domain, error" + << error.localizedDescription; + return; + } + + qCInfo(lcFileProviderSettingsController) << "Successfully signalled file provider domain"; + // TODO: Provide some feedback in the UI + }]; + } + + [[nodiscard]] FileProviderDomainSyncStatus *domainSyncStatusForAccount(const QString &userIdAtHost) const + { + return _fileProviderDomainSyncStatuses.value(userIdAtHost); + } + +private slots: + void updateDomainSyncStatuses() + { + qCInfo(lcFileProviderSettingsController) << "Updating domain sync statuses"; + _fileProviderDomainSyncStatuses.clear(); + const auto enabledAccounts = nsEnabledAccounts(); + for (NSString *const domainIdentifier in enabledAccounts) { + const auto qDomainIdentifier = QString::fromNSString(domainIdentifier); + const auto syncStatus = new FileProviderDomainSyncStatus(qDomainIdentifier, q); + _fileProviderDomainSyncStatuses.insert(qDomainIdentifier, syncStatus); + } + } + +private: + [[nodiscard]] NSArray *nsEnabledAccounts() const + { + return (NSArray *)[_userDefaults objectForKey:_accountsKey]; + } + + void fetchMaterialisedFilesStorageUsage() + { + qCInfo(lcFileProviderSettingsController) << "Fetching materialised files storage usage"; + + [NSFileProviderManager getDomainsWithCompletionHandler: ^(NSArray *const domains, NSError *const error) { + if (error != nil) { + qCWarning(lcFileProviderSettingsController) << "Could not get file provider domains:" + << error.localizedDescription + << "Will try again in 2 secs"; + + // HACK: Sometimes the system is not in a state where it wants to give us access to + // the file provider domains. We will try again in 2 seconds and hope it works + const auto thisQobject = (QObject*)this; + dispatch_async(dispatch_get_main_queue(), ^{ + [NSTimer scheduledTimerWithTimeInterval:2 repeats:NO block:^(NSTimer *const timer) { + Q_UNUSED(timer) + QMetaObject::invokeMethod(thisQobject, [this] { fetchMaterialisedFilesStorageUsage(); }); + }]; + }); + return; + } + + for (NSFileProviderDomain *const domain in domains) { + qCInfo(lcFileProviderSettingsController) << "Checking storage use for domain:" << domain.identifier; + + NSFileProviderManager *const managerForDomain = [NSFileProviderManager managerForDomain:domain]; + if (managerForDomain == nil) { + qCWarning(lcFileProviderSettingsController) << "Got a nil file provider manager for domain" + << domain.identifier + << ", returning early."; + return; + } + + const id enumerator = [managerForDomain enumeratorForMaterializedItems]; + Q_ASSERT(enumerator != nil); + [enumerator retain]; + + FileProviderStorageUseEnumerationObserver *const storageUseObserver = [[FileProviderStorageUseEnumerationObserver alloc] init]; + [storageUseObserver retain]; + storageUseObserver.enumerationFinishedHandler = ^(NSError *const error) { + qCInfo(lcFileProviderSettingsController) << "Enumeration finished for" << domain.identifier; + if (error != nil) { + qCWarning(lcFileProviderSettingsController) << "Error while enumerating storage use" << error.localizedDescription; + [storageUseObserver release]; + [enumerator release]; + return; + } + + const auto items = storageUseObserver.materialisedItems; + Q_ASSERT(items != nil); + + // Remember that OCC::Account::userIdAtHost == domain.identifier for us + const auto qDomainIdentifier = QString::fromNSString(domain.identifier); + QVector qMaterialisedItems; + qMaterialisedItems.reserve(items.count); + for (const id item in items) { + const auto itemMetadata = FileProviderItemMetadata::fromNSFileProviderItem(item, qDomainIdentifier); + const auto storageUsage = _storageUsage.value(qDomainIdentifier) + itemMetadata.documentSize(); + qCDebug(lcFileProviderSettingsController) << "Adding item" << itemMetadata.identifier() + << "with size" << itemMetadata.documentSize() + << "to storage usage for account" << qDomainIdentifier + << "with total size" << storageUsage; + qMaterialisedItems.append(itemMetadata); + _storageUsage.insert(qDomainIdentifier, storageUsage); + } + _materialisedFiles.insert(qDomainIdentifier, qMaterialisedItems); + + emit q->localStorageUsageForAccountChanged(qDomainIdentifier); + emit q->materialisedItemsForAccountChanged(qDomainIdentifier); + + [storageUseObserver release]; + [enumerator release]; + }; + [enumerator enumerateItemsForObserver:storageUseObserver startingAtPage:NSFileProviderInitialPageSortedByName]; + } + }]; + } + + void initialCheck() + { + qCInfo(lcFileProviderSettingsController) << "Running initial checks for file provider settings controller."; + + NSArray *const vfsEnabledAccounts = nsEnabledAccounts(); + if (vfsEnabledAccounts != nil) { + updateDomainSyncStatuses(); + connect(q, &FileProviderSettingsController::vfsEnabledAccountsChanged, this, &MacImplementation::updateDomainSyncStatuses); + return; + } + + qCInfo(lcFileProviderSettingsController) << "Initial check for file provider settings found nil enabled vfs accounts array." + << "Enabling all accounts on initial setup."; + + [[maybe_unused]] const auto result = enableVfsForAllAccounts(); + } + + FileProviderSettingsController *q = nullptr; + NSUserDefaults *_userDefaults = NSUserDefaults.standardUserDefaults; + NSString *_accountsKey = [NSString stringWithUTF8String:enabledAccountsSettingsKey]; + QHash> _materialisedFiles; + QHash _storageUsage; + QHash _fileProviderDomainSyncStatuses; +}; + +FileProviderSettingsController *FileProviderSettingsController::instance() +{ + static FileProviderSettingsController controller; + return &controller; +} + +FileProviderSettingsController::FileProviderSettingsController(QObject *parent) + : QObject{parent} + , d(new FileProviderSettingsController::MacImplementation(this)) +{ + const auto accManager = AccountManager::instance(); + const auto accountsList = accManager->accounts(); + + for (const auto &accountState : accountsList) { + const auto userInfo = new UserInfo(accountState.data(), false, false, this); + const auto account = accountState->account(); + const auto accountUserIdAtHost = account->userIdAtHostWithPort(); + + _userInfos.insert(accountUserIdAtHost, userInfo); + connect(userInfo, &UserInfo::fetchedLastInfo, this, [this, accountUserIdAtHost] { + emit remoteStorageUsageForAccountChanged(accountUserIdAtHost); + }); + userInfo->setActive(true); + } +} + +QQuickWidget *FileProviderSettingsController::settingsViewWidget(const QString &accountUserIdAtHost, + QWidget *const parent, + const QQuickWidget::ResizeMode resizeMode) +{ + const auto settingsViewWidget = new QQuickWidget(Systray::instance()->trayEngine(), parent); + settingsViewWidget->setResizeMode(resizeMode); + settingsViewWidget->setSource(QUrl(fpSettingsQmlPath)); + settingsViewWidget->rootObject()->setProperty(fpAccountUserIdAtHostProp, accountUserIdAtHost); + return settingsViewWidget; +} + +QStringList FileProviderSettingsController::vfsEnabledAccounts() const +{ + return d->enabledAccounts(); +} + +bool FileProviderSettingsController::vfsEnabledForAccount(const QString &userIdAtHost) const +{ + return d->vfsEnabledForAccount(userIdAtHost); +} + +void FileProviderSettingsController::setVfsEnabledForAccount(const QString &userIdAtHost, const bool setEnabled) +{ + const auto enabledAccountsAction = d->setVfsEnabledForAccount(userIdAtHost, setEnabled); + if (enabledAccountsAction == MacImplementation::VfsAccountsAction::VfsAccountsEnabledChanged) { + emit vfsEnabledAccountsChanged(); + } +} + +unsigned long long FileProviderSettingsController::localStorageUsageForAccount(const QString &userIdAtHost) const +{ + return d->localStorageUsageForAccount(userIdAtHost); +} + +float FileProviderSettingsController::localStorageUsageGbForAccount(const QString &userIdAtHost) const +{ + return gbFromBytesWithOneDecimal(localStorageUsageForAccount(userIdAtHost)); +} + +unsigned long long FileProviderSettingsController::remoteStorageUsageForAccount(const QString &userIdAtHost) const +{ + const auto userInfoForAccount = _userInfos.value(userIdAtHost); + if (!userInfoForAccount) { + return 0; + } + + return userInfoForAccount->lastQuotaUsedBytes(); +} + +float FileProviderSettingsController::remoteStorageUsageGbForAccount(const QString &userIdAtHost) const +{ + return gbFromBytesWithOneDecimal(remoteStorageUsageForAccount(userIdAtHost)); +} + +QAbstractListModel *FileProviderSettingsController::materialisedItemsModelForAccount(const QString &userIdAtHost) +{ + const auto items = d->materialisedItemsForAccount(userIdAtHost); + if (items.isEmpty()) { + return nullptr; + } + + const auto model = new FileProviderMaterialisedItemsModel(this); + model->setItems(items); + + connect(this, &FileProviderSettingsController::materialisedItemsForAccountChanged, model, [this, model, userIdAtHost](const QString &accountUserIdAtHost) { + if (accountUserIdAtHost != userIdAtHost) { + return; + } + + const auto items = d->materialisedItemsForAccount(userIdAtHost); + model->setItems(items); + }); + + return model; +} + +void FileProviderSettingsController::createEvictionWindowForAccount(const QString &userIdAtHost) +{ + const auto engine = Systray::instance()->trayEngine(); + QQmlComponent component(engine, QUrl(fpEvictionDialogQmlPath)); + const auto model = materialisedItemsModelForAccount(userIdAtHost); + const auto genericDialog = component.createWithInitialProperties({ + {fpAccountUserIdAtHostProp, userIdAtHost}, + {fpMaterialisedItemsModelProp, QVariant::fromValue(model)}, + }); + const auto dialog = qobject_cast(genericDialog); + Q_ASSERT(dialog); + dialog->show(); +} + +void FileProviderSettingsController::signalFileProviderDomain(const QString &userIdAtHost) +{ + d->signalFileProviderDomain(userIdAtHost); +} + +void FileProviderSettingsController::createDebugArchive(const QString &userIdAtHost) +{ + const auto filename = QFileDialog::getSaveFileName(nullptr, + tr("Create Debug Archive"), + QStandardPaths::writableLocation(QStandardPaths::StandardLocation::DocumentsLocation), + tr("Text files") + " (*.txt)"); + if (filename.isEmpty()) { + return; + } + FileProvider::instance()->createDebugArchiveForDomain(userIdAtHost, filename); +} + +FileProviderDomainSyncStatus *FileProviderSettingsController::domainSyncStatusForAccount(const QString &userIdAtHost) const +{ + return d->domainSyncStatusForAccount(userIdAtHost); +} + +} // namespace Mac + +} // namespace OCC diff --git a/src/gui/macOS/fileproviderstorageuseenumerationobserver.h b/src/gui/macOS/fileproviderstorageuseenumerationobserver.h new file mode 100644 index 000000000000..149d4249a6d4 --- /dev/null +++ b/src/gui/macOS/fileproviderstorageuseenumerationobserver.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#import +#import + +typedef void(^UsageEnumerationFinishedHandler)(NSError *const error); + +@interface FileProviderStorageUseEnumerationObserver : NSObject + +@property (readwrite, strong) UsageEnumerationFinishedHandler enumerationFinishedHandler; +@property (readonly) NSSet> *materialisedItems; + +@end diff --git a/src/gui/macOS/fileproviderstorageuseenumerationobserver.m b/src/gui/macOS/fileproviderstorageuseenumerationobserver.m new file mode 100644 index 000000000000..619f632cd015 --- /dev/null +++ b/src/gui/macOS/fileproviderstorageuseenumerationobserver.m @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#import "fileproviderstorageuseenumerationobserver.h" + +@implementation FileProviderStorageUseEnumerationObserver + +- (instancetype)init +{ + self = [super init]; + if (self) { + _materialisedItems = [NSSet set]; + } + return self; +} + +// NSFileProviderEnumerationObserver protocol methods +- (void)didEnumerateItems:(NSArray> *)updatedItems +{ + NSMutableSet> * const existingItems = self.materialisedItems.mutableCopy; + + for (const id item in updatedItems) { + NSLog(@"StorageUseEnumerationObserver: Enumerating %@ with size %llu", + item.filename, item.documentSize.unsignedLongLongValue); + [existingItems addObject:item]; + } + + _materialisedItems = existingItems.copy; +} + +- (void)finishEnumeratingWithError:(NSError *)error +{ + dispatch_async(dispatch_get_main_queue(), ^{ + self.enumerationFinishedHandler(error); + }); +} + +- (void)finishEnumeratingUpToPage:(NSFileProviderPage)nextPage +{ + dispatch_async(dispatch_get_main_queue(), ^{ + self.enumerationFinishedHandler(nil); + }); +} + +@end diff --git a/src/gui/macOS/fileproviderutils.h b/src/gui/macOS/fileproviderutils.h new file mode 100644 index 000000000000..063be6d6d4d7 --- /dev/null +++ b/src/gui/macOS/fileproviderutils.h @@ -0,0 +1,50 @@ +/* + * Copyright 2023 (c) Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#pragma once + +class QString; + +@class NSFileProviderDomain; +@class NSFileProviderManager; + +/** + * This file contains the FileProviderUtils namespace, which contains + * utility functions for the FileProvider extension. + * + * Unlike other classes or namespaces in this module, this does not have + * a clear file separation between C++ and Objective C++ code. + * This is intended as a completely Objective-C++ namespace! + * + * You should threfore try to avoid using this in C++ code wherever possible + * and only use this in *_mac.mm implementation files. + */ + +namespace OCC { + +namespace Mac { + +namespace FileProviderUtils { + +// Synchronous function to get the domain for a domain identifier +NSFileProviderDomain *domainForIdentifier(const QString &domainIdentifier); + +// Synchronous function to get manager for a domain identifier +NSFileProviderManager *managerForDomainIdentifier(const QString &domainIdentifier); + +} // namespace FileProviderUtils + +} // namespace Mac + +} // namespace OCC diff --git a/src/gui/macOS/fileproviderutils_mac.mm b/src/gui/macOS/fileproviderutils_mac.mm new file mode 100644 index 000000000000..1f3d046d9fcf --- /dev/null +++ b/src/gui/macOS/fileproviderutils_mac.mm @@ -0,0 +1,94 @@ +/* + * Copyright 2023 (c) Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "fileproviderutils.h" + +#include +#include + +#import + +namespace OCC { + +namespace Mac { + +namespace FileProviderUtils { + +Q_LOGGING_CATEGORY(lcMacFileProviderUtils, "nextcloud.gui.macfileproviderutils", QtInfoMsg) + +NSFileProviderDomain *domainForIdentifier(const QString &domainIdentifier) +{ + __block NSFileProviderDomain *foundDomain = nil; + NSString *const nsDomainIdentifier = domainIdentifier.toNSString(); + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + // getDomainsWithCompletionHandler is asynchronous -- we create a dispatch semaphore in order + // to wait until it is done. This should tell you that we should not call this method very + // often! + + [NSFileProviderManager getDomainsWithCompletionHandler:^(NSArray *const domains, NSError *const error) { + if (error != nil) { + qCWarning(lcMacFileProviderUtils) << "Error fetching domains:" + << error.localizedDescription; + dispatch_semaphore_signal(semaphore); + return; + } + + for (NSFileProviderDomain *const domain in domains) { + if ([domain.identifier isEqualToString:nsDomainIdentifier]) { + [domain retain]; + foundDomain = domain; + break; + } + } + + dispatch_semaphore_signal(semaphore); + }]; + + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + dispatch_release(semaphore); + + if (foundDomain == nil) { + qCWarning(lcMacFileProviderUtils) << "No matching item domain for identifier" + << domainIdentifier; + } + + return foundDomain; +} + +NSFileProviderManager *managerForDomainIdentifier(const QString &domainIdentifier) +{ + NSFileProviderDomain * const domain = domainForIdentifier(domainIdentifier); + if (domain == nil) { + qCWarning(lcMacFileProviderUtils) << "Received null domain for identifier" + << domainIdentifier + << "cannot acquire manager"; + return nil; + } + + NSFileProviderManager * const manager = [NSFileProviderManager managerForDomain:domain]; + if (manager == nil) { + qCWarning(lcMacFileProviderUtils) << "Received null manager for domain" + << domainIdentifier; + } + + [domain release]; + return manager; +} + +} // namespace FileProviderUtils + +} // namespace Mac + +} // namespace OCC diff --git a/src/gui/macOS/fileproviderxpc.h b/src/gui/macOS/fileproviderxpc.h index 25efb05ad289..9e234ae2ee3d 100644 --- a/src/gui/macOS/fileproviderxpc.h +++ b/src/gui/macOS/fileproviderxpc.h @@ -39,6 +39,7 @@ public slots: void configureExtensions(); void authenticateExtension(const QString &extensionAccountId) const; void unauthenticateExtension(const QString &extensionAccountId) const; + void createDebugArchiveForExtension(const QString &extensionAccountId, const QString &filename) const; private slots: void slotAccountStateChanged(AccountState::State state) const; diff --git a/src/gui/macOS/fileproviderxpc_mac.mm b/src/gui/macOS/fileproviderxpc_mac.mm index 399c1af6d4bf..260804b71bc3 100644 --- a/src/gui/macOS/fileproviderxpc_mac.mm +++ b/src/gui/macOS/fileproviderxpc_mac.mm @@ -102,5 +102,39 @@ break; } } +void FileProviderXPC::createDebugArchiveForExtension(const QString &extensionAccountId, const QString &filename) const +{ + qCInfo(lcFileProviderXPC) << "Creating debug archive for extension" << extensionAccountId << "at" << filename; + // You need to fetch the contents from the extension and then create the archive from the client side. + // The extension is not allowed to ask for permission to write into the file system as it is not a user facing process. + const auto clientCommService = (NSObject *)_clientCommServices.value(extensionAccountId); + const auto group = dispatch_group_create(); + __block NSString *rcvdDebugLogString; + dispatch_group_enter(group); + [clientCommService createDebugLogStringWithCompletionHandler:^(NSString *const debugLogString, NSError *const error) { + if (error != nil) { + qCWarning(lcFileProviderXPC) << "Error getting debug log string" << error.localizedDescription; + dispatch_group_leave(group); + return; + } else if (debugLogString == nil) { + qCWarning(lcFileProviderXPC) << "Debug log string is nil"; + dispatch_group_leave(group); + return; + } + rcvdDebugLogString = [NSString stringWithString:debugLogString]; + [rcvdDebugLogString retain]; + dispatch_group_leave(group); + }]; + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + + QFile debugLogFile(filename); + if (debugLogFile.open(QIODevice::WriteOnly)) { + debugLogFile.write(rcvdDebugLogString.UTF8String); + debugLogFile.close(); + qCInfo(lcFileProviderXPC) << "Debug log file written to" << filename; + } else { + qCWarning(lcFileProviderXPC) << "Could not open debug log file" << filename; + } +} } // namespace OCC::Mac diff --git a/src/gui/macOS/progressobserver.h b/src/gui/macOS/progressobserver.h new file mode 100644 index 000000000000..51b23ed92fbc --- /dev/null +++ b/src/gui/macOS/progressobserver.h @@ -0,0 +1,30 @@ +/* +* Copyright 2024 (c) Claudio Cambra +* +* 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. +* +* This program is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +* for more details. +*/ + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void(^ProgressKVOChangeHandler)(NSProgress *const progress); + +@interface ProgressObserver : NSObject + +@property (readonly) NSProgress *progress; +@property (readwrite, copy) ProgressKVOChangeHandler progressKVOChangeHandler; + +- (instancetype)initWithProgress:(NSProgress *)progress; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/gui/macOS/progressobserver.m b/src/gui/macOS/progressobserver.m new file mode 100644 index 000000000000..b52e13b26161 --- /dev/null +++ b/src/gui/macOS/progressobserver.m @@ -0,0 +1,42 @@ +/* +* Copyright 2024 (c) Claudio Cambra +* +* 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. +* +* This program is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +* for more details. +*/ + +#import "progressobserver.h" + +@implementation ProgressObserver + +- (instancetype)initWithProgress:(NSProgress *)progress +{ + self = [super init]; + if (self) { + _progress = progress; + [_progress addObserver:self forKeyPath:@"totalUnitCount" options:NSKeyValueObservingOptionNew context:nil]; + [_progress addObserver:self forKeyPath:@"completedUnitCount" options:NSKeyValueObservingOptionNew context:nil]; + [_progress addObserver:self forKeyPath:@"cancelled" options:NSKeyValueObservingOptionNew context:nil]; + [_progress addObserver:self forKeyPath:@"paused" options:NSKeyValueObservingOptionNew context:nil]; + [_progress addObserver:self forKeyPath:@"fileTotalCount" options:NSKeyValueObservingOptionNew context:nil]; + [_progress addObserver:self forKeyPath:@"fileCompletedCount" options:NSKeyValueObservingOptionNew context:nil]; + } + return self; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + self.progressKVOChangeHandler(self.progress); +} + +@end diff --git a/src/gui/macOS/ui/FileProviderEvictionDialog.qml b/src/gui/macOS/ui/FileProviderEvictionDialog.qml new file mode 100644 index 000000000000..5884b63770a9 --- /dev/null +++ b/src/gui/macOS/ui/FileProviderEvictionDialog.qml @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import Style 1.0 +import "../../filedetails" +import "../../tray" + +import com.nextcloud.desktopclient 1.0 + +ApplicationWindow { + id: root + + property var materialisedItemsModel: null + property string accountUserIdAtHost: "" + + title: qsTr("Evict materialised files") + flags: Qt.Dialog | Qt.WindowStaysOnTopHint + width: 640 + height: 480 + + ListView { + anchors.fill: parent + model: root.materialisedItemsModel + delegate: FileProviderFileDelegate { + width: parent.width + height: 60 + onEvictItem: root.materialisedItemsModel.evictItem(identifier, domainIdentifier) + } + } +} diff --git a/src/gui/macOS/ui/FileProviderFileDelegate.qml b/src/gui/macOS/ui/FileProviderFileDelegate.qml new file mode 100644 index 000000000000..25d812f6623e --- /dev/null +++ b/src/gui/macOS/ui/FileProviderFileDelegate.qml @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import Style 1.0 +import "../../filedetails" +import "../../tray" + +import com.nextcloud.desktopclient 1.0 + +Item { + id: root + + signal evictItem(string identifier, string domainIdentifier) + + // Match with model rolenames for automagic setting of properties + required property string identifier + required property string domainIdentifier + required property string fileName + required property string userVisiblePath + required property string fileTypeString + + required property string fileSizeString + + RowLayout { + id: internalLayout + + anchors.fill: parent + + Image { + id: fileIconImage + Layout.fillHeight: true + verticalAlignment: Image.AlignVCenter + horizontalAlignment: Image.AlignHCenter + source: "image://tray-image-provider/:/fileicon/" + root.userVisiblePath + sourceSize.width: Style.trayListItemIconSize + sourceSize.height: Style.trayListItemIconSize + fillMode: Image.PreserveAspectFit + } + + Column { + Layout.fillWidth: true + + EnforcedPlainTextLabel { + id: fileNameLabel + width: parent.width + text: root.fileName + } + + EnforcedPlainTextLabel { + id: pathLabel + width: parent.width + text: root.userVisiblePath + elide: Text.ElideLeft + } + + Row { + width: parent.width + spacing: Style.smallSpacing + + EnforcedPlainTextLabel { + id: fileSizeLabel + text: root.fileSizeString + font.bold: true + } + + EnforcedPlainTextLabel { + id: fileTypeLabel + text: root.fileTypeString + color: Style.ncSecondaryTextColor + } + } + } + + CustomButton { + id: deleteButton + + Layout.minimumWidth: implicitWidth + Layout.fillHeight: true + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + + text: qsTr("Delete") + bgColor: Style.errorBoxBackgroundColor + onClicked: root.evictItem(root.identifier, root.domainIdentifier) + } + } +} diff --git a/src/gui/macOS/ui/FileProviderSettings.qml b/src/gui/macOS/ui/FileProviderSettings.qml new file mode 100644 index 000000000000..f3844f2458c2 --- /dev/null +++ b/src/gui/macOS/ui/FileProviderSettings.qml @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import Style 1.0 +import "../../filedetails" +import "../../tray" + +import com.nextcloud.desktopclient 1.0 + +Page { + id: root + + property bool showBorder: true + property var controller: FileProviderSettingsController + property string accountUserIdAtHost: "" + + title: qsTr("Virtual files settings") + + // TODO: Rather than setting all these palette colours manually, + // create a custom style and do it for all components globally. + palette { + text: Style.ncTextColor + windowText: Style.ncTextColor + buttonText: Style.ncTextColor + brightText: Style.ncTextBrightColor + highlight: Style.lightHover + highlightedText: Style.ncTextColor + light: Style.lightHover + midlight: Style.ncSecondaryTextColor + mid: Style.darkerHover + dark: Style.menuBorder + button: Style.buttonBackgroundColor + window: Style.backgroundColor + base: Style.backgroundColor + toolTipBase: Style.backgroundColor + toolTipText: Style.ncTextColor + } + + background: Rectangle { + color: palette.window + border.width: root.showBorder ? Style.normalBorderWidth : 0 + border.color: root.palette.dark + } + + padding: Style.standardSpacing + + ColumnLayout { + anchors { + top: parent.top + left: parent.left + right: parent.right + } + + EnforcedPlainTextLabel { + Layout.fillWidth: true + text: qsTr("General settings") + font.bold: true + font.pointSize: Style.subheaderFontPtSize + elide: Text.ElideRight + } + + CheckBox { + id: vfsEnabledCheckBox + Layout.fillWidth: true + text: qsTr("Enable virtual files") + checked: root.controller.vfsEnabledForAccount(root.accountUserIdAtHost) + onClicked: root.controller.setVfsEnabledForAccount(root.accountUserIdAtHost, checked) + } + + Loader { + id: vfsSettingsLoader + + Layout.fillWidth: true + Layout.fillHeight: true + + active: vfsEnabledCheckBox.checked + sourceComponent: ColumnLayout { + Rectangle { + Layout.fillWidth: true + height: Style.normalBorderWidth + color: Style.ncSecondaryTextColor + } + + FileProviderSyncStatus { + syncStatus: root.controller.domainSyncStatusForAccount(root.accountUserIdAtHost) + } + + FileProviderStorageInfo { + id: storageInfo + localUsedStorage: root.controller.localStorageUsageGbForAccount(root.accountUserIdAtHost) + remoteUsedStorage: root.controller.remoteStorageUsageGbForAccount(root.accountUserIdAtHost) + + onEvictDialogRequested: root.controller.createEvictionWindowForAccount(root.accountUserIdAtHost) + + Connections { + target: root.controller + + function onLocalStorageUsageForAccountChanged(accountUserIdAtHost) { + if (root.accountUserIdAtHost !== accountUserIdAtHost) { + return; + } + storageInfo.localUsedStorage = root.controller.localStorageUsageGbForAccount(root.accountUserIdAtHost); + } + + function onRemoteStorageUsageForAccountChanged(accountUserIdAtHost) { + if (root.accountUserIdAtHost !== accountUserIdAtHost) { + return; + } + storageInfo.remoteUsedStorage = root.controller.remoteStorageUsageGbForAccount(root.accountUserIdAtHost); + } + } + } + + EnforcedPlainTextLabel { + Layout.fillWidth: true + Layout.topMargin: Style.standardSpacing + text: qsTr("Advanced") + font.bold: true + font.pointSize: Style.subheaderFontPtSize + elide: Text.ElideRight + } + + CustomButton { + text: qsTr("Signal file provider domain") + onClicked: root.controller.signalFileProviderDomain(root.accountUserIdAtHost) + } + + CustomButton { + text: qsTr("Create debug archive") + onClicked: root.controller.createDebugArchive(root.accountUserIdAtHost) + } + } + } + } +} diff --git a/src/gui/macOS/ui/FileProviderStorageInfo.qml b/src/gui/macOS/ui/FileProviderStorageInfo.qml new file mode 100644 index 000000000000..918950723927 --- /dev/null +++ b/src/gui/macOS/ui/FileProviderStorageInfo.qml @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 by Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import Style 1.0 +import "../../filedetails" +import "../../tray" + +import com.nextcloud.desktopclient 1.0 + +GridLayout { + id: root + + signal evictDialogRequested() + + required property real localUsedStorage + required property real remoteUsedStorage + + Layout.fillWidth: true + columns: 2 + + EnforcedPlainTextLabel { + Layout.row: 0 + Layout.column: 0 + Layout.alignment: Layout.AlignLeft | Layout.AlignVCenter + Layout.fillWidth: true + text: qsTr("Local storage use") + font.bold: true + } + + EnforcedPlainTextLabel { + Layout.row: 0 + Layout.column: 1 + Layout.alignment: Layout.AlignRight | Layout.AlignVCenter + text: qsTr("%1 GB of %2 GB remote files synced").arg(root.localUsedStorage.toFixed(2)).arg(root.remoteUsedStorage.toFixed(2)); + color: Style.ncSecondaryTextColor + horizontalAlignment: Text.AlignRight + } + + ProgressBar { + Layout.row: 1 + Layout.columnSpan: root.columns + Layout.fillWidth: true + value: root.localUsedStorage / root.remoteUsedStorage + } +} \ No newline at end of file diff --git a/src/gui/macOS/ui/FileProviderSyncStatus.qml b/src/gui/macOS/ui/FileProviderSyncStatus.qml new file mode 100644 index 000000000000..11f093bbcd88 --- /dev/null +++ b/src/gui/macOS/ui/FileProviderSyncStatus.qml @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 by Claudio Cambra + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import Style 1.0 +import "../../filedetails" +import "../../tray" + +import com.nextcloud.desktopclient 1.0 + +GridLayout { + id: root + + required property var syncStatus + + rows: syncStatus.syncing ? 2 : 1 + + NCBusyIndicator { + id: syncIcon + + property int size: Style.trayListItemIconSize * 0.8 + + Layout.row: 0 + Layout.rowSpan: root.syncStatus.syncing ? 2 : 1 + Layout.column: 0 + Layout.preferredWidth: size + Layout.preferredHeight: size + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + padding: 0 + spacing: 0 + imageSource: root.syncStatus.icon + running: root.syncStatus.syncing + } + + EnforcedPlainTextLabel { + Layout.row: 0 + Layout.column: 1 + Layout.columnSpan: root.syncStatus.syncing ? 2 : 1 + Layout.fillWidth: true + font.bold: true + font.pointSize: Style.headerFontPtSize + text: root.syncStatus.syncing ? qsTr("Syncing") : qsTr("All synced!") + } + + NCProgressBar { + Layout.row: 1 + Layout.column: 1 + Layout.fillWidth: true + value: root.syncStatus.fractionCompleted + visible: root.syncStatus.syncing + } +} \ No newline at end of file diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index 6749df8a6e91..c14b5125a086 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -62,6 +62,10 @@ #include #include +#ifdef BUILD_FILE_PROVIDER_MODULE +#include "macOS/fileprovidersettingscontroller.h" +#endif + namespace OCC { Q_LOGGING_CATEGORY(lcOwnCloudGui, "com.nextcloud.owncloudgui") @@ -146,6 +150,10 @@ ownCloudGui::ownCloudGui(Application *parent) qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserAppsModel", UserAppsModel::instance()); qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "Theme", Theme::instance()); qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "Systray", Systray::instance()); + +#ifdef BUILD_FILE_PROVIDER_MODULE + qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "FileProviderSettingsController", Mac::FileProviderSettingsController::instance()); +#endif } void ownCloudGui::createTray() diff --git a/src/gui/settingsdialog.cpp b/src/gui/settingsdialog.cpp index 130e5deac1c7..8f8b1e2f8f76 100644 --- a/src/gui/settingsdialog.cpp +++ b/src/gui/settingsdialog.cpp @@ -39,6 +39,7 @@ #include #include #include +#include namespace { const QString TOOLBAR_CSS() @@ -132,8 +133,6 @@ SettingsDialog::SettingsDialog(ownCloudGui *gui, QWidget *parent) auto *networkSettings = new NetworkSettings; _ui->stack->addWidget(networkSettings); - connect(_ui->stack, &QStackedWidget::currentChanged, this, &SettingsDialog::currentPageChanged); - _actionGroupWidgets.insert(generalAction, generalSettings); _actionGroupWidgets.insert(networkAction, networkSettings); diff --git a/src/gui/settingsdialog.h b/src/gui/settingsdialog.h index 6034e21bbf99..f992d50ea7b3 100644 --- a/src/gui/settingsdialog.h +++ b/src/gui/settingsdialog.h @@ -28,11 +28,11 @@ class QStandardItemModel; namespace OCC { -class AccountState; - namespace Ui { class SettingsDialog; } + +class AccountState; class AccountSettings; class Application; class FolderMan; diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index 510435ebf1b0..fd526682db6d 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -59,9 +59,14 @@ Systray *Systray::instance() return _instance; } +QQmlApplicationEngine *Systray::trayEngine() const +{ + return _trayEngine.get(); +} + void Systray::setTrayEngine(QQmlApplicationEngine *trayEngine) { - _trayEngine = trayEngine; + _trayEngine = std::make_unique(trayEngine); _trayEngine->setNetworkAccessManagerFactory(&_accessManagerFactory); @@ -112,7 +117,7 @@ void Systray::create() _trayEngine->rootContext()->setContextProperty("activityModel", UserModel::instance()->currentActivityModel()); } - QQmlComponent trayWindowComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/tray/Window.qml")); + QQmlComponent trayWindowComponent(trayEngine(), QStringLiteral("qrc:/qml/src/gui/tray/Window.qml")); if(trayWindowComponent.isError()) { qCWarning(lcSystray) << trayWindowComponent.errorString(); @@ -243,7 +248,7 @@ void Systray::createCallDialog(const Activity &callNotification, const AccountSt {"link", callNotification._link}, }; - const auto callDialog = new QQmlComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/tray/CallNotificationDialog.qml")); + const auto callDialog = new QQmlComponent(trayEngine(), QStringLiteral("qrc:/qml/src/gui/tray/CallNotificationDialog.qml")); if(callDialog->isError()) { qCWarning(lcSystray) << callDialog->errorString(); @@ -265,7 +270,7 @@ void Systray::createEditFileLocallyLoadingDialog(const QString &fileName) qCDebug(lcSystray) << "Opening a file local editing dialog..."; - const auto editFileLocallyLoadingDialog = new QQmlComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/tray/EditFileLocallyLoadingDialog.qml")); + const auto editFileLocallyLoadingDialog = new QQmlComponent(trayEngine(), QStringLiteral("qrc:/qml/src/gui/tray/EditFileLocallyLoadingDialog.qml")); if (editFileLocallyLoadingDialog->isError()) { qCWarning(lcSystray) << editFileLocallyLoadingDialog->errorString(); @@ -287,7 +292,7 @@ void Systray::destroyEditFileLocallyLoadingDialog() void Systray::createResolveConflictsDialog(const OCC::ActivityList &allConflicts) { - const auto conflictsDialog = std::make_unique(_trayEngine, QStringLiteral("qrc:/qml/src/gui/ResolveConflictsDialog.qml")); + const auto conflictsDialog = std::make_unique(trayEngine(), QStringLiteral("qrc:/qml/src/gui/ResolveConflictsDialog.qml")); const QVariantMap initialProperties{ {"allConflicts", QVariant::fromValue(allConflicts)}, }; @@ -378,7 +383,7 @@ void Systray::createFileDetailsDialog(const QString &localPath) {"localPath", localPath}, }; - QQmlComponent fileDetailsDialog(_trayEngine, QStringLiteral("qrc:/qml/src/gui/filedetails/FileDetailsWindow.qml")); + QQmlComponent fileDetailsDialog(trayEngine(), QStringLiteral("qrc:/qml/src/gui/filedetails/FileDetailsWindow.qml")); if (!fileDetailsDialog.isError()) { const auto createdDialog = fileDetailsDialog.createWithInitialProperties(initialProperties); diff --git a/src/gui/systray.h b/src/gui/systray.h index 87592e935dc4..605286084089 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -60,8 +60,7 @@ double menuBarThickness(); * @brief The Systray class * @ingroup gui */ -class Systray - : public QSystemTrayIcon +class Systray : public QSystemTrayIcon { Q_OBJECT @@ -97,6 +96,8 @@ class Systray bool raiseDialogs(); + [[nodiscard]] QQmlApplicationEngine* trayEngine() const; + signals: void currentUserChanged(); void openAccountWizard(); @@ -176,7 +177,7 @@ private slots: bool _isOpen = false; bool _syncIsPaused = true; - QPointer _trayEngine; + std::unique_ptr _trayEngine; QPointer _contextMenu; QSharedPointer _trayWindow; diff --git a/src/libsync/configfile.cpp b/src/libsync/configfile.cpp index 7583956d8f5d..d46ad50f0e87 100644 --- a/src/libsync/configfile.cpp +++ b/src/libsync/configfile.cpp @@ -111,8 +111,6 @@ static constexpr char certPath[] = "http_certificatePath"; static constexpr char certPasswd[] = "http_certificatePasswd"; static const QSet validUpdateChannels { QStringLiteral("stable"), QStringLiteral("beta") }; - -static constexpr auto macFileProviderModuleEnabledC = "macFileProviderModuleEnabled"; } namespace OCC { @@ -1229,16 +1227,4 @@ void ConfigFile::setDiscoveredLegacyConfigPath(const QString &discoveredLegacyCo _discoveredLegacyConfigPath = discoveredLegacyConfigPath; } -bool ConfigFile::macFileProviderModuleEnabled() const -{ - QSettings settings(configFile(), QSettings::IniFormat); - return settings.value(macFileProviderModuleEnabledC, false).toBool(); -} - -void ConfigFile::setMacFileProviderModuleEnabled(const bool moduleEnabled) -{ - QSettings settings(configFile(), QSettings::IniFormat); - settings.setValue(QLatin1String(macFileProviderModuleEnabledC), moduleEnabled); -} - } diff --git a/src/libsync/configfile.h b/src/libsync/configfile.h index 7ae1f98df207..97ec16e2f5ad 100644 --- a/src/libsync/configfile.h +++ b/src/libsync/configfile.h @@ -236,9 +236,6 @@ class OWNCLOUDSYNC_EXPORT ConfigFile [[nodiscard]] static QString discoveredLegacyConfigPath(); static void setDiscoveredLegacyConfigPath(const QString &discoveredLegacyConfigPath); - [[nodiscard]] bool macFileProviderModuleEnabled() const; - void setMacFileProviderModuleEnabled(const bool moduleEnabled); - protected: [[nodiscard]] QVariant getPolicySetting(const QString &policy, const QVariant &defaultValue = QVariant()) const; void storeData(const QString &group, const QString &key, const QVariant &value); diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml index 4d1360c31e0a..9dd321c4b3a8 100644 --- a/theme/Style/Style.qml +++ b/theme/Style/Style.qml @@ -33,6 +33,10 @@ QtObject { // We are using pixel size because this is cross platform comparable, point size isn't readonly property int topLinePixelSize: pixelSize readonly property int subLinePixelSize: topLinePixelSize - 2 + readonly property int defaultFontPtSize: fontMetrics.font.pointSize + readonly property int subheaderFontPtSize: defaultFontPtSize + 2 + readonly property int headerFontPtSize: defaultFontPtSize + 4 + readonly property int titleFontPtSize: defaultFontPtSize + 8 // Dimensions and sizes property int trayWindowWidth: variableSize(400)