mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
feat(GODT-2517): replace status window with native tray icon context menu.
feat(GODT-2517): show main windows on left click (Linux & Windows). feat(GODT-2517): removed old QML status window. feat(GODT-2517): polishing. feat(GODT-2517): renaming + removal of dead code (v2 tests).
This commit is contained in:
@ -114,6 +114,7 @@ add_executable(bridge-gui
|
||||
EventStreamWorker.cpp EventStreamWorker.h
|
||||
LogUtils.cpp LogUtils.h
|
||||
main.cpp
|
||||
TrayIcon.cpp TrayIcon.h
|
||||
Pch.h
|
||||
QMLBackend.cpp QMLBackend.h
|
||||
UserList.cpp UserList.h
|
||||
|
||||
@ -49,6 +49,9 @@ QMLBackend::QMLBackend()
|
||||
/// \param[in] serviceConfig
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::init(GRPCConfig const &serviceConfig) {
|
||||
trayIcon_.reset(new TrayIcon());
|
||||
this->setNormalTrayIcon();
|
||||
|
||||
connect(this, &QMLBackend::fatalError, &app(), &AppController::onFatalError);
|
||||
|
||||
users_ = new UserList(this);
|
||||
@ -99,6 +102,14 @@ bool QMLBackend::waitForEventStreamReaderToFinish(qint32 timeoutMs) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The list of users
|
||||
//****************************************************************************************************************************************************
|
||||
UserList const &QMLBackend::users() const {
|
||||
return *users_;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The build year as a string (e.g. 2023)
|
||||
//****************************************************************************************************************************************************
|
||||
@ -591,7 +602,6 @@ void QMLBackend::toggleIsTelemetryDisabled(bool isDisabled) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] scheme the scheme name
|
||||
//****************************************************************************************************************************************************
|
||||
@ -861,6 +871,49 @@ void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::setNormalTrayIcon() {
|
||||
if (trayIcon_) {
|
||||
trayIcon_->setState(TrayIcon::State::Normal, tr("Connected"), ":/qml/icons/ic-connected.svg");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] stateString A string describing the state.
|
||||
/// \param[in] statusIcon The path of the status icon.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::setErrorTrayIcon(QString const &stateString, QString const &statusIcon) {
|
||||
if (trayIcon_) {
|
||||
trayIcon_->setState(TrayIcon::State::Error, stateString, statusIcon);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] stateString A string describing the state.
|
||||
/// \param[in] statusIcon The path of the status icon.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::setWarnTrayIcon(QString const &stateString, QString const &statusIcon) {
|
||||
if (trayIcon_) {
|
||||
trayIcon_->setState(TrayIcon::State::Warn, stateString, statusIcon);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] stateString A string describing the state.
|
||||
/// \param[in] statusIcon The path of the status icon.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::setUpdateTrayIcon(QString const &stateString, QString const &statusIcon) {
|
||||
if (trayIcon_) {
|
||||
trayIcon_->setState(TrayIcon::State::Update, stateString, statusIcon);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] imapPort The IMAP port.
|
||||
/// \param[in] smtpPort The SMTP port.
|
||||
@ -915,7 +968,7 @@ void QMLBackend::onLoginAlreadyLoggedIn(QString const &userID) {
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::onUserBadEvent(QString const &userID, QString const& ) {
|
||||
void QMLBackend::onUserBadEvent(QString const &userID, QString const &) {
|
||||
HANDLE_EXCEPTION(
|
||||
if (badEventDisplayQueue_.contains(userID)) {
|
||||
app().log().error("Received 'bad event' for a user that is already in the queue.");
|
||||
@ -944,8 +997,9 @@ void QMLBackend::onIMAPLoginFailed(QString const &username) {
|
||||
if ((!user) || (user->state() != UserState::SignedOut)) { // We want to pop-up only if a signed-out user has been detected
|
||||
return;
|
||||
}
|
||||
if (user->isInIMAPLoginFailureCooldown())
|
||||
if (user->isInIMAPLoginFailureCooldown()) {
|
||||
return;
|
||||
}
|
||||
user->startImapLoginFailureCooldown(60 * 60 * 1000); // 1 hour cooldown during which we will not display this notification to this user again.
|
||||
emit selectUser(user->id());
|
||||
emit imapLoginWhileSignedOut(username);
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
|
||||
#include "MacOS/DockIcon.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "TrayIcon.h"
|
||||
#include "UserList.h"
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/GRPC/GRPCUtils.h>
|
||||
@ -43,6 +44,7 @@ public: // member functions.
|
||||
QMLBackend &operator=(QMLBackend &&) = delete; ///< Disabled move assignment operator.
|
||||
void init(GRPCConfig const &serviceConfig); ///< Initialize the backend.
|
||||
bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish.
|
||||
UserList const& users() const; ///< Return the list of users
|
||||
|
||||
// invokable methods can be called from QML. They generally return a value, which slots cannot do.
|
||||
Q_INVOKABLE static QString buildYear(); ///< Return the application build year.
|
||||
@ -178,6 +180,10 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
||||
void onVersionChanged(); ///< Slot for the version change signal.
|
||||
void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC
|
||||
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event.
|
||||
void setNormalTrayIcon(); ///< Set the tray icon to normal.
|
||||
void setErrorTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'error' state.
|
||||
void setWarnTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'warn' state.
|
||||
void setUpdateTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'update' state.
|
||||
|
||||
public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding
|
||||
void onMailServerSettingsChanged(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Slot for the ConnectionModeChanged gRPC event.
|
||||
@ -237,8 +243,10 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
||||
void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event.
|
||||
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
|
||||
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
|
||||
void showHelp(); ///< Signal for the 'showHelp' event (from the context menu).
|
||||
void showSettings(); ///< Signal for the 'showHelp' event (from the context menu).
|
||||
void selectUser(QString const& userID); ///< Signal emitted in order to selected a user with a given ID in the list.
|
||||
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
|
||||
void selectUser(QString const); ///< Signal that request the given user account to be displayed.
|
||||
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
|
||||
|
||||
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
|
||||
@ -261,7 +269,7 @@ private: // data members
|
||||
bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP.
|
||||
bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP.
|
||||
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
|
||||
|
||||
std::unique_ptr<TrayIcon> trayIcon_;
|
||||
friend class AppController;
|
||||
};
|
||||
|
||||
|
||||
@ -5,11 +5,7 @@
|
||||
<file>qml/AccountView.qml</file>
|
||||
<file>qml/Banner.qml</file>
|
||||
<file>qml/Bridge.qml</file>
|
||||
<file>qml/Bridge_test.qml</file>
|
||||
<file>qml/bridgeqml.qmlproject</file>
|
||||
<file>qml/BridgeTest/UserControl.qml</file>
|
||||
<file>qml/BridgeTest/UserList.qml</file>
|
||||
<file>qml/BridgeTest/UserModel.qml</file>
|
||||
<file>qml/BugReportView.qml</file>
|
||||
<file>qml/Configuration.qml</file>
|
||||
<file>qml/ConfigurationItem.qml</file>
|
||||
@ -28,6 +24,7 @@
|
||||
<file>qml/icons/ic-connected.svg</file>
|
||||
<file>qml/icons/ic-copy.svg</file>
|
||||
<file>qml/icons/ic-cross-close.svg</file>
|
||||
<file>qml/icons/ic-dot.svg</file>
|
||||
<file>qml/icons/ic-drive.svg</file>
|
||||
<file>qml/icons/ic-exclamation-circle-filled.svg</file>
|
||||
<file>qml/icons/ic-external-link.svg</file>
|
||||
@ -104,17 +101,6 @@
|
||||
<file>qml/ConnectionModeSettings.qml</file>
|
||||
<file>qml/SplashScreen.qml</file>
|
||||
<file>qml/Status.qml</file>
|
||||
<file>qml/StatusWindow.qml</file>
|
||||
<file>qml/tests/Buttons.qml</file>
|
||||
<file>qml/tests/ButtonsColumn.qml</file>
|
||||
<file>qml/tests/CheckBoxes.qml</file>
|
||||
<file>qml/tests/ComboBoxes.qml</file>
|
||||
<file>qml/tests/RadioButtons.qml</file>
|
||||
<file>qml/tests/Switches.qml</file>
|
||||
<file>qml/tests/Test.qml</file>
|
||||
<file>qml/tests/TestComponents.qml</file>
|
||||
<file>qml/tests/TextAreas.qml</file>
|
||||
<file>qml/tests/TextFields.qml</file>
|
||||
<file>qml/WelcomeGuide.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
250
internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp
Normal file
250
internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp
Normal file
@ -0,0 +1,250 @@
|
||||
// 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 "TrayIcon.h"
|
||||
#include "QMLBackend.h"
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
QColor const normalColor(30, 168, 133); /// The normal state color.
|
||||
QColor const errorColor(220, 50, 81); ///< The error state color.
|
||||
QColor const warnColor(255, 153, 0); ///< The warn state color.
|
||||
QColor const updateColor(35, 158, 206); ///< The warn state color.
|
||||
QColor const greyColor(112, 109, 107); ///< The grey color.
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Create a single resolution icon from an image. Throw an exception in case of failure.
|
||||
//****************************************************************************************************************************************************
|
||||
QIcon loadIconFromImage(QString const &path) {
|
||||
QPixmap const pixmap(path);
|
||||
if (pixmap.isNull()) {
|
||||
throw Exception(QString("Could create icon from image '%1'.").arg(path));
|
||||
}
|
||||
return QIcon(pixmap);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Retrieve the color associated with a tray icon state.
|
||||
///
|
||||
/// \param[in] state The state.
|
||||
/// \return The color associated with a given tray icon state.
|
||||
//****************************************************************************************************************************************************
|
||||
QColor stateColor(TrayIcon::State state) {
|
||||
switch (state) {
|
||||
case TrayIcon::State::Normal:
|
||||
return normalColor;
|
||||
case TrayIcon::State::Error:
|
||||
return errorColor;
|
||||
case TrayIcon::State::Warn:
|
||||
return warnColor;
|
||||
case TrayIcon::State::Update:
|
||||
return updateColor;
|
||||
default:
|
||||
app().log().error(QString("Unknown tray icon state %1.").arg(static_cast<qint32>(state)));
|
||||
return normalColor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Return the text identifying a state in resource file names.
|
||||
///
|
||||
/// \param[in] state The state.
|
||||
/// \param[in] The text identifying the state in resource file names.
|
||||
//****************************************************************************************************************************************************
|
||||
QString stateText(TrayIcon::State state) {
|
||||
switch (state) {
|
||||
case TrayIcon::State::Normal:
|
||||
return "norm";
|
||||
case TrayIcon::State::Error:
|
||||
return "error";
|
||||
case TrayIcon::State::Warn:
|
||||
return "warn";
|
||||
case TrayIcon::State::Update:
|
||||
return "update";
|
||||
default:
|
||||
app().log().error(QString("Unknown tray icon state %1.").arg(static_cast<qint32>(state)));
|
||||
return "norm";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
TrayIcon::TrayIcon()
|
||||
: QSystemTrayIcon()
|
||||
, menu_(new QMenu) {
|
||||
|
||||
this->generateDotIcons();
|
||||
this->setContextMenu(menu_.get());
|
||||
|
||||
connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow);
|
||||
connect(this, &TrayIcon::selectUser, &app().backend(), &QMLBackend::selectUser);
|
||||
connect(this, &TrayIcon::activated, this, &TrayIcon::onActivated);
|
||||
|
||||
this->show();
|
||||
this->setState(State::Normal, QString(), QString());
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::onMenuAboutToShow() {
|
||||
this->refreshContextMenu();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::onUserClicked() {
|
||||
try {
|
||||
auto action = dynamic_cast<QAction *>(this->sender());
|
||||
if (!action) {
|
||||
throw Exception("Could not retrieve context menu action.");
|
||||
}
|
||||
|
||||
QString const &userID = action->data().toString();
|
||||
if (userID.isNull()) {
|
||||
throw Exception("Could not retrieve context menu's selected user.");
|
||||
}
|
||||
|
||||
emit selectUser(userID);
|
||||
} catch (Exception const &e) {
|
||||
app().log().error(e.qwhat());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] reason The icon activation reason.
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::onActivated(QSystemTrayIcon::ActivationReason reason) {
|
||||
if ((QSystemTrayIcon::Trigger == reason) && !onMacOS()) {
|
||||
app().backend().showMainWindow();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::generateDotIcons() {
|
||||
QPixmap dotSVG(":/qml/icons/ic-dot.svg");
|
||||
struct IconColor {
|
||||
QIcon &icon;
|
||||
QColor color;
|
||||
};
|
||||
for (auto pair: QList<IconColor> {{ greenDot_, normalColor }, { greyDot_, greyColor }, { orangeDot_, warnColor }}) {
|
||||
QPixmap p = dotSVG;
|
||||
QPainter painter(&p);
|
||||
painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
painter.fillRect(p.rect(), pair.color);
|
||||
painter.end();
|
||||
pair.icon = QIcon(p);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] state The state.
|
||||
/// \param[in] stateString A string describing the state.
|
||||
/// \param[in] statusIconPath The status icon path.
|
||||
/// \param[in] statusIconColor The color for the status icon in hex.
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::setState(TrayIcon::State state, QString const &stateString, QString const &statusIconPath) {
|
||||
stateString_ = stateString;
|
||||
state_ = state;
|
||||
QString const style = onMacOS() ? "mono" : "color";
|
||||
QString const text = stateText(state);
|
||||
|
||||
|
||||
QIcon icon = loadIconFromImage(QString(":/qml/icons/systray-%1-%2.png").arg(style, text));
|
||||
icon.setIsMask(true);
|
||||
this->setIcon(icon);
|
||||
|
||||
this->generateStatusIcon(statusIconPath, stateColor(state));
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] svgPath The path of the SVG file for the icon.
|
||||
/// \param[in] color The color to apply to the icon.
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::generateStatusIcon(QString const &svgPath, QColor const &color) {
|
||||
// We use the SVG path as pixmap mask and fill it with the appropriate color
|
||||
QString resourcePath = svgPath;
|
||||
resourcePath.replace(QRegularExpression(R"(^\.\/)"), ":/qml/"); // QML resource path are a bit different from the Qt resources path.
|
||||
QPixmap pixmap(resourcePath);
|
||||
QPainter painter(&pixmap);
|
||||
painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
painter.fillRect(pixmap.rect(), color);
|
||||
painter.end();
|
||||
statusIcon_ = QIcon(pixmap);
|
||||
}
|
||||
|
||||
|
||||
//**********************************************************************************************************************
|
||||
//
|
||||
//**********************************************************************************************************************
|
||||
void TrayIcon::refreshContextMenu() {
|
||||
if (!menu_) {
|
||||
app().log().error("Native tray icon context menu is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
menu_->clear();
|
||||
menu_->addAction(statusIcon_, stateString_, &app().backend(), &QMLBackend::showMainWindow);
|
||||
menu_->addSeparator();
|
||||
UserList const &users = app().backend().users();
|
||||
qint32 const userCount = users.count();
|
||||
for (qint32 i = 0; i < userCount; i++) {
|
||||
User const &user = *users.get(i);
|
||||
UserState const state = user.state();
|
||||
auto action = new QAction(user.primaryEmailOrUsername());
|
||||
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
|
||||
action->setData(user.id());
|
||||
connect(action, &QAction::triggered, this, &TrayIcon::onUserClicked);
|
||||
if (i < 10) {
|
||||
action->setShortcut(QKeySequence(QString("Ctrl+%1").arg((i + 1) % 10)));
|
||||
}
|
||||
menu_->addAction(action);
|
||||
}
|
||||
if (userCount) {
|
||||
menu_->addSeparator();
|
||||
}
|
||||
menu_->addAction(tr("&Open Bridge"), QKeySequence("Ctrl+O"), &app().backend(), &QMLBackend::showMainWindow);
|
||||
menu_->addAction(tr("&Help"), QKeySequence("Ctrl+F1"), &app().backend(), &QMLBackend::showHelp);
|
||||
menu_->addAction(tr("&Settings"), QKeySequence("Ctrl+,"), &app().backend(), &QMLBackend::showSettings);
|
||||
menu_->addSeparator();
|
||||
menu_->addAction(tr("&Quit Bridge"), QKeySequence("Ctrl+Q"), &app().backend(), &QMLBackend::quit);
|
||||
}
|
||||
70
internal/frontend/bridge-gui/bridge-gui/TrayIcon.h
Normal file
70
internal/frontend/bridge-gui/bridge-gui/TrayIcon.h
Normal file
@ -0,0 +1,70 @@
|
||||
// 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_NATIVE_TRAY_ICON_H
|
||||
#define BRIDGE_GUI_NATIVE_TRAY_ICON_H
|
||||
|
||||
|
||||
//**********************************************************************************************************************
|
||||
/// \brief A native tray icon.
|
||||
//**********************************************************************************************************************
|
||||
class TrayIcon: public QSystemTrayIcon {
|
||||
Q_OBJECT
|
||||
public: // typedef enum
|
||||
enum class State {
|
||||
Normal,
|
||||
Error,
|
||||
Warn,
|
||||
Update,
|
||||
}; ///< Enumeration for the state.
|
||||
|
||||
public: // data members
|
||||
TrayIcon(); ///< Default constructor.
|
||||
~TrayIcon() override = default; ///< Destructor.
|
||||
TrayIcon(TrayIcon const&) = delete; ///< Disabled copy-constructor.
|
||||
TrayIcon(TrayIcon&&) = delete; ///< Disabled assignment copy-constructor.
|
||||
TrayIcon& operator=(TrayIcon const&) = delete; ///< Disabled assignment operator.
|
||||
TrayIcon& operator=(TrayIcon&&) = delete; ///< Disabled move assignment operator.
|
||||
void setState(State state, QString const& stateString, QString const &statusIconPath); ///< Set the state of the icon
|
||||
|
||||
signals:
|
||||
void selectUser(QString const& userID); ///< Signal for selecting a user with a given userID
|
||||
|
||||
private slots:
|
||||
void onMenuAboutToShow(); ///< Slot called before the context menu is shown.
|
||||
void onUserClicked(); ///< Slot triggered when clicking on a user in the context menu.
|
||||
static void onActivated(QSystemTrayIcon::ActivationReason reason); ///< Slot for the activation of the system tray icon.
|
||||
|
||||
private: // member functions.
|
||||
void generateDotIcons(); ///< generate the colored dot icons used for user status.
|
||||
void generateStatusIcon(QString const &svgPath, QColor const& color); ///< Generate the status icon.
|
||||
void refreshContextMenu(); ///< Refresh the context menu.
|
||||
|
||||
private: // data members
|
||||
State state_ { State::Normal }; ///< The state of the tray icon.
|
||||
QString stateString_; ///< The current state string.
|
||||
std::unique_ptr<QMenu> menu_; ///< The context menu for the tray icon. Not owned by the tray icon.
|
||||
QIcon statusIcon_; ///< The path of the status icon displayed in the context menu.
|
||||
QIcon greenDot_; ///< The green dot icon.
|
||||
QIcon greyDot_; ///< The grey dot icon.
|
||||
QIcon orangeDot_; ///< The orange dot icon.
|
||||
};
|
||||
|
||||
|
||||
|
||||
#endif //BRIDGE_GUI_NATIVE_TRAY_ICON_H
|
||||
@ -26,11 +26,8 @@ import Notifications
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
function isInInterval(num, lower_limit, upper_limit) {
|
||||
return lower_limit <= num && num <= upper_limit
|
||||
}
|
||||
function bound(num, lower_limit, upper_limit) {
|
||||
return Math.max(lower_limit, Math.min(upper_limit, num))
|
||||
function bound(num, lowerLimit, upperLimit) {
|
||||
return Math.max(lowerLimit, Math.min(upperLimit, num))
|
||||
}
|
||||
|
||||
property var title: Backend.appname
|
||||
@ -38,10 +35,30 @@ QtObject {
|
||||
property Notifications _notifications: Notifications {
|
||||
id: notifications
|
||||
frontendMain: mainWindow
|
||||
frontendStatus: statusWindow
|
||||
frontendTray: trayIcon
|
||||
}
|
||||
|
||||
property NotificationFilter _trayNotificationFilter: NotificationFilter {
|
||||
id: trayNotificationFilter
|
||||
source: root._notifications ? root._notifications.all : undefined
|
||||
onTopmostChanged: {
|
||||
if (topmost) {
|
||||
switch (topmost.type) {
|
||||
case Notification.NotificationType.Danger:
|
||||
Backend.setErrorTrayIcon(topmost.brief, topmost.icon)
|
||||
return
|
||||
case Notification.NotificationType.Warning:
|
||||
Backend.setWarnTrayIcon(topmost.brief, topmost.icon)
|
||||
return
|
||||
case Notification.NotificationType.Info:
|
||||
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon)
|
||||
return
|
||||
}
|
||||
}
|
||||
Backend.setNormalTrayIcon()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
property MainWindow _mainWindow: MainWindow {
|
||||
id: mainWindow
|
||||
visible: false
|
||||
@ -66,190 +83,6 @@ QtObject {
|
||||
}
|
||||
}
|
||||
|
||||
property StatusWindow _statusWindow: StatusWindow {
|
||||
id: statusWindow
|
||||
visible: false
|
||||
|
||||
title: root.title
|
||||
notifications: root._notifications
|
||||
|
||||
onShowMainWindow: {
|
||||
mainWindow.showAndRise()
|
||||
}
|
||||
|
||||
onShowHelp: {
|
||||
mainWindow.showHelp()
|
||||
mainWindow.showAndRise()
|
||||
}
|
||||
|
||||
onShowSettings: {
|
||||
mainWindow.showSettings()
|
||||
mainWindow.showAndRise()
|
||||
}
|
||||
|
||||
onSelectUser: function(userID) {
|
||||
mainWindow.selectUser(userID)
|
||||
mainWindow.showAndRise()
|
||||
}
|
||||
|
||||
onQuit: {
|
||||
mainWindow.hide()
|
||||
trayIcon.visible = false
|
||||
Backend.quit()
|
||||
}
|
||||
|
||||
property rect screenRect
|
||||
property rect iconRect
|
||||
|
||||
// use binding from function with width and height as arguments so it will be recalculated every time width and height are changed
|
||||
property point position: getPosition(width, height)
|
||||
x: position.x
|
||||
y: position.y
|
||||
|
||||
function getPosition(_width, _height) {
|
||||
if (screenRect.width === 0 || screenRect.height === 0) {
|
||||
return Qt.point(0, 0)
|
||||
}
|
||||
|
||||
var _x = 0
|
||||
var _y = 0
|
||||
|
||||
// fit above
|
||||
_y = iconRect.top - height
|
||||
if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) {
|
||||
// position preferably in the horizontal center but bound to the screen rect
|
||||
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
|
||||
return Qt.point(_x, _y)
|
||||
}
|
||||
|
||||
// fit below
|
||||
_y = iconRect.bottom
|
||||
if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) {
|
||||
// position preferably in the horizontal center but bound to the screen rect
|
||||
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
|
||||
return Qt.point(_x, _y)
|
||||
}
|
||||
|
||||
// fit to the left
|
||||
_x = iconRect.left - width
|
||||
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
|
||||
// position preferably in the vertical center but bound to the screen rect
|
||||
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
|
||||
return Qt.point(_x, _y)
|
||||
}
|
||||
|
||||
// fit to the right
|
||||
_x = iconRect.right
|
||||
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
|
||||
// position preferably in the vertical center but bound to the screen rect
|
||||
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
|
||||
return Qt.point(_x, _y)
|
||||
}
|
||||
|
||||
// Fallback: position status window right above icon and let window manager decide.
|
||||
console.warn("Can't position status window: screenRect =", screenRect, "iconRect =", iconRect)
|
||||
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
|
||||
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
|
||||
return Qt.point(_x, _y)
|
||||
}
|
||||
}
|
||||
|
||||
property SystemTrayIcon _trayIcon: SystemTrayIcon {
|
||||
id: trayIcon
|
||||
visible: true
|
||||
icon.source: getTrayIconPath()
|
||||
icon.mask: true // make sure that systems like macOS will use proper color
|
||||
tooltip: `${root.title} v${Backend.version}`
|
||||
onActivated: function(reason) {
|
||||
function calcStatusWindowPosition() {
|
||||
// On some platforms (X11 / Plasma) Qt does not provide icon position and geometry info.
|
||||
// In this case we rely on cursor position
|
||||
var iconRect = Qt.rect(geometry.x, geometry.y, geometry.width, geometry.height)
|
||||
if (geometry.width == 0 && geometry.height == 0) {
|
||||
var mousePos = Backend.getCursorPos()
|
||||
iconRect.x = mousePos.x
|
||||
iconRect.y = mousePos.y
|
||||
iconRect.width = 0
|
||||
iconRect.height = 0
|
||||
}
|
||||
|
||||
// Find screen
|
||||
var screen
|
||||
for (var i in Qt.application.screens) {
|
||||
var _screen = Qt.application.screens[i]
|
||||
if (
|
||||
isInInterval(iconRect.x, _screen.virtualX, _screen.virtualX + _screen.width) &&
|
||||
isInInterval(iconRect.y, _screen.virtualY, _screen.virtualY + _screen.height)
|
||||
) {
|
||||
screen = _screen
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!screen) {
|
||||
// Fallback to primary screen
|
||||
screen = Qt.application.screens[0]
|
||||
}
|
||||
|
||||
// In case we used mouse to detect icon position - we want to make a fake icon rectangle from a point
|
||||
if (iconRect.width == 0 && iconRect.height == 0) {
|
||||
iconRect.x = bound(iconRect.x - 16, screen.virtualX, screen.virtualX + screen.width - 32)
|
||||
iconRect.y = bound(iconRect.y - 16, screen.virtualY, screen.virtualY + screen.height - 32)
|
||||
iconRect.width = 32
|
||||
iconRect.height = 32
|
||||
}
|
||||
|
||||
statusWindow.screenRect = Qt.rect(screen.virtualX, screen.virtualY, screen.width, screen.height)
|
||||
statusWindow.iconRect = iconRect
|
||||
}
|
||||
|
||||
function toggleWindow(win) {
|
||||
if (win.visible) {
|
||||
win.close()
|
||||
} else {
|
||||
win.showAndRise()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
switch (reason) {
|
||||
case SystemTrayIcon.Unknown:
|
||||
break;
|
||||
case SystemTrayIcon.Context:
|
||||
case SystemTrayIcon.Trigger:
|
||||
case SystemTrayIcon.DoubleClick:
|
||||
case SystemTrayIcon.MiddleClick:
|
||||
calcStatusWindowPosition()
|
||||
toggleWindow(statusWindow)
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
property NotificationFilter _systrayfilter: NotificationFilter {
|
||||
source: root._notifications ? root._notifications.all : undefined
|
||||
}
|
||||
|
||||
function getTrayIconPath() {
|
||||
var color = Backend.goos == "darwin" ? "mono" : "color"
|
||||
|
||||
var level = "norm"
|
||||
if (_systrayfilter.topmost) {
|
||||
switch (_systrayfilter.topmost.type) {
|
||||
case Notification.NotificationType.Danger:
|
||||
level = "error"
|
||||
break;
|
||||
case Notification.NotificationType.Warning:
|
||||
level = "warn"
|
||||
break;
|
||||
case Notification.NotificationType.Info:
|
||||
level = "update"
|
||||
break;
|
||||
}
|
||||
}
|
||||
return `qrc:/qml/icons/systray-${color}-${level}.png`
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!Backend) {
|
||||
@ -266,7 +99,7 @@ QtObject {
|
||||
var c = Backend.users.count
|
||||
var u = Backend.users.get(0)
|
||||
// DEBUG
|
||||
if (c != 0) {
|
||||
if (c !== 0) {
|
||||
console.log("users non zero", c)
|
||||
console.log("first user", u )
|
||||
}
|
||||
@ -290,7 +123,7 @@ QtObject {
|
||||
}
|
||||
|
||||
function setColorScheme() {
|
||||
if (Backend.colorSchemeName == "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
|
||||
if (Backend.colorSchemeName == "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle
|
||||
if (Backend.colorSchemeName === "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
|
||||
if (Backend.colorSchemeName === "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,315 +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/>.
|
||||
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var user
|
||||
property var userIndex
|
||||
|
||||
spacing : 5
|
||||
|
||||
Layout.fillHeight: true
|
||||
//Layout.fillWidth: true
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
TextField {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: user !== undefined ? user.username : ""
|
||||
|
||||
onEditingFinished: {
|
||||
user.username = text
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Switch {
|
||||
id: userLoginSwitch
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
text: "LoggedIn"
|
||||
enabled: user !== undefined && user.username.length > 0
|
||||
|
||||
checked: user ? root.user.state == EUserState.Connected : false
|
||||
|
||||
onCheckedChanged: {
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
if (user === Backend.loginUser) {
|
||||
var newUserObject = Backend.userComponent.createObject(Backend, {username: user.username, loggedIn: true, setupGuideSeen: user.setupGuideSeen})
|
||||
Backend.users.append( { object: newUserObject } )
|
||||
|
||||
user.username = ""
|
||||
user.resetLoginRequests()
|
||||
return
|
||||
}
|
||||
|
||||
user.state = EUserState.Connected
|
||||
user.resetLoginRequests()
|
||||
return
|
||||
} else {
|
||||
user.state = EUserState.SignedOut
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
text: "Setup guide seen"
|
||||
enabled: user !== undefined && user.username.length > 0
|
||||
|
||||
checked: user ? user.setupGuideSeen : false
|
||||
|
||||
onCheckedChanged: {
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
|
||||
user.setupGuideSeen = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: loginLabel
|
||||
text: "Login:"
|
||||
|
||||
Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "name/pass error"
|
||||
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordProvided
|
||||
|
||||
onClicked: {
|
||||
Backend.loginUsernamePasswordError("")
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "free user error"
|
||||
enabled: user !== undefined //&& user.isLoginRequested
|
||||
onClicked: {
|
||||
Backend.loginFreeUserError()
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "connection error"
|
||||
enabled: user !== undefined //&& user.isLoginRequested
|
||||
onClicked: {
|
||||
Backend.loginConnectionError("")
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: faLabel
|
||||
text: "2FA:"
|
||||
|
||||
Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "request"
|
||||
|
||||
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordRequested
|
||||
onClicked: {
|
||||
Backend.login2FARequested(user.username)
|
||||
user.isLogin2FARequested = true
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "error"
|
||||
|
||||
enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
|
||||
onClicked: {
|
||||
Backend.login2FAError("")
|
||||
user.isLogin2FAProvided = false
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Abort"
|
||||
|
||||
enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
|
||||
onClicked: {
|
||||
Backend.login2FAErrorAbort("")
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: passLabel
|
||||
text: "2 Password:"
|
||||
|
||||
Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "request"
|
||||
|
||||
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2PasswordRequested && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
|
||||
onClicked: {
|
||||
Backend.login2PasswordRequested("")
|
||||
user.isLogin2PasswordRequested = true
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "error"
|
||||
|
||||
enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
|
||||
onClicked: {
|
||||
Backend.login2PasswordError("")
|
||||
|
||||
user.isLogin2PasswordProvided = false
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Abort"
|
||||
|
||||
enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
|
||||
onClicked: {
|
||||
Backend.login2PasswordErrorAbort("")
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Login Finished"
|
||||
|
||||
onClicked: {
|
||||
Backend.loginFinished(0+loginFinishedIndex.text)
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
TextField {
|
||||
id: loginFinishedIndex
|
||||
colorScheme: root.colorScheme
|
||||
label: "Index:"
|
||||
text: root.userIndex
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Already logged in"
|
||||
|
||||
onClicked: {
|
||||
Backend.loginAlreadyLoggedIn(0+loginAlreadyLoggedInIndex.text)
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
TextField {
|
||||
id: loginAlreadyLoggedInIndex
|
||||
colorScheme: root.colorScheme
|
||||
label: "Index:"
|
||||
text: root.userIndex
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
TextField {
|
||||
colorScheme: root.colorScheme
|
||||
label: "used:"
|
||||
text: user && user.usedBytes ? user.usedBytes : 0
|
||||
onEditingFinished: {
|
||||
user.usedBytes = parseFloat(text)
|
||||
}
|
||||
implicitWidth: 200
|
||||
}
|
||||
TextField {
|
||||
colorScheme: root.colorScheme
|
||||
label: "total:"
|
||||
text: user && user.totalBytes ? user.totalBytes : 0
|
||||
onEditingFinished: {
|
||||
user.totalBytes = parseFloat(text)
|
||||
}
|
||||
implicitWidth: 200
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Split mode"}
|
||||
Toggle { colorScheme: root.colorScheme; checked: user ? user.splitMode : false; onClicked: {user.splitMode = !user.splitMode}}
|
||||
Button { colorScheme: root.colorScheme; text: "Toggle Finished"; onClicked: {user.toggleSplitModeFinished()}}
|
||||
}
|
||||
|
||||
TextArea { // TODO: this is causing binding loop on implicitWidth
|
||||
colorScheme: root.colorScheme
|
||||
text: user && user.addresses ? user.addresses.join("\n") : "user@protonmail.com"
|
||||
Layout.fillWidth: true
|
||||
|
||||
onEditingFinished: {
|
||||
user.addresses = text.split("\n")
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
@ -1,101 +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/>.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
property alias currentIndex: usersListView.currentIndex
|
||||
ListView {
|
||||
id: usersListView
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: 200
|
||||
|
||||
model: Backend.usersTest
|
||||
highlightFollowsCurrentItem: true
|
||||
|
||||
delegate: Item {
|
||||
|
||||
implicitHeight: children[0].implicitHeight + anchors.topMargin + anchors.bottomMargin
|
||||
implicitWidth: children[0].implicitWidth + anchors.leftMargin + anchors.rightMargin
|
||||
|
||||
width: usersListView.width
|
||||
|
||||
anchors.margins: 10
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: modelData.username
|
||||
anchors.margins: 10
|
||||
anchors.fill: parent
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
usersListView.currentIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
highlight: Rectangle {
|
||||
color: root.colorScheme.interaction_default_active
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
text: "+"
|
||||
|
||||
onClicked: {
|
||||
var newUserObject = Backend.userComponent.createObject(Backend)
|
||||
newUserObject.username = Backend.loginUser.username.length > 0 ? Backend.loginUser.username : "test@protonmail.com"
|
||||
newUserObject.state = EUserState.Connected
|
||||
newUserObject.setupGuideSeen = true // Backend.loginUser.setupGuideSeen
|
||||
|
||||
Backend.loginUser.username = ""
|
||||
Backend.loginUser.state = EUserState.SignedOut
|
||||
Backend.loginUser.setupGuideSeen = false
|
||||
|
||||
Backend.users.append( { object: newUserObject } )
|
||||
}
|
||||
}
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "-"
|
||||
|
||||
enabled: usersListView.currentIndex != 0
|
||||
|
||||
onClicked: {
|
||||
// var userObject = Backend.users.get(usersListView.currentIndex - 1)
|
||||
Backend.users.remove(usersListView.currentIndex - 1)
|
||||
// userObject.deleteLater()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,28 +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/>.
|
||||
|
||||
import QtQml.Models
|
||||
|
||||
ListModel {
|
||||
// overriding get method to ignore any role and return directly object itself
|
||||
function get(row) {
|
||||
if (row < 0 || row >= count) {
|
||||
return undefined
|
||||
}
|
||||
return data(index(row, 0), Qt.DisplayRole)
|
||||
}
|
||||
}
|
||||
@ -1,982 +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/>.
|
||||
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import QtQml.Models
|
||||
|
||||
import Qt.labs.platform
|
||||
|
||||
import Proton
|
||||
|
||||
import "./BridgeTest"
|
||||
import BridgePreview
|
||||
|
||||
import Notifications
|
||||
|
||||
Window {
|
||||
id: root
|
||||
|
||||
x: 10
|
||||
y: 10
|
||||
width: 800
|
||||
height: 800
|
||||
|
||||
property ColorScheme colorScheme: ProtonStyle.darkStyle
|
||||
|
||||
flags : Qt.Window | Qt.Dialog
|
||||
visible : true
|
||||
title : "Bridge Test GUI"
|
||||
|
||||
// This is needed because on MacOS if first window shown is not transparent -
|
||||
// all other windows of application will not have transparent background (black
|
||||
// instead of transparency). In our case that mean that if BridgeTest will be
|
||||
// shown before StatusWindow - StatusWindow will not have transparent corners.
|
||||
color: "transparent"
|
||||
|
||||
function getCursorPos() {
|
||||
return BridgePreview.getCursorPos()
|
||||
}
|
||||
|
||||
function restart() {
|
||||
root.quit()
|
||||
console.log("Restarting....")
|
||||
root.openBridge()
|
||||
}
|
||||
|
||||
function openBridge() {
|
||||
bridge = bridgeComponent.createObject()
|
||||
var showSetupGuide = false
|
||||
if (showSetupGuide) {
|
||||
var newUserObject = root.userComponent.createObject(root)
|
||||
newUserObject.username = "LerooooyJenkins@protonmail.com"
|
||||
newUserObject.state = EUserState.Connected
|
||||
newUserObject.setupGuideSeen = false
|
||||
root.users.append( { object: newUserObject } )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function quit() {
|
||||
if (bridge !== undefined && bridge !== null) {
|
||||
bridge.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
function guiReady() {
|
||||
console.log("Gui Ready")
|
||||
}
|
||||
|
||||
function _log(msg, color) {
|
||||
logTextArea.text += "<p style='color: " + color + ";'>" + msg + "</p>"
|
||||
logTextArea.text += "\n"
|
||||
}
|
||||
|
||||
function log(msg) {
|
||||
console.log(msg)
|
||||
_log(msg, root.colorScheme.signal_info)
|
||||
}
|
||||
|
||||
function error(msg) {
|
||||
console.error(msg)
|
||||
_log(msg, root.colorScheme.signal_danger)
|
||||
}
|
||||
|
||||
// No user object should be put in this list until a successful login
|
||||
property var users: UserModel {
|
||||
id: _users
|
||||
|
||||
onRowsInserted: {
|
||||
for (var i = first; i <= last; i++) {
|
||||
_usersTest.insert(i + 1, { object: get(i) } )
|
||||
}
|
||||
}
|
||||
|
||||
onRowsRemoved: {
|
||||
_usersTest.remove(first + 1, first - last + 1)
|
||||
}
|
||||
|
||||
onRowsMoved: {
|
||||
_usersTest.move(start + 1, row + 1, end - start + 1)
|
||||
}
|
||||
|
||||
onDataChanged: {
|
||||
for (var i = topLeft.row; i <= bottomRight.row; i++) {
|
||||
_usersTest.set(i + 1, { object: get(i) } )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this list is used on test gui: it contains same users list as users above + fake user to represent login request of new user on pos 0
|
||||
property var usersTest: UserModel {
|
||||
id: _usersTest
|
||||
}
|
||||
|
||||
property var userComponent: Component {
|
||||
id: _userComponent
|
||||
|
||||
QtObject {
|
||||
property string username: ""
|
||||
property bool loggedIn: false
|
||||
property bool splitMode: false
|
||||
|
||||
property bool setupGuideSeen: true
|
||||
|
||||
property var usedBytes: 5350*1024*1024
|
||||
property var totalBytes: 20*1024*1024*1024
|
||||
property string avatarText: "jd"
|
||||
|
||||
property string password: "SMj975NnEYYsqu55GGmlpv"
|
||||
property var addresses: [
|
||||
"jaanedoe@protonmail.com",
|
||||
"jane@pm.me",
|
||||
"jdoe@pm.me"
|
||||
]
|
||||
|
||||
signal loginUsernamePasswordError()
|
||||
signal loginFreeUserError()
|
||||
signal loginConnectionError()
|
||||
signal login2FARequested()
|
||||
signal login2FAError()
|
||||
signal login2FAErrorAbort()
|
||||
signal login2PasswordRequested()
|
||||
signal login2PasswordError()
|
||||
signal login2PasswordErrorAbort()
|
||||
|
||||
// Test purpose only:
|
||||
property bool isFakeUser: this === root.loginUser
|
||||
|
||||
function userSignal(msg) {
|
||||
if (isFakeUser) {
|
||||
return
|
||||
}
|
||||
|
||||
root.log("<- User (" + username + "): " + msg)
|
||||
}
|
||||
|
||||
function toggleSplitMode(makeActive) {
|
||||
userSignal("toggle split mode "+makeActive)
|
||||
}
|
||||
signal toggleSplitModeFinished()
|
||||
|
||||
function configureAppleMail(address){
|
||||
userSignal("configure apple mail "+address)
|
||||
}
|
||||
|
||||
function logout(){
|
||||
userSignal("logout")
|
||||
loggedIn = false
|
||||
}
|
||||
function remove(){
|
||||
console.log("remove this", users.count)
|
||||
for (var i=0; i<users.count; i++) {
|
||||
if (users.get(i) === this) {
|
||||
users.remove(i,1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onLoginUsernamePasswordError: {
|
||||
userSignal("loginUsernamePasswordError")
|
||||
}
|
||||
onLoginFreeUserError: {
|
||||
userSignal("loginFreeUserError")
|
||||
}
|
||||
onLoginConnectionError: {
|
||||
userSignal("loginConnectionError")
|
||||
}
|
||||
onLogin2FARequested: {
|
||||
userSignal("login2FARequested")
|
||||
}
|
||||
onLogin2FAError: {
|
||||
userSignal("login2FAError")
|
||||
}
|
||||
onLogin2FAErrorAbort: {
|
||||
userSignal("login2FAErrorAbort")
|
||||
}
|
||||
onLogin2PasswordRequested: {
|
||||
userSignal("login2PasswordRequested")
|
||||
}
|
||||
onLogin2PasswordError: {
|
||||
userSignal("login2PasswordError")
|
||||
}
|
||||
onLogin2PasswordErrorAbort: {
|
||||
userSignal("login2PasswordErrorAbort")
|
||||
}
|
||||
|
||||
function resetLoginRequests() {
|
||||
isLoginRequested = false
|
||||
isLogin2FARequested = false
|
||||
isLogin2FAProvided = false
|
||||
isLogin2PasswordRequested = false
|
||||
isLogin2PasswordProvided = false
|
||||
}
|
||||
|
||||
property bool isLoginRequested: false
|
||||
|
||||
property bool isLogin2FARequested: false
|
||||
property bool isLogin2FAProvided: false
|
||||
|
||||
property bool isLogin2PasswordRequested: false
|
||||
property bool isLogin2PasswordProvided: false
|
||||
}
|
||||
}
|
||||
|
||||
// this it fake user used only for representing first login request
|
||||
property var loginUser
|
||||
Component.onCompleted: {
|
||||
var newLoginUser = _userComponent.createObject()
|
||||
root.loginUser = newLoginUser
|
||||
root.loginUser.setupGuideSeen = false
|
||||
_usersTest.append({object: newLoginUser})
|
||||
|
||||
newLoginUser.loginUsernamePasswordError.connect(root.loginUsernamePasswordError)
|
||||
newLoginUser.loginFreeUserError.connect(root.loginFreeUserError)
|
||||
newLoginUser.loginConnectionError.connect(root.loginConnectionError)
|
||||
newLoginUser.login2FARequested.connect(root.login2FARequested)
|
||||
newLoginUser.login2FAError.connect(root.login2FAError)
|
||||
newLoginUser.login2FAErrorAbort.connect(root.login2FAErrorAbort)
|
||||
newLoginUser.login2PasswordRequested.connect(root.login2PasswordRequested)
|
||||
newLoginUser.login2PasswordError.connect(root.login2PasswordError)
|
||||
newLoginUser.login2PasswordErrorAbort.connect(root.login2PasswordErrorAbort)
|
||||
|
||||
|
||||
// add one user on start
|
||||
var hasUserOnStart = false
|
||||
if (hasUserOnStart) {
|
||||
var newUserObject = root.userComponent.createObject(root)
|
||||
newUserObject.username = "LerooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooyJenkins@protonmail.com"
|
||||
newUserObject.loggedIn = EUserState.Connected
|
||||
newUserObject.setupGuideSeen = true
|
||||
root.users.append( { object: newUserObject } )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TabBar {
|
||||
id: tabBar
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
TabButton {
|
||||
text: "Global settings"
|
||||
}
|
||||
|
||||
TabButton {
|
||||
text: "User control"
|
||||
}
|
||||
|
||||
TabButton {
|
||||
text: "Notifications"
|
||||
}
|
||||
|
||||
TabButton {
|
||||
text: "Log"
|
||||
}
|
||||
|
||||
TabButton {
|
||||
text: "Settings signals"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: root.colorScheme.background_norm
|
||||
|
||||
anchors.top: tabBar.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
implicitHeight: children[0].contentHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].contentWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
|
||||
StackLayout {
|
||||
anchors.fill: parent
|
||||
currentIndex: tabBar.currentIndex
|
||||
anchors.margins: 10
|
||||
|
||||
RowLayout {
|
||||
id: globalTab
|
||||
spacing : 5
|
||||
|
||||
ColumnLayout {
|
||||
spacing : 5
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Global settings"
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
id: styleRadioGroup
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: "Light UI"
|
||||
checked: ProtonStyle.currentStyle === ProtonStyle.lightStyle
|
||||
ButtonGroup.group: styleRadioGroup
|
||||
|
||||
onCheckedChanged: {
|
||||
if (checked && ProtonStyle.currentStyle !== ProtonStyle.lightStyle) {
|
||||
root.colorSchemeName = "light"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: "Dark UI"
|
||||
checked: ProtonStyle.currentStyle === ProtonStyle.darkStyle
|
||||
ButtonGroup.group: styleRadioGroup
|
||||
|
||||
onCheckedChanged: {
|
||||
if (checked && ProtonStyle.currentStyle !== ProtonStyle.darkStyle) {
|
||||
root.colorSchemeName = "dark"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: showOnStartupCheckbox
|
||||
colorScheme: root.colorScheme
|
||||
text: "Show on startup"
|
||||
checked: root.showOnStartup
|
||||
onCheckedChanged: {
|
||||
root.showOnStartup = checked
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: showSplashScreen
|
||||
colorScheme: root.colorScheme
|
||||
text: "Show splash screen"
|
||||
checked: root.showSplashScreen
|
||||
onCheckedChanged: {
|
||||
root.showSplashScreen = checked
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
//Layout.fillWidth: true
|
||||
|
||||
text: "Open Bridge"
|
||||
enabled: bridge === undefined || bridge === null
|
||||
onClicked: root.openBridge()
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
//Layout.fillWidth: true
|
||||
|
||||
text: "Close Bridge"
|
||||
enabled: bridge !== undefined && bridge !== null
|
||||
onClicked: {
|
||||
bridge.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing : 5
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Notifications"
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Notify: danger"
|
||||
enabled: bridge !== undefined && bridge !== null
|
||||
onClicked: {
|
||||
bridge.mainWindow.notifyOnlyPaidUsers()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Notify: warning"
|
||||
enabled: bridge !== undefined && bridge !== null
|
||||
onClicked: {
|
||||
bridge.mainWindow.notifyUpdateManually()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Notify: success"
|
||||
enabled: bridge !== undefined && bridge !== null
|
||||
onClicked: {
|
||||
bridge.mainWindow.notifyUserAdded()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: usersTab
|
||||
UserList {
|
||||
id: usersListView
|
||||
Layout.fillHeight: true
|
||||
colorScheme: root.colorScheme
|
||||
backend: root
|
||||
}
|
||||
|
||||
UserControl {
|
||||
colorScheme: root.colorScheme
|
||||
backend: root
|
||||
user: ((root.usersTest.count > usersListView.currentIndex) && usersListView.currentIndex != -1) ? root.usersTest.get(usersListView.currentIndex) : undefined
|
||||
userIndex: usersListView.currentIndex - 1 // -1 because 0 index is fake user
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: notificationsTab
|
||||
spacing: 5
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 5
|
||||
|
||||
Switch {
|
||||
text: "Internet connection"
|
||||
colorScheme: root.colorScheme
|
||||
checked: true
|
||||
onCheckedChanged: {
|
||||
checked ? root.internetOn() : root.internetOff()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update manual ready"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateManualReady("3.14.1592")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update manual done"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateManualRestartNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update manual error"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateManualError()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update force"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateForce("3.14.1592")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update force error"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateForceError()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update silent done"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateSilentRestartNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update silent error"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateSilentError()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update is latest version"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateIsLatestVersion()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Bug report send OK"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.reportBugFinished()
|
||||
root.bugReportSendSuccess()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 5
|
||||
|
||||
Button {
|
||||
text: "Bug report send error"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.reportBugFinished()
|
||||
root.bugReportSendError()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Cache anavailable"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.cacheUnavailable()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Cache can't move"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.cacheCantMove()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Cache location change success"
|
||||
onClicked: {
|
||||
root.cacheLocationChangeSuccess()
|
||||
}
|
||||
colorScheme: root.colorScheme
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Disk full"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.diskFull()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "No keychain"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.notifyHasNoKeychain()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Rebuild keychain"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.notifyRebuildKeychain()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Address changed"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.addressChanged("p@v.el")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Address changed + Logout"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.addressChangedLogout("p@v.el")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextArea {
|
||||
id: logTextArea
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
Layout.preferredWidth: 400
|
||||
Layout.preferredHeight: 200
|
||||
|
||||
textFormat: TextEdit.RichText
|
||||
//readOnly: true
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: settingsTab
|
||||
ColumnLayout {
|
||||
RowLayout {
|
||||
Label {colorScheme : root.colorScheme ; text : "GOOS : "}
|
||||
Button {colorScheme : root.colorScheme ; text : "Linux" ; onClicked : root.goos = "linux" ; enabled: root.goos != "linux"}
|
||||
Button {colorScheme : root.colorScheme ; text : "Windows" ; onClicked : root.goos = "windows" ; enabled: root.goos != "windows"}
|
||||
Button {colorScheme : root.colorScheme ; text : "macOS" ; onClicked : root.goos = "darwin" ; enabled: root.goos != "darwin"}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Automatic updates:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isAutomaticUpdateOn; onClicked: root.isAutomaticUpdateOn = !root.isAutomaticUpdateOn}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Autostart:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isAutostartOn; onClicked: root.isAutostartOn = !root.isAutostartOn}
|
||||
Button {colorScheme: root.colorScheme; text: "Toggle finished"; onClicked: root.toggleAutostartFinished()}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Beta:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isBetaEnabled; onClicked: root.isBetaEnabled = !root.isBetaEnabled}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "DoH:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isDoHEnabled; onClicked: root.isDoHEnabled = !root.isDoHEnabled}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "All Mail disabled:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isAllMailVisible; onClicked: root.isAllMailVisible = !root.isAllMailVisible}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Ports:"}
|
||||
TextField {
|
||||
colorScheme:root.colorScheme
|
||||
label: "IMAP"
|
||||
text: root.portIMAP
|
||||
onEditingFinished: root.portIMAP = this.text*1
|
||||
validator: IntValidator {bottom: 1; top: 65536}
|
||||
}
|
||||
TextField {
|
||||
colorScheme:root.colorScheme
|
||||
label: "SMTP"
|
||||
text: root.portSMTP
|
||||
onEditingFinished: root.portSMTP = this.text*1
|
||||
validator: IntValidator {bottom: 1; top: 65536}
|
||||
}
|
||||
Button {colorScheme: root.colorScheme; text: "Change finished"; onClicked: root.changePortFinished()}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "SMTP using SSL:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.useSSLForSMTP; onClicked: root.useSSLForSMTP = !root.useSSLForSMTP}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Local cache:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isDiskCacheEnabled; onClicked: root.isDiskCacheEnabled = !root.isDiskCacheEnabled}
|
||||
TextField {
|
||||
colorScheme:root.colorScheme
|
||||
label: "Path"
|
||||
text: root.diskCachePath.toString().replace("file://", "")
|
||||
implicitWidth: 160
|
||||
onEditingFinished: {
|
||||
root.diskCachePath = Qt.resolvedUrl("file://"+text)
|
||||
}
|
||||
}
|
||||
Button {colorScheme: root.colorScheme; text: "Change finished"; onClicked: root.changeLocalCacheFinished()}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Reset:"}
|
||||
Button {colorScheme: root.colorScheme; text: "Finished"; onClicked: root.resetFinished()}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Check update:"}
|
||||
Button {colorScheme: root.colorScheme; text: "Finished"; onClicked: root.checkUpdatesFinished()}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Bridge bridge
|
||||
|
||||
property string goos: "darwin"
|
||||
|
||||
property bool showOnStartup: true // this actually needs to be false, but since we use Bridge_test for testing purpose - lets default this to true just for convenience
|
||||
property bool dockIconVisible: false
|
||||
|
||||
// this signals are used only when trying to login with new user (i.e. not in users model)
|
||||
signal loginUsernamePasswordError(string errorMsg)
|
||||
signal loginFreeUserError()
|
||||
signal loginConnectionError(string errorMsg)
|
||||
signal login2FARequested(string username)
|
||||
signal login2FAError(string errorMsg)
|
||||
signal login2FAErrorAbort(string errorMsg)
|
||||
signal login2PasswordRequested()
|
||||
signal login2PasswordError(string errorMsg)
|
||||
signal login2PasswordErrorAbort(string errorMsg)
|
||||
signal loginFinished(int index)
|
||||
signal loginAlreadyLoggedIn(int index)
|
||||
|
||||
signal internetOff()
|
||||
signal internetOn()
|
||||
|
||||
signal updateManualReady(var version)
|
||||
signal updateManualRestartNeeded()
|
||||
signal updateManualError()
|
||||
signal updateForce(var version)
|
||||
signal updateForceError()
|
||||
signal updateSilentRestartNeeded()
|
||||
signal updateSilentError()
|
||||
signal updateIsLatestVersion()
|
||||
function checkUpdates(){
|
||||
console.log("check updates")
|
||||
}
|
||||
signal checkUpdatesFinished()
|
||||
function installUpdate() {
|
||||
console.log("manuall install update triggered")
|
||||
}
|
||||
|
||||
|
||||
property bool isDiskCacheEnabled: true
|
||||
// Qt.resolvedUrl("file:///C:/Users/user/AppData/Roaming/protonmail/bridge-v3/cache/c11/messages")
|
||||
property url diskCachePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
signal cacheUnavailable()
|
||||
signal cacheCantMove()
|
||||
signal cacheLocationChangeSuccess()
|
||||
signal diskFull()
|
||||
function changeLocalCache(enableDiskCache, diskCachePath) {
|
||||
console.debug("-> disk cache", enableDiskCache, diskCachePath)
|
||||
}
|
||||
signal changeLocalCacheFinished()
|
||||
|
||||
|
||||
// Settings
|
||||
property bool isAutomaticUpdateOn : true
|
||||
function toggleAutomaticUpdate(makeItActive) {
|
||||
console.debug("-> silent updates", makeItActive, root.isAutomaticUpdateOn)
|
||||
var callback = function () {
|
||||
root.isAutomaticUpdateOn = makeItActive;
|
||||
console.debug("-> CHANGED silent updates", makeItActive, root.isAutomaticUpdateOn)
|
||||
}
|
||||
atimer.onTriggered.connect(callback)
|
||||
atimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: atimer
|
||||
interval: 2000
|
||||
running: false
|
||||
repeat: false
|
||||
}
|
||||
|
||||
property bool isAutostartOn : true // Example of settings with loading state
|
||||
function toggleAutostart(makeItActive) {
|
||||
console.debug("-> autostart", makeItActive, root.isAutostartOn)
|
||||
}
|
||||
signal toggleAutostartFinished()
|
||||
|
||||
property bool isBetaEnabled : false
|
||||
function toggleBeta(makeItActive){
|
||||
console.debug("-> beta", makeItActive, root.isBetaEnabled)
|
||||
root.isBetaEnabled = makeItActive
|
||||
}
|
||||
|
||||
property bool isDoHEnabled : true
|
||||
function toggleDoH(makeItActive){
|
||||
console.debug("-> DoH", makeItActive, root.isDoHEnabled)
|
||||
root.isDoHEnabled = makeItActive
|
||||
}
|
||||
|
||||
property bool isAllMailVisible : true
|
||||
function changeIsAllMailVisible(isVisible){
|
||||
console.debug("-> All Mail Visible", isVisible, root.isAllMailVisible)
|
||||
root.isAllMailVisible = isVisible
|
||||
}
|
||||
|
||||
|
||||
property bool useSSLForSMTP: false
|
||||
function toggleUseSSLForSMTP(makeItActive){
|
||||
console.debug("-> SMTP SSL", makeItActive, root.useSSLForSMTP)
|
||||
}
|
||||
signal toggleUseSSLFinished()
|
||||
|
||||
property string hostname: "127.0.0.1"
|
||||
property int portIMAP: 1143
|
||||
property int portSMTP: 1025
|
||||
function changePorts(imapPort, smtpPort){
|
||||
console.debug("-> ports", imapPort, smtpPort)
|
||||
}
|
||||
function isPortFree(port){
|
||||
if (port == portIMAP) return false
|
||||
if (port == portSMTP) return false
|
||||
if (port == 12345) return false
|
||||
return true
|
||||
}
|
||||
signal changePortFinished()
|
||||
signal imapPortStartupError()
|
||||
signal smtpPortStartupError()
|
||||
|
||||
function triggerReset() {
|
||||
console.debug("-> trigger reset")
|
||||
}
|
||||
signal resetFinished()
|
||||
|
||||
property string version: "2.0.X-BridePreview"
|
||||
property url logsPath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
property url licensePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
property url releaseNotesLink: Qt.resolvedUrl("https://proton.me/download/bridge/early_releases.html")
|
||||
property url dependencyLicensesLink: Qt.resolvedUrl("https://github.com/ProtonMail/proton-bridge/v3/blob/master/COPYING_NOTES.md#dependencies")
|
||||
property url landingPageLink: Qt.resolvedUrl("https://proton.me/mail/bridge#download")
|
||||
|
||||
property string colorSchemeName: "light"
|
||||
function changeColorScheme(newScheme){
|
||||
root.colorSchemeName = newScheme
|
||||
}
|
||||
|
||||
|
||||
property string currentEmailClient: "" // "Apple Mail 14.0"
|
||||
function updateCurrentMailClient(){
|
||||
currentEmailClient = "Apple Mail 14.0"
|
||||
}
|
||||
|
||||
function reportBug(description,address,emailClient,includeLogs){
|
||||
console.log("report bug")
|
||||
console.log(" description",description)
|
||||
console.log(" address",address)
|
||||
console.log(" emailClient",emailClient)
|
||||
console.log(" includeLogs",includeLogs)
|
||||
}
|
||||
signal reportBugFinished()
|
||||
signal bugReportSendSuccess()
|
||||
signal bugReportSendError()
|
||||
|
||||
property var availableKeychain: ["gnome-keyring", "pass", "macos-keychain", "windows-credentials"]
|
||||
property string currentKeychain: availableKeychain[0]
|
||||
function changeKeychain(wantedKeychain){
|
||||
console.log("Changing keychain from", root.currentKeychain, "to", wantedKeychain)
|
||||
root.currentKeychain = wantedKeychain
|
||||
root.changeKeychainFinished()
|
||||
}
|
||||
signal changeKeychainFinished()
|
||||
signal notifyHasNoKeychain()
|
||||
signal notifyRebuildKeychain()
|
||||
|
||||
signal noActiveKeyForRecipient(string email)
|
||||
signal showMainWindow()
|
||||
|
||||
signal addressChanged(string address)
|
||||
signal addressChangedLogout(string address)
|
||||
signal userDisconnected(string username)
|
||||
signal apiCertIssue()
|
||||
|
||||
property bool showSplashScreen: false
|
||||
|
||||
|
||||
function login(username, password) {
|
||||
root.log("-> login(" + username + ", " + password + ")")
|
||||
|
||||
loginUser.username = username
|
||||
loginUser.isLoginRequested = true
|
||||
}
|
||||
|
||||
function login2FA(username, code) {
|
||||
root.log("-> login2FA(" + username + ", " + code + ")")
|
||||
|
||||
loginUser.isLogin2FAProvided = true
|
||||
}
|
||||
|
||||
function login2Password(username, password) {
|
||||
root.log("-> login2FA(" + username + ", " + password + ")")
|
||||
|
||||
loginUser.isLogin2PasswordProvided = true
|
||||
}
|
||||
|
||||
function loginAbort(username) {
|
||||
root.log("-> loginAbort(" + username + ")")
|
||||
|
||||
loginUser.resetLoginRequests()
|
||||
}
|
||||
|
||||
|
||||
onLoginUsernamePasswordError: {
|
||||
console.debug("<- loginUsernamePasswordError")
|
||||
}
|
||||
onLoginFreeUserError: {
|
||||
console.debug("<- loginFreeUserError")
|
||||
}
|
||||
onLoginConnectionError: {
|
||||
console.debug("<- loginConnectionError")
|
||||
}
|
||||
onLogin2FARequested: {
|
||||
console.debug("<- login2FARequested", username)
|
||||
}
|
||||
onLogin2FAError: {
|
||||
console.debug("<- login2FAError")
|
||||
}
|
||||
onLogin2FAErrorAbort: {
|
||||
console.debug("<- login2FAErrorAbort")
|
||||
}
|
||||
onLogin2PasswordRequested: {
|
||||
console.debug("<- login2PasswordRequested")
|
||||
}
|
||||
onLogin2PasswordError: {
|
||||
console.debug("<- login2PasswordError")
|
||||
}
|
||||
onLogin2PasswordErrorAbort: {
|
||||
console.debug("<- login2PasswordErrorAbort")
|
||||
}
|
||||
onLoginFinished: {
|
||||
console.debug("<- loginFinished", index)
|
||||
}
|
||||
onLoginAlreadyLoggedIn: {
|
||||
console.debug("<- loginAlreadyLoggedIn", index)
|
||||
}
|
||||
|
||||
onInternetOff: {
|
||||
console.debug("<- internetOff")
|
||||
}
|
||||
onInternetOn: {
|
||||
console.debug("<- internetOn")
|
||||
}
|
||||
|
||||
Component {
|
||||
id: bridgeComponent
|
||||
|
||||
Bridge {
|
||||
backend: root
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
onClosing: {
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
@ -24,8 +24,6 @@ import QtQuick.Controls
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
import "tests"
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
colorScheme: ProtonStyle.currentStyle
|
||||
@ -79,10 +77,6 @@ ApplicationWindow {
|
||||
root.showAndRise()
|
||||
}
|
||||
|
||||
function onSelectUser(userID) {
|
||||
root.selectUser(userID)
|
||||
}
|
||||
|
||||
function onLoginFinished(index, wasSignedOut) {
|
||||
var user = Backend.users.get(index)
|
||||
if (user && !wasSignedOut) {
|
||||
@ -90,6 +84,21 @@ ApplicationWindow {
|
||||
}
|
||||
console.debug("Login finished", index)
|
||||
}
|
||||
|
||||
function onShowHelp() {
|
||||
root.showHelp()
|
||||
root.showAndRise()
|
||||
}
|
||||
|
||||
function onShowSettings() {
|
||||
root.showSettings()
|
||||
root.showAndRise()
|
||||
}
|
||||
|
||||
function onSelectUser(userID) {
|
||||
contentWrapper.selectUser(userID)
|
||||
root.showAndRise()
|
||||
}
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
|
||||
@ -24,8 +24,6 @@ QtObject {
|
||||
id: root
|
||||
|
||||
property MainWindow frontendMain
|
||||
property StatusWindow frontendStatus
|
||||
property SystemTrayIcon frontendTray
|
||||
|
||||
signal askEnableBeta()
|
||||
signal askEnableSplitMode(var user)
|
||||
@ -140,7 +138,7 @@ QtObject {
|
||||
|
||||
property Notification imapPortChangeError: Notification {
|
||||
description: qsTr("The IMAP port could not be changed.")
|
||||
brief: qsTr("IMAP port change error")
|
||||
brief: qsTr("IMAP port error")
|
||||
icon: "./icons/ic-alert.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Connection
|
||||
@ -156,7 +154,7 @@ QtObject {
|
||||
|
||||
property Notification smtpPortChangeError: Notification {
|
||||
description: qsTr("The SMTP port could not be changed.")
|
||||
brief: qsTr("SMTP port change error")
|
||||
brief: qsTr("SMTP port error")
|
||||
icon: "./icons/ic-alert.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Connection
|
||||
@ -172,7 +170,7 @@ QtObject {
|
||||
|
||||
property Notification imapConnectionModeChangeError: Notification {
|
||||
description: qsTr("The IMAP connection mode could not be changed.")
|
||||
brief: qsTr("IMAP Connection mode change error")
|
||||
brief: qsTr("IMAP Connection mode error")
|
||||
icon: "./icons/ic-alert.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Connection
|
||||
@ -196,7 +194,7 @@ QtObject {
|
||||
|
||||
property Notification smtpConnectionModeChangeError: Notification {
|
||||
description: qsTr("The SMTP connection mode could not be changed.")
|
||||
brief: qsTr("SMTP Connection mode change error")
|
||||
brief: qsTr("SMTP Connection mode error")
|
||||
icon: "./icons/ic-alert.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Connection
|
||||
@ -227,7 +225,7 @@ QtObject {
|
||||
var link = Backend.releaseNotesLink
|
||||
return `${descr} <a href="${link}">${text}</a>`
|
||||
}
|
||||
brief: qsTr("Update available.")
|
||||
brief: qsTr("Update available")
|
||||
icon: "./icons/ic-info-circle-filled.svg"
|
||||
type: Notification.NotificationType.Info
|
||||
group: Notifications.Group.Update | Notifications.Group.Dialogs
|
||||
@ -514,7 +512,7 @@ QtObject {
|
||||
// login
|
||||
property Notification loginConnectionError: Notification {
|
||||
description: qsTr("Bridge is not able to contact the server, please check your internet connection.")
|
||||
brief: description
|
||||
brief: qsTr("Connection error")
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Configuration
|
||||
@ -538,7 +536,7 @@ QtObject {
|
||||
|
||||
property Notification onlyPaidUsers: Notification {
|
||||
description: qsTr("Bridge is exclusive to our paid plans. Upgrade your account to use Bridge.")
|
||||
brief: description
|
||||
brief: qsTr("Upgrade your account")
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Configuration
|
||||
@ -562,7 +560,7 @@ QtObject {
|
||||
|
||||
property Notification alreadyLoggedIn: Notification {
|
||||
description: qsTr("This account is already signed in.")
|
||||
brief: description
|
||||
brief: qsTr("Already signed in")
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Info
|
||||
group: Notifications.Group.Configuration
|
||||
@ -587,7 +585,7 @@ QtObject {
|
||||
// Bug reports
|
||||
property Notification bugReportSendSuccess: Notification {
|
||||
description: qsTr("Thank you for the report. We'll get back to you as soon as we can.")
|
||||
brief: description
|
||||
brief: qsTr("Report sent")
|
||||
icon: "./icons/ic-info-circle-filled.svg"
|
||||
type: Notification.NotificationType.Success
|
||||
group: Notifications.Group.Configuration
|
||||
@ -611,7 +609,7 @@ QtObject {
|
||||
|
||||
property Notification bugReportSendError: Notification {
|
||||
description: qsTr("Report could not be sent. Try again or email us directly.")
|
||||
brief: description
|
||||
brief: qsTr("Error sending report")
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Configuration
|
||||
@ -634,8 +632,8 @@ QtObject {
|
||||
// Cache
|
||||
property Notification cacheUnavailable: Notification {
|
||||
title: qsTr("Cache location is unavailable")
|
||||
description: qsTr("Check the directory or change it in your settings.")
|
||||
brief: qsTr("The current cache location is unavailable. Check the directory or change it in your settings.")
|
||||
description: qsTr("The current cache location is unavailable. Check the directory or change it in your settings.")
|
||||
brief: title
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Warning
|
||||
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
|
||||
@ -725,7 +723,7 @@ QtObject {
|
||||
// Other
|
||||
property Notification accountChanged: Notification {
|
||||
description: qsTr("The address list for .... account has changed. You need to reconfigure your email client.")
|
||||
brief: qsTr("The address list for your account has changed. Reconfigure your email client.")
|
||||
brief: qsTr("Address list changed")
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Configuration
|
||||
@ -742,7 +740,7 @@ QtObject {
|
||||
property Notification diskFull: Notification {
|
||||
title: qsTr("Your disk is almost full")
|
||||
description: qsTr("Quit Bridge and free disk space or disable the local cache (not recommended).")
|
||||
brief: qsTr("Your disk is almost full. Free disk space or disable the local cache.")
|
||||
brief: title
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Warning
|
||||
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
|
||||
@ -948,8 +946,8 @@ QtObject {
|
||||
|
||||
property Notification noKeychain: Notification {
|
||||
title: qsTr("No keychain available")
|
||||
description: qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.")
|
||||
brief: title
|
||||
description: qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.")
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
|
||||
@ -982,8 +980,8 @@ QtObject {
|
||||
|
||||
property Notification rebuildKeychain: Notification {
|
||||
title: qsTr("Your macOS keychain might be corrupted")
|
||||
description: qsTr("Bridge is not able to access your macOS keychain. Please consult the instructions on our support page.")
|
||||
brief: title
|
||||
description: qsTr("Bridge is not able to access your macOS keychain. Please consult the instructions on our support page.")
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
|
||||
@ -1014,8 +1012,8 @@ QtObject {
|
||||
|
||||
property Notification addressChanged: Notification {
|
||||
title: qsTr("Address list changes")
|
||||
brief: title
|
||||
description: qsTr("The address list for your account has changed. You might need to reconfigure your email client.")
|
||||
brief: description
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Warning
|
||||
group: Notifications.Group.Configuration
|
||||
@ -1047,11 +1045,11 @@ QtObject {
|
||||
|
||||
property Notification apiCertIssue: Notification {
|
||||
title: qsTr("Unable to establish a \nsecure connection to \nProton servers")
|
||||
brief: qsTr("Cannot establish secure connection")
|
||||
description: qsTr("Bridge cannot verify the authenticity of Proton servers on your current network due to a TLS certificate error. " +
|
||||
"Start Bridge again after ensuring your connection is secure and/or connecting to a VPN. Learn more about TLS pinning " +
|
||||
"<a href=\"https://proton.me/blog/tls-ssl-certificate#Extra-security-precautions-taken-by-ProtonMail\">here</a>.")
|
||||
|
||||
brief: title
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Dialogs | Notifications.Group.Connection
|
||||
@ -1078,6 +1076,7 @@ QtObject {
|
||||
|
||||
property Notification noActiveKeyForRecipient: Notification {
|
||||
title: qsTr("Unable to send \nencrypted message")
|
||||
brief: title
|
||||
description: "#PlaceholderText#"
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
@ -1174,8 +1173,9 @@ QtObject {
|
||||
}
|
||||
|
||||
property Notification genericError: Notification {
|
||||
title: "#PlaceholderText#"
|
||||
description: "#PlaceholderText#"
|
||||
title: ""
|
||||
brief: title
|
||||
description: ""
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Dialogs
|
||||
@ -1201,7 +1201,7 @@ QtObject {
|
||||
|
||||
property Notification genericQuestion: Notification {
|
||||
title: ""
|
||||
brief: ""
|
||||
brief: title
|
||||
description: ""
|
||||
type: Notification.NotificationType.Warning
|
||||
group: Notifications.Group.Dialogs
|
||||
|
||||
@ -1,352 +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/>.
|
||||
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
Window {
|
||||
id: root
|
||||
|
||||
height: contentLayout.implicitHeight
|
||||
width: contentLayout.implicitWidth
|
||||
|
||||
flags: (Qt.platform.os === "linux" ? Qt.Tool : 0) | Qt.FramelessWindowHint | Qt.NoDropShadowWindowHint | Qt.WindowStaysOnTopHint | Qt.WA_TranslucentBackground
|
||||
color: "transparent"
|
||||
|
||||
property ColorScheme colorScheme: ProtonStyle.currentStyle
|
||||
|
||||
property var notifications
|
||||
|
||||
signal showMainWindow()
|
||||
signal showHelp()
|
||||
signal showSettings()
|
||||
signal selectUser(string userID)
|
||||
signal quit()
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
function enableHoverOnOpenBridgeButton() {
|
||||
openBridgeButton.hoverEnabled = true
|
||||
mouseArea.positionChanged.disconnect(enableHoverOnOpenBridgeButton)
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) { // GODT-1479 To avoid a visual glitch where the 'Open bridge button' would appear hovered when the status windows opens,
|
||||
// we've disabled hover on it when it was last closed. Re-enabling hover here will not work on all platforms. so we temporarily connect
|
||||
// mouse move event over the window's mouseArea to a function that will re-enable hover on the open bridge button.
|
||||
openBridgeButton.focus = false
|
||||
mouseArea.positionChanged.connect(enableHoverOnOpenBridgeButton)
|
||||
} else {
|
||||
menu.close()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
|
||||
Layout.minimumHeight: 201
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
ColumnLayout {
|
||||
Layout.minimumWidth: 448
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
implicitHeight: 12
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: parent.height * 2
|
||||
radius: ProtonStyle.dialog_radius
|
||||
|
||||
color: {
|
||||
if (!statusItem.activeNotification) {
|
||||
return root.colorScheme.signal_success
|
||||
}
|
||||
|
||||
switch (statusItem.activeNotification.type) {
|
||||
case Notification.NotificationType.Danger:
|
||||
return root.colorScheme.signal_danger
|
||||
case Notification.NotificationType.Warning:
|
||||
return root.colorScheme.signal_warning
|
||||
case Notification.NotificationType.Success:
|
||||
return root.colorScheme.signal_success
|
||||
case Notification.NotificationType.Info:
|
||||
return root.colorScheme.signal_info
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
|
||||
color: colorScheme.background_norm
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
anchors.topMargin: 8
|
||||
anchors.bottomMargin: 8
|
||||
anchors.leftMargin: 24
|
||||
anchors.rightMargin: 24
|
||||
|
||||
spacing: 8
|
||||
|
||||
Status {
|
||||
id: statusItem
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Layout.topMargin: 12
|
||||
Layout.bottomMargin: 12
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
|
||||
notificationWhitelist: Notifications.Group.Connection | Notifications.Group.Update | Notifications.Group.Configuration
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
secondary: true
|
||||
|
||||
Layout.topMargin: 12
|
||||
Layout.bottomMargin: 12
|
||||
|
||||
visible: statusItem.activeNotification && statusItem.activeNotification.action.length > 0
|
||||
action: statusItem.activeNotification && statusItem.activeNotification.action.length > 0 ? statusItem.activeNotification.action[0] : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: root.colorScheme.background_norm
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 24
|
||||
anchors.rightMargin: 24
|
||||
color: root.colorScheme.border_norm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
Layout.maximumHeight: accountListView.count ?
|
||||
accountListView.contentHeight / accountListView.count * 3 + accountListView.anchors.topMargin + accountListView.anchors.bottomMargin :
|
||||
Number.POSITIVE_INFINITY
|
||||
|
||||
color: root.colorScheme.background_norm
|
||||
clip: true
|
||||
|
||||
implicitHeight: children[0].contentHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].contentWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
|
||||
ListView {
|
||||
id: accountListView
|
||||
|
||||
model: Backend.users
|
||||
anchors.fill: parent
|
||||
|
||||
anchors.topMargin: 8
|
||||
anchors.bottomMargin: 8
|
||||
anchors.leftMargin: 24
|
||||
anchors.rightMargin: 24
|
||||
|
||||
interactive: contentHeight > parent.height
|
||||
snapMode: ListView.SnapToItem
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
spacing: 4
|
||||
|
||||
delegate: Item {
|
||||
id: viewItem
|
||||
width: ListView.view.width
|
||||
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
property var user: Backend.users.get(index)
|
||||
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
anchors.fill: parent
|
||||
|
||||
AccountDelegate {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Layout.topMargin: 12
|
||||
Layout.bottomMargin: 12
|
||||
|
||||
user: viewItem.user
|
||||
colorScheme: root.colorScheme
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.topMargin: 12
|
||||
Layout.bottomMargin: 12
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
visible: viewItem.user ? (viewItem.user.state === EUserState.SignedOut) : false
|
||||
text: qsTr("Sign in")
|
||||
onClicked: {
|
||||
root.selectUser(viewItem.user.id) // selectUser will show login screen if user is in SignedOut state.
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
|
||||
implicitHeight: children[1].implicitHeight + children[1].anchors.topMargin + children[1].anchors.bottomMargin
|
||||
implicitWidth: children[1].implicitWidth + children[1].anchors.leftMargin + children[1].anchors.rightMargin
|
||||
|
||||
// background:
|
||||
clip: true
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: parent.height * 2
|
||||
radius: ProtonStyle.dialog_radius
|
||||
|
||||
color: root.colorScheme.background_weak
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 0
|
||||
|
||||
Button {
|
||||
id: openBridgeButton
|
||||
colorScheme: root.colorScheme
|
||||
secondary: true
|
||||
text: qsTr("Open Bridge")
|
||||
|
||||
borderless: true
|
||||
labelType: Label.LabelType.Caption_semibold
|
||||
|
||||
onClicked: {
|
||||
// GODT-1479: we disable hover for the button to avoid a visual glitch where the button is
|
||||
// wrongly hovered when re-opening the status window after clicking
|
||||
hoverEnabled = false;
|
||||
root.showMainWindow()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
secondary: true
|
||||
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
|
||||
borderless: true
|
||||
checkable: true
|
||||
|
||||
onClicked: {
|
||||
menu.open()
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: menu
|
||||
colorScheme: root.colorScheme
|
||||
modal: true
|
||||
|
||||
y: 0 - height
|
||||
|
||||
MenuItem {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Help")
|
||||
onClicked: {
|
||||
root.showHelp()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Settings")
|
||||
onClicked: {
|
||||
root.showSettings()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Quit Bridge")
|
||||
onClicked: {
|
||||
root.close()
|
||||
root.quit()
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
parent.checked = false
|
||||
}
|
||||
onOpened: {
|
||||
parent.checked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (!active) root.close()
|
||||
}
|
||||
|
||||
function showAndRise() {
|
||||
root.show()
|
||||
root.raise()
|
||||
if (!root.active) {
|
||||
root.requestActivate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(0.533333,0,0,0.533333,238.933,238.933)">
|
||||
<circle cx="512" cy="512" r="480" style="fill:white;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 593 B |
Reference in New Issue
Block a user