From 4c11f6763e24d15113a08ec45fe27b83171aedc2 Mon Sep 17 00:00:00 2001 From: Felix Weilbach Date: Tue, 14 Sep 2021 13:17:03 +0200 Subject: [PATCH] Show sync progress in main dialog Fixes #3662 Signed-off-by: Felix Weilbach --- resources.qrc | 1 + src/gui/CMakeLists.txt | 1 + src/gui/main.cpp | 2 + src/gui/tray/ActivityItem.qml | 8 +- src/gui/tray/SyncStatus.qml | 89 ++++++++ src/gui/tray/UserModel.cpp | 5 + src/gui/tray/UserModel.h | 2 + src/gui/tray/Window.qml | 36 ++-- src/gui/tray/syncstatussummary.cpp | 314 +++++++++++++++++++++++++++++ src/gui/tray/syncstatussummary.h | 85 ++++++++ src/libsync/theme.cpp | 32 ++- src/libsync/theme.h | 12 ++ 12 files changed, 568 insertions(+), 19 deletions(-) create mode 100644 src/gui/tray/SyncStatus.qml create mode 100644 src/gui/tray/syncstatussummary.cpp create mode 100644 src/gui/tray/syncstatussummary.h diff --git a/resources.qrc b/resources.qrc index 0b95c4274..134c11cc9 100644 --- a/resources.qrc +++ b/resources.qrc @@ -7,6 +7,7 @@ src/gui/tray/Window.qml src/gui/tray/UserLine.qml src/gui/tray/HeaderButton.qml + src/gui/tray/SyncStatus.qml theme/Style/Style.qml theme/Style/qmldir src/gui/tray/ActivityActionButton.qml diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 59713e6d6..bbc666374 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -113,6 +113,7 @@ set(client_SRCS userstatusselectormodel.cpp emojimodel.cpp fileactivitylistmodel.cpp + tray/syncstatussummary.cpp tray/ActivityData.cpp tray/ActivityListModel.cpp tray/UserModel.cpp diff --git a/src/gui/main.cpp b/src/gui/main.cpp index 71e53abbe..47421220b 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -30,6 +30,7 @@ #include "cocoainitializer.h" #include "userstatusselectormodel.h" #include "emojimodel.h" +#include "tray/syncstatussummary.h" #if defined(BUILD_UPDATER) #include "updater/updater.h" @@ -59,6 +60,7 @@ int main(int argc, char **argv) Q_INIT_RESOURCE(resources); Q_INIT_RESOURCE(theme); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SyncStatusSummary"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "EmojiModel"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "UserStatusSelectorModel"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "ActivityListModel"); diff --git a/src/gui/tray/ActivityItem.qml b/src/gui/tray/ActivityItem.qml index e84976e62..ed85ed9ea 100644 --- a/src/gui/tray/ActivityItem.qml +++ b/src/gui/tray/ActivityItem.qml @@ -18,7 +18,6 @@ MouseArea { Rectangle { anchors.fill: parent - anchors.margins: 2 color: (parent.containsMouse ? Style.lightHover : "transparent") } @@ -41,7 +40,7 @@ MouseArea { Image { id: activityIcon Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - Layout.leftMargin: 8 + Layout.leftMargin: 20 Layout.preferredWidth: shareButton.icon.width Layout.preferredHeight: shareButton.icon.height verticalAlignment: Qt.AlignCenter @@ -53,13 +52,12 @@ MouseArea { Column { id: activityTextColumn - Layout.leftMargin: 8 + Layout.leftMargin: 14 Layout.topMargin: 4 Layout.bottomMargin: 4 Layout.fillWidth: true - Layout.fillHeight: true spacing: 4 - Layout.alignment: Qt.AlignLeft + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Text { id: activityTextTitle diff --git a/src/gui/tray/SyncStatus.qml b/src/gui/tray/SyncStatus.qml new file mode 100644 index 000000000..7bf9a2d55 --- /dev/null +++ b/src/gui/tray/SyncStatus.qml @@ -0,0 +1,89 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import Style 1.0 + +import com.nextcloud.desktopclient 1.0 as NC + +RowLayout { + id: layout + + property alias model: syncStatus + + spacing: 0 + + NC.SyncStatusSummary { + id: syncStatus + } + + Image { + id: syncIcon + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.topMargin: 16 + Layout.bottomMargin: 16 + Layout.leftMargin: 16 + + source: syncStatus.syncIcon + sourceSize.width: 32 + sourceSize.height: 32 + rotation: syncStatus.syncing ? 0 : 0 + } + + RotationAnimator { + target: syncIcon + running: syncStatus.syncing + from: 0 + to: 360 + loops: Animation.Infinite + duration: 3000 + } + + ColumnLayout { + id: syncProgressLayout + + Layout.alignment: Qt.AlignVCenter + Layout.topMargin: 8 + Layout.rightMargin: 16 + Layout.leftMargin: 10 + Layout.bottomMargin: 8 + Layout.fillWidth: true + Layout.fillHeight: true + + Text { + id: syncProgressText + + Layout.fillWidth: true + + text: syncStatus.syncStatusString + verticalAlignment: Text.AlignVCenter + font.pixelSize: Style.topLinePixelSize + font.bold: true + } + + Loader { + Layout.fillWidth: true + + active: syncStatus.syncing; + visible: syncStatus.syncing + + sourceComponent: ProgressBar { + id: syncProgressBar + + value: syncStatus.syncProgress + } + } + + Text { + id: syncProgressDetailText + + Layout.fillWidth: true + + text: syncStatus.syncStatusDetailString + visible: syncStatus.syncStatusDetailString !== "" + color: "#808080" + font.pixelSize: Style.subLinePixelSize + } + } +} diff --git a/src/gui/tray/UserModel.cpp b/src/gui/tray/UserModel.cpp index 56a387aa5..c9e3d92b5 100644 --- a/src/gui/tray/UserModel.cpp +++ b/src/gui/tray/UserModel.cpp @@ -563,6 +563,11 @@ AccountPtr User::account() const return _account->account(); } +AccountStatePtr User::accountState() const +{ + return _account; +} + void User::setCurrentUser(const bool &isCurrent) { _isCurrentUser = isCurrent; diff --git a/src/gui/tray/UserModel.h b/src/gui/tray/UserModel.h index c11b2c476..eb3be9136 100644 --- a/src/gui/tray/UserModel.h +++ b/src/gui/tray/UserModel.h @@ -9,6 +9,7 @@ #include #include "ActivityListModel.h" +#include "accountfwd.h" #include "accountmanager.h" #include "folderman.h" #include "NotificationCache.h" @@ -36,6 +37,7 @@ public: User(AccountStatePtr &account, const bool &isCurrent = false, QObject *parent = nullptr); AccountPtr account() const; + AccountStatePtr accountState() const; bool isConnected() const; bool isCurrentUser() const; diff --git a/src/gui/tray/Window.qml b/src/gui/tray/Window.qml index 57a019341..5bda9242d 100644 --- a/src/gui/tray/Window.qml +++ b/src/gui/tray/Window.qml @@ -50,12 +50,14 @@ Window { // see also id:accountMenu below userLineInstantiator.active = false; userLineInstantiator.active = true; + syncStatus.model.load(); } Connections { target: UserModel function onNewUserSelected() { accountMenu.close(); + syncStatus.model.load(); } } @@ -564,20 +566,28 @@ Window { } } // Rectangle trayWindowHeaderBackground - ActivityList { - anchors.top: trayWindowHeaderBackground.bottom - anchors.left: trayWindowBackground.left - anchors.right: trayWindowBackground.right - anchors.bottom: trayWindowBackground.bottom + SyncStatus { + id: syncStatus + + anchors.top: trayWindowHeaderBackground.bottom + anchors.left: trayWindowBackground.left + anchors.right: trayWindowBackground.right + } + + ActivityList { + anchors.top: syncStatus.bottom + anchors.left: trayWindowBackground.left + anchors.right: trayWindowBackground.right + anchors.bottom: trayWindowBackground.bottom - model: activityModel - onShowFileActivity: { - openFileActivityDialog(displayPath, absolutePath) - } - onActivityItemClicked: { - model.triggerDefaultAction(index) - } - } + model: activityModel + onShowFileActivity: { + openFileActivityDialog(displayPath, absolutePath) + } + onActivityItemClicked: { + model.triggerDefaultAction(index) + } + } Loader { id: fileActivityDialogLoader diff --git a/src/gui/tray/syncstatussummary.cpp b/src/gui/tray/syncstatussummary.cpp new file mode 100644 index 000000000..a66a5d9d9 --- /dev/null +++ b/src/gui/tray/syncstatussummary.cpp @@ -0,0 +1,314 @@ +/* + * Copyright (C) by Felix Weilbach + * + * 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 "syncstatussummary.h" +#include "folderman.h" +#include "navigationpanehelper.h" +#include "networkjobs.h" +#include "syncresult.h" +#include "tray/UserModel.h" + +#include + +namespace { + +OCC::SyncResult::Status determineSyncStatus(const OCC::SyncResult &syncResult) +{ + const auto status = syncResult.status(); + + if (status == OCC::SyncResult::Success || status == OCC::SyncResult::Problem) { + if (syncResult.hasUnresolvedConflicts()) { + return OCC::SyncResult::Problem; + } + return OCC::SyncResult::Success; + } else if (status == OCC::SyncResult::SyncPrepare || status == OCC::SyncResult::Undefined) { + return OCC::SyncResult::SyncRunning; + } + return status; +} +} + +namespace OCC { + +Q_LOGGING_CATEGORY(lcSyncStatusModel, "nextcloud.gui.syncstatusmodel", QtInfoMsg) + +SyncStatusSummary::SyncStatusSummary(QObject *parent) + : QObject(parent) +{ + const auto folderMan = FolderMan::instance(); + connect(folderMan, &FolderMan::folderListChanged, this, &SyncStatusSummary::onFolderListChanged); + connect(folderMan, &FolderMan::folderSyncStateChange, this, &SyncStatusSummary::onFolderSyncStateChanged); +} + +void SyncStatusSummary::load() +{ + auto accountState = UserModel::instance()->currentUser()->accountState(); + + if (_accountState.data() == accountState.data()) { + return; + } + + _accountState = accountState; + clearFolderErrors(); + connectToFoldersProgress(FolderMan::instance()->map()); + auto syncStateFallbackNeeded = true; + for (const auto &folder : FolderMan::instance()->map()) { + if (_accountState.data() != folder->accountState()) { + continue; + } + onFolderSyncStateChanged(folder); + syncStateFallbackNeeded = false; + } + + if (syncStateFallbackNeeded) { + setSyncing(false); + setSyncStatusDetailString(""); + if (_accountState && !_accountState->isConnected()) { + setSyncStatusString(tr("Offline")); + setSyncIcon(Theme::instance()->folderOffline()); + } else { + setSyncStatusString(tr("All synced!")); + setSyncIcon(Theme::instance()->syncStatusOk()); + } + } +} + +double SyncStatusSummary::syncProgress() const +{ + return _progress; +} + +QUrl SyncStatusSummary::syncIcon() const +{ + return _syncIcon; +} + +bool SyncStatusSummary::syncing() const +{ + return _isSyncing; +} + +void SyncStatusSummary::onFolderListChanged(const OCC::Folder::Map &folderMap) +{ + connectToFoldersProgress(folderMap); +} + +void SyncStatusSummary::markFolderAsError(const Folder *folder) +{ + _foldersWithErrors.insert(folder->alias()); +} + +void SyncStatusSummary::markFolderAsSuccess(const Folder *folder) +{ + _foldersWithErrors.erase(folder->alias()); +} + +bool SyncStatusSummary::folderErrors() const +{ + return _foldersWithErrors.size() != 0; +} + +bool SyncStatusSummary::folderError(const Folder *folder) const +{ + return _foldersWithErrors.find(folder->alias()) != _foldersWithErrors.end(); +} + +void SyncStatusSummary::clearFolderErrors() +{ + _foldersWithErrors.clear(); +} + +void SyncStatusSummary::setSyncStateForFolder(const Folder *folder) +{ + if (_accountState && !_accountState->isConnected()) { + setSyncing(false); + setSyncStatusString(tr("Offline")); + setSyncStatusDetailString(""); + setSyncIcon(Theme::instance()->folderOffline()); + return; + } + + const auto state = determineSyncStatus(folder->syncResult()); + + switch (state) { + case SyncResult::Success: + case SyncResult::SyncPrepare: + // Success should only be shown if all folders were fine + if (!folderErrors() || folderError(folder)) { + setSyncing(false); + setSyncStatusString(tr("All synced!")); + setSyncStatusDetailString(""); + setSyncIcon(Theme::instance()->syncStatusOk()); + markFolderAsSuccess(folder); + } + break; + case SyncResult::Error: + case SyncResult::SetupError: + setSyncing(false); + setSyncStatusString(tr("Some files couldn't be synced!")); + setSyncStatusDetailString(tr("See below for errors")); + setSyncIcon(Theme::instance()->syncStatusError()); + markFolderAsError(folder); + break; + case SyncResult::SyncRunning: + case SyncResult::NotYetStarted: + setSyncing(true); + setSyncStatusString(tr("Syncing")); + setSyncStatusDetailString(""); + setSyncIcon(Theme::instance()->syncStatusRunning()); + break; + case SyncResult::Paused: + case SyncResult::SyncAbortRequested: + setSyncing(false); + setSyncStatusString(tr("Sync paused")); + setSyncStatusDetailString(""); + setSyncIcon(Theme::instance()->syncStatusPause()); + break; + case SyncResult::Problem: + case SyncResult::Undefined: + setSyncing(false); + setSyncStatusString(tr("Some files had problems during the sync!")); + setSyncStatusDetailString(tr("See below for warnings")); + setSyncIcon(Theme::instance()->syncStatusWarning()); + markFolderAsError(folder); + break; + } +} + +void SyncStatusSummary::onFolderSyncStateChanged(const Folder *folder) +{ + if (!folder) { + return; + } + + if (!_accountState || folder->accountState() != _accountState.data()) { + return; + } + + setSyncStateForFolder(folder); +} + +constexpr double calculateOverallPercent( + qint64 totalFileCount, qint64 completedFile, qint64 totalSize, qint64 completedSize) +{ + int overallPercent = 0; + if (totalFileCount > 0) { + // Add one 'byte' for each file so the percentage is moving when deleting or renaming files + overallPercent = qRound(double(completedSize + completedFile) / double(totalSize + totalFileCount) * 100.0); + } + overallPercent = qBound(0, overallPercent, 100); + return overallPercent / 100.0; +} + +void SyncStatusSummary::onFolderProgressInfo(const ProgressInfo &progress) +{ + const qint64 completedSize = progress.completedSize(); + const qint64 currentFile = progress.currentFile(); + const qint64 completedFile = progress.completedFiles(); + const qint64 totalSize = qMax(completedSize, progress.totalSize()); + const qint64 totalFileCount = qMax(currentFile, progress.totalFiles()); + + setSyncProgress(calculateOverallPercent(totalFileCount, completedFile, totalSize, completedSize)); + + if (totalSize > 0) { + const auto completedSizeString = Utility::octetsToString(completedSize); + const auto totalSizeString = Utility::octetsToString(totalSize); + + if (progress.trustEta()) { + setSyncStatusDetailString( + tr("%1 of %2 ยท %3 left") + .arg(completedSizeString, totalSizeString) + .arg(Utility::durationToDescriptiveString1(progress.totalProgress().estimatedEta))); + } else { + setSyncStatusDetailString(tr("%1 of %2").arg(completedSizeString, totalSizeString)); + } + } + + if (totalFileCount > 0) { + setSyncStatusString(tr("Syncing file %1 of %2").arg(currentFile).arg(totalFileCount)); + } +} + +void SyncStatusSummary::setSyncing(bool value) +{ + if (value == _isSyncing) { + return; + } + + _isSyncing = value; + emit syncingChanged(); +} + +void SyncStatusSummary::setSyncProgress(double value) +{ + if (_progress == value) { + return; + } + + _progress = value; + emit syncProgressChanged(); +} + +void SyncStatusSummary::setSyncStatusString(const QString &value) +{ + if (_syncStatusString == value) { + return; + } + + _syncStatusString = value; + emit syncStatusStringChanged(); +} + +QString SyncStatusSummary::syncStatusString() const +{ + return _syncStatusString; +} + +QString SyncStatusSummary::syncStatusDetailString() const +{ + return _syncStatusDetailString; +} + +void SyncStatusSummary::setSyncIcon(const QUrl &value) +{ + if (_syncIcon == value) { + return; + } + + _syncIcon = value; + emit syncIconChanged(); +} + +void SyncStatusSummary::setSyncStatusDetailString(const QString &value) +{ + if (_syncStatusDetailString == value) { + return; + } + + _syncStatusDetailString = value; + emit syncStatusDetailStringChanged(); +} + +void SyncStatusSummary::connectToFoldersProgress(const Folder::Map &folderMap) +{ + for (const auto &folder : folderMap) { + if (folder->accountState() == _accountState.data()) { + connect( + folder, &Folder::progressInfo, this, &SyncStatusSummary::onFolderProgressInfo, Qt::UniqueConnection); + } else { + disconnect(folder, &Folder::progressInfo, this, &SyncStatusSummary::onFolderProgressInfo); + } + } +} +} diff --git a/src/gui/tray/syncstatussummary.h b/src/gui/tray/syncstatussummary.h new file mode 100644 index 000000000..f4abec198 --- /dev/null +++ b/src/gui/tray/syncstatussummary.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) by Felix Weilbach + * + * 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 "accountstate.h" +#include "folderman.h" + +#include +#include + +#include + +namespace OCC { + +class SyncStatusSummary : public QObject +{ + Q_OBJECT + + Q_PROPERTY(double syncProgress READ syncProgress NOTIFY syncProgressChanged) + Q_PROPERTY(QUrl syncIcon READ syncIcon NOTIFY syncIconChanged) + Q_PROPERTY(bool syncing READ syncing NOTIFY syncingChanged) + Q_PROPERTY(QString syncStatusString READ syncStatusString NOTIFY syncStatusStringChanged) + Q_PROPERTY(QString syncStatusDetailString READ syncStatusDetailString NOTIFY syncStatusDetailStringChanged) + +public: + explicit SyncStatusSummary(QObject *parent = nullptr); + + double syncProgress() const; + QUrl syncIcon() const; + bool syncing() const; + QString syncStatusString() const; + QString syncStatusDetailString() const; + +signals: + void syncProgressChanged(); + void syncIconChanged(); + void syncingChanged(); + void syncStatusStringChanged(); + void syncStatusDetailStringChanged(); + +public slots: + void load(); + +private: + void connectToFoldersProgress(const Folder::Map &map); + + void onFolderListChanged(const OCC::Folder::Map &folderMap); + void onFolderProgressInfo(const ProgressInfo &progress); + void onFolderSyncStateChanged(const Folder *folder); + + void setSyncStateForFolder(const Folder *folder); + void markFolderAsError(const Folder *folder); + void markFolderAsSuccess(const Folder *folder); + bool folderErrors() const; + bool folderError(const Folder *folder) const; + void clearFolderErrors(); + + void setSyncProgress(double value); + void setSyncing(bool value); + void setSyncStatusString(const QString &value); + void setSyncStatusDetailString(const QString &value); + void setSyncIcon(const QUrl &value); + + AccountStatePtr _accountState; + std::set _foldersWithErrors; + + QUrl _syncIcon = Theme::instance()->syncStatusOk(); + double _progress = 1.0; + bool _isSyncing = false; + QString _syncStatusString = tr("All synced!"); + QString _syncStatusDetailString; +}; +} diff --git a/src/libsync/theme.cpp b/src/libsync/theme.cpp index c4e6e83b7..699951130 100644 --- a/src/libsync/theme.cpp +++ b/src/libsync/theme.cpp @@ -156,7 +156,37 @@ QUrl Theme::statusAwayImageSource() const QUrl Theme::statusInvisibleImageSource() const { - return imagePathToUrl(themeImagePath("user-status-invisible", 16)); + return imagePathToUrl(themeImagePath("user-status-invisible", 64)); +} + +QUrl Theme::syncStatusOk() const +{ + return imagePathToUrl(themeImagePath("state-ok", 16)); +} + +QUrl Theme::syncStatusError() const +{ + return imagePathToUrl(themeImagePath("state-error", 16)); +} + +QUrl Theme::syncStatusRunning() const +{ + return imagePathToUrl(themeImagePath("state-sync", 16)); +} + +QUrl Theme::syncStatusPause() const +{ + return imagePathToUrl(themeImagePath("state-pause", 16)); +} + +QUrl Theme::syncStatusWarning() const +{ + return imagePathToUrl(themeImagePath("state-warning", 16)); +} + +QUrl Theme::folderOffline() const +{ + return imagePathToUrl(themeImagePath("state-offline")); } QString Theme::version() const diff --git a/src/libsync/theme.h b/src/libsync/theme.h index c0e2433e5..37e3c52c1 100644 --- a/src/libsync/theme.h +++ b/src/libsync/theme.h @@ -151,6 +151,18 @@ public: */ QUrl statusInvisibleImageSource() const; + QUrl syncStatusOk() const; + + QUrl syncStatusError() const; + + QUrl syncStatusRunning() const; + + QUrl syncStatusPause() const; + + QUrl syncStatusWarning() const; + + QUrl folderOffline() const; + /** * @brief configFileName * @return the name of the config file.