Show sync progress in main dialog

Fixes #3662

Signed-off-by: Felix Weilbach <felix.weilbach@nextcloud.com>
This commit is contained in:
Felix Weilbach 2021-09-14 13:17:03 +02:00 committed by Matthieu Gallien (Rebase PR Action)
parent 375dc92454
commit 4c11f6763e
12 changed files with 568 additions and 19 deletions

View file

@ -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>

View file

@ -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

View file

@ -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");

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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
model: activityModel
onShowFileActivity: {
openFileActivityDialog(displayPath, absolutePath)
}
onActivityItemClicked: {
model.triggerDefaultAction(index)
}
}
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)
}
}
Loader {
id: fileActivityDialogLoader

View file

@ -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);
}
}
}
}

View file

@ -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;
};
}

View file

@ -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

View file

@ -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.