feat(GODT-2261): sync progress in GUI.

This commit is contained in:
Xavier Michelon
2023-02-21 15:25:50 +01:00
parent 5007d451c2
commit 89112baf96
34 changed files with 3243 additions and 606 deletions

View File

@ -1,225 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge 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.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include "BridgeLib.h"
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/BridgeUtils.h>
using namespace bridgepp;
#ifdef __cplusplus
extern "C" {
#endif
typedef char *(*FuncReturningCString)();
#ifdef __cplusplus
}
#endif
namespace {
FuncReturningCString goosFunc = nullptr; ///< A pointer to the dynamically loaded GoOS function.
FuncReturningCString userCacheDirFunc = nullptr; ///< A pointer to the dynamically loaded UserCache function.
FuncReturningCString userConfigDirFunc = nullptr; ///< A pointer to the dynamically loaded UserConfig function.
FuncReturningCString userDataDirFunc = nullptr; ///< A pointer to the dynamically loaded UserData function.
void (*deleteCStringFunc)(char *) = nullptr; ///< A pointer to the deleteCString function.
#if defined(Q_OS_WINDOWS)
typedef HINSTANCE LibHandle;
#else
typedef void *LibHandle;
#endif
LibHandle loadDynamicLibrary(QString const &path); ///< Load a dynamic library.
void *getFuncPointer(LibHandle lib, QString const &funcName); ///< Retrieve a function pointer from a dynamic library.
//****************************************************************************************************************************************************
/// \return The path to the bridgelib library file.
//****************************************************************************************************************************************************
QString bridgelibPath() {
QString const path = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath("bridgelib.");
switch (os()) {
case OS::Windows:
return path + "dll";
case OS::MacOS:
return path + "dylib";
case OS::Linux:
default:
return path + "so";
}
}
#if defined(Q_OS_WINDOWS)
//****************************************************************************************************************************************************
/// \param[in] path The path of the library file.
/// \return A pointer to the library object
//****************************************************************************************************************************************************
LibHandle loadDynamicLibrary(QString const &path) {
if (!QFileInfo::exists(path)) {
throw Exception(QString("The dynamic library file bridgelib.dylib could not be found at '%1'.").arg(path));
}
LibHandle handle = LoadLibrary(reinterpret_cast<LPCWSTR>(path.toStdWString().c_str()));
if (!handle) {
throw Exception(QString("The bridgelib dynamic library file '%1' could not be opened.").arg(path));
}
return handle;
}
//****************************************************************************************************************************************************
/// \param[in] lib A handle to the library
/// \param[in] funcName The name of the function.
/// \return A pointer to the function
//****************************************************************************************************************************************************
void *getFuncPointer(LibHandle lib, QString const &funcName) {
void *pointer = reinterpret_cast<void*>(GetProcAddress(lib, funcName.toLocal8Bit()));
if (!pointer)
throw Exception(QString("Could not locate function %1 in bridgelib dynamic library").arg(funcName));
return pointer;
}
#else
#include <dlfcn.h>
//****************************************************************************************************************************************************
/// \param[in] path The path of the library file.
/// \return A pointer to the library object
//****************************************************************************************************************************************************
void *loadDynamicLibrary(QString const &path) {
if (!QFileInfo::exists(path)) {
throw Exception(QString("The dynamic library file bridgelib.dylib could not be found at '%1'.").arg(path));
}
void *lib = dlopen(path.toLocal8Bit().data(), RTLD_LAZY);
if (!lib) {
throw Exception(QString("The bridgelib dynamic library file '%1' could not be opened.").arg(path));
}
return lib;
}
//****************************************************************************************************************************************************
/// \param[in] lib A handle to the library
/// \param[in] funcName The name of the function.
/// \return A pointer to the function
//****************************************************************************************************************************************************
void *getFuncPointer(LibHandle lib, QString const &funcName) {
void *pointer = dlsym(lib, funcName.toLocal8Bit());
if (!pointer) {
throw Exception(QString("Could not locate function %1 in bridgelib dynamic library").arg(funcName));
}
return pointer;
}
#endif // defined(Q_OS_WINDOWS)
}
namespace bridgelib {
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void loadLibrary() {
try {
LibHandle lib = loadDynamicLibrary(bridgelibPath());
goosFunc = reinterpret_cast<FuncReturningCString>(getFuncPointer(lib, "GoOS"));
userCacheDirFunc = reinterpret_cast<FuncReturningCString>(getFuncPointer(lib, "UserCacheDir"));
userConfigDirFunc = reinterpret_cast<FuncReturningCString>(getFuncPointer(lib, "UserConfigDir"));
userDataDirFunc = reinterpret_cast<FuncReturningCString>(getFuncPointer(lib, "UserDataDir"));
deleteCStringFunc = reinterpret_cast<void (*)(char*)>(getFuncPointer(lib, "DeleteCString"));
} catch (Exception const &e) {
throw Exception("Error loading the bridgelib dynamic library file.", e.qwhat());
}
}
//****************************************************************************************************************************************************
/// \brief Converts a C-style string returned by a go library function to a QString, and release the memory allocated for the C-style string.
/// \param[in] cString The C-style string, in UTF-8 format.
/// \return A QString.
//****************************************************************************************************************************************************
QString goToQString(char *const cString) {
if (!cString) {
return QString();
}
QString const result = QString::fromUtf8(cString);
deleteCStringFunc(cString);
return result;
}
//****************************************************************************************************************************************************
/// \return The value of the Go runtime.GOOS constant.
//****************************************************************************************************************************************************
QString goos() {
return goToQString(goosFunc());
}
//****************************************************************************************************************************************************
/// \return The path to the user cache folder.
//****************************************************************************************************************************************************
QString userCacheDir() {
return goToQString(userCacheDirFunc());
}
//****************************************************************************************************************************************************
/// \return The path to the user cache folder.
//****************************************************************************************************************************************************
QString userConfigDir() {
return goToQString(userConfigDirFunc());
}
//****************************************************************************************************************************************************
/// \return The path to the user data folder.
//****************************************************************************************************************************************************
QString userDataDir() {
return goToQString(userDataDirFunc());
}
}

View File

@ -1,36 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge 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.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#ifndef BRIDGE_GUI_BRIDGELIB_H
#define BRIDGE_GUI_BRIDGELIB_H
namespace bridgelib {
void loadLibrary();
QString goos();
QString userCacheDir();
QString userConfigDir();
QString userDataDir();
} // namespace bridgelib
#endif //BRIDGE_GUI_BRIDGELIB_H

View File

@ -152,10 +152,9 @@ add_executable(bridge-gui
Resources.qrc
AppController.cpp AppController.h
BridgeApp.cpp BridgeApp.h
BridgeLib.cpp BridgeLib.h
CommandLine.cpp CommandLine.h
EventStreamWorker.cpp EventStreamWorker.h
Log.cpp Log.h
LogUtils.cpp LogUtils.h
main.cpp
Pch.h
QMLBackend.cpp QMLBackend.h

View File

@ -16,9 +16,9 @@
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include "Log.h"
#include "BridgeLib.h"
#include "LogUtils.h"
#include "BuildConfig.h"
#include <bridgepp/BridgeLib.h>
using namespace bridgepp;

View File

@ -16,8 +16,8 @@
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#ifndef BRIDGE_GUI_LOG_H
#define BRIDGE_GUI_LOG_H
#ifndef BRIDGE_GUI_LOG_UTILS_H
#define BRIDGE_GUI_LOG_UTILS_H
#include <bridgepp/Log/Log.h>
@ -26,4 +26,4 @@
bridgepp::Log &initLog(); ///< Initialize the application log.
#endif //BRIDGE_GUI_LOG_H
#endif //BRIDGE_GUI_LOG_UTILS_H

View File

@ -18,8 +18,8 @@
#include "QMLBackend.h"
#include "BuildConfig.h"
#include "BridgeLib.h"
#include "EventStreamWorker.h"
#include <bridgepp/BridgeLib.h>
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/Worker/Overseer.h>

View File

@ -17,12 +17,8 @@
#include "SentryUtils.h"
#include "BuildConfig.h"
#include "BridgeLib.h"
#include <bridgepp/BridgeLib.h>
#include <bridgepp/BridgeUtils.h>
#include <QByteArray>
#include <QCryptographicHash>
#include <QString>
#include <QSysInfo>
using namespace bridgepp;

View File

@ -39,6 +39,9 @@ void UserList::connectGRPCEvents() const {
connect(&client, &GRPCClient::userChanged, this, &UserList::onUserChanged);
connect(&client, &GRPCClient::toggleSplitModeFinished, this, &UserList::onToggleSplitModeFinished);
connect(&client, &GRPCClient::usedBytesChanged, this, &UserList::onUsedBytesChanged);
connect(&client, &GRPCClient::syncStarted, this, &UserList::onSyncStarted);
connect(&client, &GRPCClient::syncFinished, this, &UserList::onSyncFinished);
connect(&client, &GRPCClient::syncProgress, this, &UserList::onSyncProgress);
}
@ -251,3 +254,47 @@ void UserList::onUsedBytesChanged(QString const &userID, qint64 usedBytes) {
}
users_[index]->setUsedBytes(usedBytes);
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
//****************************************************************************************************************************************************
void UserList::onSyncStarted(QString const &userID) {
int const index = this->rowOfUserID(userID);
if (index < 0) {
app().log().error(QString("Received onSyncStarted event for unknown userID %1").arg(userID));
return;
}
users_[index]->setIsSyncing(true);
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
//****************************************************************************************************************************************************
void UserList::onSyncFinished(QString const &userID) {
int const index = this->rowOfUserID(userID);
if (index < 0) {
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
return;
}
users_[index]->setIsSyncing(false);
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
/// \param[in] progress The sync progress ratio.
/// \param[in] elapsedMs The elapsed sync time in milliseconds.
/// \param[in] remainingMs The remaining sync time in milliseconds.
//****************************************************************************************************************************************************
void UserList::onSyncProgress(QString const &userID, double progress, float elapsedMs, float remainingMs) {
Q_UNUSED(elapsedMs)
Q_UNUSED(remainingMs)
int const index = this->rowOfUserID(userID);
if (index < 0) {
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
return;
}
users_[index]->setSyncProgress(progress);
}

View File

@ -61,7 +61,10 @@ public slots: ///< handler for signals coming from the gRPC service
void onUserChanged(QString const &userID);
void onToggleSplitModeFinished(QString const &userID);
void onUsedBytesChanged(QString const &userID, qint64 usedBytes); ///< Slot for usedBytesChanged events.
void onSyncStarted(QString const &userID); ///< Slot for syncStarted events.
void onSyncFinished(QString const &userID); ///< Slot for syncFinished events.
void onSyncProgress(QString const &userID, double progress, float elapsedMs, float remainingMs); ///< Slot for syncFinished events.
private: // data members
QList<bridgepp::SPUser> users_; ///< The user list.
};

View File

@ -18,12 +18,12 @@
#include "Pch.h"
#include "BridgeApp.h"
#include "BridgeLib.h"
#include "CommandLine.h"
#include "Log.h"
#include "LogUtils.h"
#include "QMLBackend.h"
#include "SentryUtils.h"
#include "BuildConfig.h"
#include <bridgepp/BridgeLib.h>
#include <bridgepp/BridgeUtils.h>
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/FocusGRPC/FocusGRPCClient.h>

View File

@ -29,14 +29,19 @@ Item {
property var _spacing: 12 * ProtonStyle.px
property color usedSpaceColor : {
property color progressColor : {
if (!root.enabled) return root.colorScheme.text_weak
if (root.type == AccountDelegate.SmallView) return root.colorScheme.text_weak
if (root.usedFraction < .50) return root.colorScheme.signal_success
if (root.usedFraction < .75) return root.colorScheme.signal_warning
if (root.user && root.user.isSyncing) return root.colorScheme.text_weak
if (root.progressRatio < .50) return root.colorScheme.signal_success
if (root.progressRatio < .75) return root.colorScheme.signal_warning
return root.colorScheme.signal_danger
}
property real usedFraction: root.user ? reasonableFraction(root.user.usedBytes, root.user.totalBytes) : 0
property real progressRatio: {
if (!root.user)
return 0
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes)
}
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
@ -171,18 +176,21 @@ Item {
case EUserState.Locked:
return qsTr("Connecting") + dotsTimer.dots
case EUserState.Connected:
return root.usedSpace
if (root.user.isSyncing)
return qsTr("Synchronizing (%1%)").arg(Math.floor(root.user.syncProgress * 100)) + dotsTimer.dots
else
return root.usedSpace
}
}
Timer { // dots animation while connecting. 1 sec cycle, roughly similar to the webmail loading page.
Timer { // dots animation while connecting & syncing.
id:dotsTimer
property string dots: ""
interval: 250;
interval: 500;
repeat: true;
running: (root.user != null) && (root.user.state === EUserState.Locked)
running: (root.user != null) && ((root.user.state === EUserState.Locked) || (root.user.isSyncing))
onTriggered: {
dots = dots + "."
dots += "."
if (dots.length > 3)
dots = ""
}
@ -191,7 +199,7 @@ Item {
}
}
color: root.usedSpaceColor
color: root.progressColor
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Caption
@ -202,7 +210,7 @@ Item {
Label {
colorScheme: root.colorScheme
text: root.user && root.user.state == EUserState.Connected ? " / " + root.totalSpace : ""
text: root.user && root.user.state == EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
color: root.colorScheme.text_weak
type: {
switch (root.type) {
@ -213,26 +221,27 @@ Item {
}
}
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0 }
Rectangle {
id: storage_bar
id: progress_bar
visible: root.user ? root.type == AccountDelegate.LargeView : false
width: 140 * ProtonStyle.px
height: 4 * ProtonStyle.px
radius: ProtonStyle.storage_bar_radius
radius: ProtonStyle.progress_bar_radius
color: root.colorScheme.border_weak
Rectangle {
id: storage_bar_filled
radius: ProtonStyle.storage_bar_radius
color: root.usedSpaceColor
visible: root.user ? parent.visible && (root.user.state == EUserState.Connected) : false
id: progress_bar_filled
radius: ProtonStyle.progress_bar_radius
color: root.progressColor
visible: root.user ? parent.visible && (root.user.state == EUserState.Connected): false
anchors {
top : parent.top
bottom : parent.bottom
left : parent.left
}
width: Math.min(1,Math.max(0.02,root.usedFraction)) * parent.width
width: Math.min(1,Math.max(0.02,root.progressRatio)) * parent.width
}
}
}

View File

@ -362,7 +362,7 @@ QtObject {
property real banner_radius : 12 * root.px // px
property real dialog_radius : 12 * root.px // px
property real card_radius : 12 * root.px // px
property real storage_bar_radius : 3 * root.px // px
property real progress_bar_radius : 3 * root.px // px
property real tooltip_radius : 8 * root.px // px
property int heading_font_size: 28