Przeglądaj źródła

Show sync progress in main dialog

Fixes #3662

Signed-off-by: Felix Weilbach <felix.weilbach@nextcloud.com>
Felix Weilbach 4 lat temu
rodzic
commit
4c11f6763e

+ 1 - 0
resources.qrc

@@ -7,6 +7,7 @@
         <file>src/gui/tray/Window.qml</file>
         <file>src/gui/tray/UserLine.qml</file>
         <file>src/gui/tray/HeaderButton.qml</file>
+        <file>src/gui/tray/SyncStatus.qml</file>
         <file>theme/Style/Style.qml</file>
         <file>theme/Style/qmldir</file>
         <file>src/gui/tray/ActivityActionButton.qml</file>

+ 1 - 0
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

+ 2 - 0
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<SyncStatusSummary>("com.nextcloud.desktopclient", 1, 0, "SyncStatusSummary");
     qmlRegisterType<EmojiModel>("com.nextcloud.desktopclient", 1, 0, "EmojiModel");
     qmlRegisterType<UserStatusSelectorModel>("com.nextcloud.desktopclient", 1, 0, "UserStatusSelectorModel");
     qmlRegisterType<OCC::ActivityListModel>("com.nextcloud.desktopclient", 1, 0, "ActivityListModel");

+ 3 - 5
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

+ 89 - 0
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
+        }
+    }
+}

+ 5 - 0
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;

+ 2 - 0
src/gui/tray/UserModel.h

@@ -9,6 +9,7 @@
 #include <QHash>
 
 #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;

+ 23 - 13
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

+ 314 - 0
src/gui/tray/syncstatussummary.cpp

@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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 <theme.h>
+
+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);
+        }
+    }
+}
+}

+ 85 - 0
src/gui/tray/syncstatussummary.h

@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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 <theme.h>
+#include <folder.h>
+
+#include <QObject>
+
+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<QString> _foldersWithErrors;
+
+    QUrl _syncIcon = Theme::instance()->syncStatusOk();
+    double _progress = 1.0;
+    bool _isSyncing = false;
+    QString _syncStatusString = tr("All synced!");
+    QString _syncStatusDetailString;
+};
+}

+ 31 - 1
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

+ 12 - 0
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.