GODT-1554 / 1555: Implement gRPC go service and Qt 5 frontend C++ app.

WIP: updates

WIP: cache on disk and autostart.

WIP: mail, keychain and more.

WIP: updated grpc version in go mod file.

WIP: user list.

WIP: RPC service placeholder

WIP: test C++ RPC client skeleton.

Other: missing license script update.

WIP: use Qt test framework.

WIP: test for app and login calls.

WIP: test for update & cache on disk calls.

WIP: tests for mail settings calls.

WIP: all client tests.

WIP: linter fixes.

WIP: fix missing license link.

WIP: update dependency_license script for gRPC and protobuf.

WIP: removed unused file.

WIP: app & login event streaming tests.

WIP: update event stream tests.

WIP: completed event streaming tests.

GODT-1554: qt C++ frontend skeleton.

WIP: C++ backend declaration.

wip: started drafting user model.

WIP: users. not functional.

WIP: invokable methods

WIP: Exception class + backend 'injection' into QML.

WIP: switch to VCPKG to ease multi-arch compilation,  C++ RPC client skeleton.

WIP: Renaming and reorganisation

WIP:introduced new 'grpc' go frontend.

WIP: Worker & Oveerseer for thread management.

WIP: added log to C++ app.

WIP: event stream architecture on Go side.

WIP: event parsing and streamer stopping.

WIP: Moved grpc to frontend subfolder + use vcpkg for gRPC and protobuf.

WIP: windows building ok

WIP: wired a few messages

WIP: more wiring.

WIP: Fixed imports after rebase on top of devel.

WIP: wired some bool and string properties.

WIP: more properties.

WIP: wired cache on disk stuff

WIP: connect event watcher.

WIP: login

WIP: fix showSplashScreen

WIP: Wired login calls.

WIP: user list.

WIP: Refactored main().

WIP: User retrieval .

WIP: no shared pointer in user model.

WIP: fixed user count.

WIP: cached goos.

WIP: Wired autostart

WIP: beta channel toggle wired.

WIP: User removal

WIP: wired theme

WIP: implemented configure apple mail.

WIP: split mode.

WIP: fixed user updates.

WIP: fixed Quit from tray icon

WIP: wired CurrentEmailClient

WIP: wired UseSSLForSMTP

WIP: wired change ports .

WIP: wired DoH. .

WIP: wired keychain calls.

WIP: wired autoupdate option.

WIP: QML Backend clean-up.

WIP: cleanup.

WIP: moved user related files in subfolder. .

WIP: User are managed using smart pointers.

WIP: cleanup.

WIP: more cleanup.

WIP: mail events forwarding

WIP: code inspection tweaks from CLion.

WIP: moved QML, cleanup, and missing copyright notices.

WIP: Backend is not QMLBackend.

Other: fixed issues reported by Leander. [skip ci]
This commit is contained in:
Xavier Michelon
2022-05-16 10:59:45 +02:00
committed by Jakub
parent a4e54f063d
commit c11fe3e1ab
183 changed files with 53334 additions and 10851 deletions

View File

@ -0,0 +1,45 @@
// Copyright (c) 2022 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 "Pch.h"
#include "AppController.h"
#include "QMLBackend.h"
#include "GRPC/GRPCClient.h"
#include "Log.h"
//****************************************************************************************************************************************************
/// \return The AppController instance.
//****************************************************************************************************************************************************
AppController &app()
{
static AppController app;
return app;
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
AppController::AppController()
: backend_(std::make_unique<QMLBackend>())
, grpc_(std::make_unique<GRPCClient>())
, log_(std::make_unique<Log>())
{
}

View File

@ -0,0 +1,59 @@
// Copyright (c) 2022 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_QT6_APP_CONTROLLER_H
#define BRIDGE_QT6_APP_CONTROLLER_H
class QMLBackend;
class GRPCClient;
class Log;
//****************************************************************************************************************************************************
/// \brief App controller class.
//****************************************************************************************************************************************************
class AppController: public QObject
{
Q_OBJECT
friend AppController& app();
public: // member functions.
AppController(AppController const&) = delete; ///< Disabled copy-constructor.
AppController(AppController&&) = delete; ///< Disabled assignment copy-constructor.
~AppController() override = default; ///< Destructor.
AppController& operator=(AppController const&) = delete; ///< Disabled assignment operator.
AppController& operator=(AppController&&) = delete; ///< Disabled move assignment operator.
QMLBackend& backend() { return *backend_; } ///< Return a reference to the backend.
GRPCClient& grpc() { return *grpc_; } ///< Return a reference to the GRPC client.
Log& log() { return *log_; } ///< Return a reference to the log.
private: // member functions
AppController(); ///< Default constructor.
private: // data members
std::unique_ptr<QMLBackend> backend_; ///< The backend.
std::unique_ptr<GRPCClient> grpc_; ///< The RPC client.
std::unique_ptr<Log> log_; ///< The log.
};
AppController& app(); ///< Return a reference to the app controller.
#endif // BRIDGE_QT6_APP_CONTROLLER_H

View File

@ -0,0 +1,136 @@
# Copyright (c) 2022 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/>.
cmake_minimum_required(VERSION 3.22)
set(CMAKE_OSX_ARCHITECTURES x86_64) # needs to be set before the first project() directive.
#We rely on vcpkg for to get gRPC+Protobuf
if (NOT DEFINED ENV{VCPKG_ROOT})
message(FATAL_ERROR "vcpkg is required. Install vcpkg and define VCPKG_ROOT to point the the vcpkg installation folder. (e.g. ~/vcpkg/")
endif()
if (APPLE)
set(VCPKG_TARGET_TRIPLET x64-osx)
endif()
if (WIN32)
set(VCPKG_TARGET_TRIPLET x64-mingw-static)
endif()
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "toolchain")
project(bridge_qt6 LANGUAGES CXX)
if (APPLE) # On macOS, we have some Objective-C++ code in DockIcon to deal with ... the dock icon.
enable_language(OBJC OBJCXX)
endif()
if (NOT DEFINED ENV{QT5DIR})
message(FATAL_ERROR "QT5DIR needs to be defined and point to the root of your Qt5 folder (e.g. /Users/MyName/Qt/5.10.1/clang_64).")
endif()
set(CMAKE_PREFIX_PATH $ENV{QT5DIR} ${CMAKE_PREFIX_PATH})
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(Protobuf CONFIG REQUIRED)
message(STATUS "Using protobuf ${Protobuf_VERSION}")
find_package(grpc CONFIG REQUIRED)
message(STATUS "Using gRPC ${gRPC_VERSION}")
if (APPLE) # We need to link the Cocoa framework for the dock icon.
find_library(COCOA_LIBRARY Cocoa REQUIRED)
endif()
find_package(Qt5 COMPONENTS
Core
Quick
Qml
QuickControls2
REQUIRED)
find_program(PROTOC_EXE protoc)
find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin)
set(PROTO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../grpc")
set(PROTO_FILE "${PROTO_DIR}/bridge.proto")
set(GRPC_OUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/GRPC")
set(PROTO_CPP_FILE "${GRPC_OUT_DIR}/bridge.pb.cc")
set(PROTO_H_FILE "${GRPC_OUT_DIR}/bridge.pb.h")
set(GRPC_CPP_FILE "${GRPC_OUT_DIR}/bridge.grpc.pb.cc")
set(GRPC_H_FILE "${GRPC_OUT_DIR}/bridge.grpc.pb.h")
if (APPLE)
set(DOCK_ICON_SRC_FILE DockIcon/DockIcon.mm)
else()
set(DOCK_ICON_SRC_FILE DockIcon/DockIcon.cpp)
endif()
add_custom_command(
OUTPUT
${PROTO_CPP_FILE}
${PROTO_H_FILE}
${GRPC_CPP_FILE}
${GRPC_H_FILE}
COMMAND
${PROTOC_EXE}
ARGS
--proto_path=${PROTO_DIR}
--plugin=protoc-gen-grpc="${GRPC_CPP_PLUGIN}"
--cpp_out=${GRPC_OUT_DIR}
--grpc_out=${GRPC_OUT_DIR}
${PROTO_FILE}
DEPENDS
${PROTO_FILE}
COMMENT "Generating gPRC/Protobuf C++ code"
)
add_executable(bridge_qt6
${PROTO_CPP_FILE} ${PROTO_H_FILE} ${GRPC_CPP_FILE} ${GRPC_H_FILE}
AppController.cpp AppController.h
EventStreamWorker.cpp EventStreamWorker.h
Exception.cpp Exception.h
Log.cpp Log.h
main.cpp
Pch.h
QMLBackend.cpp QMLBackend.h
${DOCK_ICON_SRC_FILE} DockIcon/DockIcon.h
GRPC/GRPCClient.cpp GRPC/GRPCClient.h
GRPC/GRPCUtils.cpp GRPC/GRPCUtils.h
User/User.cpp User/User.h User/UserList.cpp User/UserList.h
Worker/Overseer.cpp Worker/Overseer.h
Worker/Worker.h)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
target_precompile_headers(bridge_qt6 PRIVATE Pch.h)
target_link_libraries(bridge_qt6
Qt5::Core
Qt5::Quick
Qt5::Qml
Qt5::QuickControls2
protobuf::libprotobuf
gRPC::grpc++
)
if (APPLE)
target_link_libraries(bridge_qt6 ${COCOA_LIBRARY})
endif()

View File

@ -0,0 +1,29 @@
// Copyright (c) 2022 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 "Pch.h"
#ifndef Q_OS_MACOS
void setDockIconVisibleState(bool visible) { Q_UNUSED(visible) }
bool getDockIconVisibleState() { return true; }
#endif

View File

@ -0,0 +1,27 @@
// Copyright (c) 2022 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_QT6_DOCK_ICON_H
#define BRIDGE_QT6_DOCK_ICON_H
void setDockIconVisibleState(bool visible); ///< Set the DOCK icon visibility state
bool getDockIconVisibleState(); ///< Get the Dock icon visibility state
#endif // #ifndef BRIDGE_QT6_DOCK_ICON_H

View File

@ -0,0 +1,49 @@
// Copyright (c) 2022 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 "Pch.h"
#include <Cocoa/Cocoa.h>
#include "DockIcon.h"
#ifdef Q_OS_MACOS
void setDockIconVisibleState(bool visible) {
if (visible) {
[NSApp setActivationPolicy: NSApplicationActivationPolicyRegular];
return;
} else {
[NSApp setActivationPolicy: NSApplicationActivationPolicyAccessory];
return;
}
}
bool getDockIconVisibleState() {
switch ([NSApp activationPolicy]) {
case NSApplicationActivationPolicyAccessory:
case NSApplicationActivationPolicyProhibited:
return false;
case NSApplicationActivationPolicyRegular:
return true;
}
}
#endif // #ifdef Q_OS_MACOS

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
// Copyright (c) 2022 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 "Pch.h"
#include "EventStreamWorker.h"
#include "GRPC/GRPCClient.h"
#include "Log.h"
#include "Exception.h"
//****************************************************************************************************************************************************
/// \param[in] parent The parent object.
//****************************************************************************************************************************************************
EventStreamReader::EventStreamReader(QObject *parent)
: Worker(parent)
{
connect(this, &EventStreamReader::started, [&]() { app().log().debug("EventStreamReader started");});
connect(this, &EventStreamReader::finished, [&]() { app().log().debug("EventStreamReader finished");});
connect(this, &EventStreamReader::error, &app().log(), &Log::error);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void EventStreamReader::run()
{
try
{
emit started();
grpc::Status const status = app().grpc().startEventStream();
if (!status.ok())
throw Exception(QString::fromStdString(status.error_message()));
emit finished();
}
catch (Exception const &e)
{
emit error(e.qwhat());
}
}

View File

@ -0,0 +1,52 @@
// Copyright (c) 2022 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_QT6_EVENT_STREAM_WORKER_H
#define BRIDGE_QT6_EVENT_STREAM_WORKER_H
#include "GRPC/bridge.grpc.pb.h"
#include "Worker/Worker.h"
//****************************************************************************************************************************************************
/// \brief Stream reader class.
//****************************************************************************************************************************************************
class EventStreamReader: public Worker
{
Q_OBJECT
public: // member functions
explicit EventStreamReader(QObject *parent); ///< Default constructor.
EventStreamReader(EventStreamReader const&) = delete; ///< Disabled copy-constructor.
EventStreamReader(EventStreamReader&&) = delete; ///< Disabled assignment copy-constructor.
~EventStreamReader() override = default; ///< Destructor.
EventStreamReader& operator=(EventStreamReader const&) = delete; ///< Disabled assignment operator.
EventStreamReader& operator=(EventStreamReader&&) = delete; ///< Disabled move assignment operator.
public slots:
void run() override; ///< Run the reader.
signals:
#pragma clang diagnostic push
#pragma ide diagnostic ignored "NotImplementedFunctions"
void eventReceived(QString eventString); ///< signal for events.
#pragma clang diagnostic pop
};
#endif //BRIDGE_QT6_EVENT_STREAM_WORKER_H

View File

@ -0,0 +1,68 @@
// Copyright (c) 2022 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 "Pch.h"
#include "Exception.h"
//****************************************************************************************************************************************************
/// \param[in] what A description of the exception
//****************************************************************************************************************************************************
Exception::Exception(QString what) noexcept
: std::exception()
, what_(std::move(what))
{
}
//****************************************************************************************************************************************************
/// \param[in] ref The Exception to copy from
//****************************************************************************************************************************************************
Exception::Exception(Exception const& ref) noexcept
: std::exception(ref)
, what_(ref.what_)
{
}
//****************************************************************************************************************************************************
/// \param[in] ref The Exception to copy from
//****************************************************************************************************************************************************
Exception::Exception(Exception&& ref) noexcept
: std::exception(ref)
, what_(ref.what_)
{
}
//****************************************************************************************************************************************************
/// \return a string describing the exception
//****************************************************************************************************************************************************
QString const& Exception::qwhat() const noexcept
{
return what_;
}
//****************************************************************************************************************************************************
/// \return A pointer to the description string of the exception.
//****************************************************************************************************************************************************
const char* Exception::what() const noexcept
{
return what_.toLocal8Bit().constData();
}

View File

@ -0,0 +1,46 @@
// Copyright (c) 2022 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_QT6_EXCEPTION_H
#define BRIDGE_QT6_EXCEPTION_H
#include <stdexcept>
//****************************************************************************************************************************************************
/// \brief Exception class.
//****************************************************************************************************************************************************
class Exception: public std::exception
{
public: // member functions
explicit Exception(QString what = QString()) noexcept; ///< Constructor
Exception(Exception const& ref) noexcept; ///< copy constructor
Exception(Exception&& ref) noexcept; ///< copy constructor
Exception& operator=(Exception const&) = delete; ///< Disabled assignment operator
Exception& operator=(Exception&&) = delete; ///< Disabled assignment operator
~Exception() noexcept override = default; ///< Destructor
QString const& qwhat() const noexcept; ///< Return the description of the exception as a QString
const char* what() const noexcept override; ///< Return the description of the exception as C style string
private: // data members
QString const what_; ///< The description of the exception
};
#endif //BRIDGE_QT6_EXCEPTION_H

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,214 @@
// Copyright (c) 2022 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_QT6_RPC_CLIENT_H
#define BRIDGE_QT6_RPC_CLIENT_H
#include "GRPC/bridge.grpc.pb.h"
#include "grpc++/grpc++.h"
#include "User/User.h"
typedef grpc::Status (grpc::Bridge::Stub::*SimpleMethod)(grpc::ClientContext*, const google::protobuf::Empty&, google::protobuf::Empty*);
typedef grpc::Status (grpc::Bridge::Stub::*BoolSetter)(grpc::ClientContext*, const google::protobuf::BoolValue&, google::protobuf::Empty*);
typedef grpc::Status (grpc::Bridge::Stub::*BoolGetter)(grpc::ClientContext*, const google::protobuf::Empty&, google::protobuf::BoolValue*);
typedef grpc::Status (grpc::Bridge::Stub::*Int32Setter)(grpc::ClientContext*, const google::protobuf::Int32Value&, google::protobuf::Empty*);
typedef grpc::Status (grpc::Bridge::Stub::*Int32Getter)(grpc::ClientContext*, const google::protobuf::Empty&, google::protobuf::Int32Value*);
typedef grpc::Status (grpc::Bridge::Stub::*StringGetter)(grpc::ClientContext*, const google::protobuf::Empty&, google::protobuf::StringValue*);
typedef grpc::Status (grpc::Bridge::Stub::*StringSetter)(grpc::ClientContext*, const google::protobuf::StringValue&, google::protobuf::Empty*);
typedef grpc::Status (grpc::Bridge::Stub::*StringParamMethod)(grpc::ClientContext*, const google::protobuf::StringValue&, google::protobuf::Empty*);
//****************************************************************************************************************************************************
/// \brief gRPC client class. This class encapsulate the gRPC service, abstracting all data type conversions.
//****************************************************************************************************************************************************
class GRPCClient: public QObject
{
Q_OBJECT
public: // member functions.
GRPCClient() = default; ///< Default constructor.
GRPCClient(GRPCClient const&) = delete; ///< Disabled copy-constructor.
GRPCClient(GRPCClient&&) = delete; ///< Disabled assignment copy-constructor.
~GRPCClient() override = default; ///< Destructor.
GRPCClient& operator=(GRPCClient const&) = delete; ///< Disabled assignment operator.
GRPCClient& operator=(GRPCClient&&) = delete; ///< Disabled move assignment operator.
bool connectToServer(QString &outError); ///< Establish connection to the gRPC server.
grpc::Status guiReady(); ///< performs the "GuiReady" gRPC call.
grpc::Status isFirstGUIStart(bool &outIsFirst); ///< performs the "IsFirstGUIStart" gRPC call.
grpc::Status isAutostartOn(bool &outIsOn); ///< Performs the "isAutostartOn" gRPC call.
grpc::Status setIsAutostartOn(bool on); ///< Performs the "setIsAutostartOn" gRPC call.
grpc::Status isBetaEnabled(bool &outEnabled); ///< Performs the "isBetaEnabled" gRPC call.
grpc::Status setisBetaEnabled(bool enabled); ///< Performs the 'setIsBetaEnabled' gRPC call.
grpc::Status colorSchemeName(QString &outName); ///< Performs the "colorSchemeName' gRPC call.
grpc::Status setColorSchemeName(QString const &name); ///< Performs the "setColorSchemeName' gRPC call.
grpc::Status currentEmailClient(QString &outName); ///< Performs the 'currentEmailClient' gRPC call.
grpc::Status quit(); ///< Perform the "Quit" gRPC call.
grpc::Status restart(); ///< Performs the Restart gRPC call.
grpc::Status isPortFree(qint32 port, bool &outFree); ///< Performs the 'IsPortFree' call.
grpc::Status showOnStartup(bool &outValue); ///< Performs the 'ShowOnStartup' call.
grpc::Status showSplashScreen(bool &outValue); ///< Performs the 'ShowSplashScreen' call.
grpc::Status goos(QString &outGoos); ///< Performs the 'GoOs' call.
grpc::Status logsPath(QUrl &outPath); ///< Performs the 'LogsPath' call.
grpc::Status licensePath(QUrl &outPath); ///< Performs the 'LicensePath' call.
grpc::Status dependencyLicensesLink(QUrl &outUrl); ///< Performs the 'DependencyLicensesLink' call.
grpc::Status version(QString &outVersion); ///< Performs the 'Version' call.
grpc::Status hostname(QString &outHostname); ///< Performs the 'Hostname' call.
signals: // app related signals
void internetStatus(bool isOn);
void toggleAutostartFinished();
void resetFinished();
void reportBugFinished();
void reportBugSuccess();
void reportBugError();
void showMainWindow();
// cache related calls
public:
grpc::Status isCacheOnDiskEnabled(bool &outEnabled); ///< Performs the 'isCacheOnDiskEnabled' call.
grpc::Status diskCachePath(QUrl &outPath); ///< Performs the 'diskCachePath' call.
grpc::Status changeLocalCache(bool enabled, QUrl const &path); ///< Performs the 'ChangeLocalCache' call.
signals:
void isCacheOnDiskEnabledChanged(bool enabled);
void diskCachePathChanged(QUrl const&outPath);
void cacheUnavailable(); // _ func() `signal:"cacheUnavailable"`
void cacheCantMove(); // _ func() `signal:"cacheCantMove"`
void cacheLocationChangeSuccess(); // _ func() `signal:"cacheLocationChangeSuccess"`
void diskFull(); // _ func() `signal:"diskFull"`
void changeLocalCacheFinished(); // _ func() `signal:"changeLocalCacheFinished"`
// mail settings related calls
public:
grpc::Status useSSLForSMTP(bool &outUseSSL); ///< Performs the 'useSSLForSMTP' gRPC call
grpc::Status setUseSSLForSMTP(bool useSSL); ///< Performs the 'currentEmailClient' gRPC call.
grpc::Status portIMAP(int &outPort); ///< Performs the 'portImap' gRPC call.
grpc::Status portSMTP(int &outPort); ///< Performs the 'portImap' gRPC call.
grpc::Status changePorts(int portIMAP, int portSMTP); ///< Performs the 'changePorts' gRPC call.
grpc::Status isDoHEnabled(bool &outEnabled); ///< Performs the 'isDoHEnabled' gRPC call.
grpc::Status setIsDoHEnabled(bool enabled); ///< Performs the 'setIsDoHEnabled' gRPC call.
signals:
void portIssueIMAP();
void portIssueSMTP();
void toggleUseSSLFinished();
void changePortFinished();
public: // login related calls
grpc::Status login(QString const &username, QString const& password); ///< Performs the 'login' call.
grpc::Status login2FA(QString const &username, QString const& code); ///< Performs the 'login2FA' call.
grpc::Status login2Passwords(QString const &username, QString const& password); ///< Performs the 'login2Passwords' call.
grpc::Status loginAbort(QString const &username); ///< Performs the 'loginAbort' call.
signals:
void loginUsernamePasswordError(QString const &errMsg); // _ func(errorMsg string) `signal:"loginUsernamePasswordError"`
void loginFreeUserError(); // _ func() `signal:"loginFreeUserError"`
void loginConnectionError(QString const &errMsg); // _ func(errorMsg string) `signal:"loginConnectionError"`
void login2FARequested(QString const &userName); // _ func(username string) `signal:"login2FARequested"`
void login2FAError(QString const& errMsg); // _ func(errorMsg string) `signal:"login2FAError"`
void login2FAErrorAbort(QString const& errMsg); // _ func(errorMsg string) `signal:"login2FAErrorAbort"`
void login2PasswordRequested(); // _ func() `signal:"login2PasswordRequested"`
void login2PasswordError(QString const& errMsg); // _ func(errorMsg string) `signal:"login2PasswordError"`
void login2PasswordErrorAbort(QString const& errMsg); // _ func(errorMsg string) `signal:"login2PasswordErrorAbort"`
void loginFinished(QString const &userID); // _ func(index int) `signal:"loginFinished"`
void loginAlreadyLoggedIn(QString const &userID); // _ func(index int) `signal:"loginAlreadyLoggedIn"`
public: // Update related calls
grpc::Status checkUpdate();
grpc::Status installUpdate();
grpc::Status setIsAutomaticUpdateOn(bool on);
grpc::Status isAutomaticUpdateOn(bool &isOn);
signals:
void updateManualError();
void updateForceError();
void updateSilentError();
void updateManualReady(QString const &version);
void updateManualRestartNeeded();
void updateForce(QString const &version);
void updateSilentRestartNeeded();
void updateIsLatestVersion();
void checkUpdatesFinished();
public: // user related calls
grpc::Status getUserList(QList<SPUser>& outUsers);
grpc::Status getUser(QString const &userID, SPUser& outUser);
grpc::Status logoutUser(QString const &userID); ///< Performs the 'logoutUser' call.
grpc::Status removeUser(QString const &userID); ///< Performs the 'removeUser' call.
grpc::Status configureAppleMail(QString const& userID, QString const &address); ///< Performs the 'configureAppleMail' call.
grpc::Status setUserSplitMode(QString const& userID, bool active); ///< Performs the 'SetUserSplitMode' call.
signals:
void toggleSplitModeFinished(QString const& userID);
void userDisconnected(QString const& username);
void userChanged(QString const& userID);
public: // keychain related calls
grpc::Status availableKeychains(QStringList &outKeychains);
grpc::Status currentKeychain(QString &outKeychain);
grpc::Status setCurrentKeychain(QString const &keychain);
signals:
void changeKeychainFinished();
void hasNoKeychain();
void rebuildKeychain();
signals: // mail releated events
void noActiveKeyForRecipient(QString const &email); // _ func(email string) `signal:noActiveKeyForRecipient`
void addressChanged(QString const &address); // _ func(address string) `signal:addressChanged`
void addressChangedLogout(QString const &address); // _ func(address string) `signal:addressChangedLogout`
void apiCertIssue();
public:
grpc::Status startEventStream(); ///< Retrieve and signal the events in the event stream.
grpc::Status stopEventStream(); ///< Stop the event stream.
private:
grpc::Status simpleMethod(SimpleMethod method); ///< perform a gRPC call to a bool setter.
grpc::Status setBool(BoolSetter setter, bool value); ///< perform a gRPC call to a bool setter.
grpc::Status getBool(BoolGetter getter, bool& outValue); ///< perform a gRPC call to a bool getter.
grpc::Status setInt32(Int32Setter setter, int value); ///< perform a gRPC call to an int setter.
grpc::Status getInt32(Int32Getter getter, int& outValue); ///< perform a gRPC call to an int getter.
grpc::Status setString(StringSetter getter, QString const& value); ///< Perform a gRPC call to a string setter.
grpc::Status getString(StringGetter getter, QString& outValue); ///< Perform a gRPC call to a string getter.
grpc::Status getURLForLocalFile(StringGetter getter, QUrl& outValue); ///< Perform a gRPC call to a string getter, with resulted converted to QUrl for a local file path.
grpc::Status getURL(StringGetter getter, QUrl& outValue); ///< Perform a gRPC call to a string getter, with resulted converted to QUrl.
grpc::Status methodWithStringParam(StringParamMethod method, QString const& str); ///< Perfom a gRPC call that takes a string as a parameter and returns an Empty.
void processAppEvent(grpc::AppEvent const &event); ///< Process an 'App' event.
void processLoginEvent(grpc::LoginEvent const &event); ///< Process a 'Login' event.
void processUpdateEvent(grpc::UpdateEvent const &event); ///< Process an 'Update' event.
void processCacheEvent(grpc::CacheEvent const &event); ///< Process a 'Cache' event.
void processMailSettingsEvent(grpc::MailSettingsEvent const &event); ///< Process a 'MailSettings' event.
void processKeychainEvent(grpc::KeychainEvent const &event); ///< Process a 'Keychain' event.
void processMailEvent(grpc::MailEvent const &event); ///< Process a 'Mail' event.
void processUserEvent(grpc::UserEvent const &event); ///< Process a 'User' event.
private: // data members.
std::shared_ptr<grpc::Channel> channel_ { nullptr }; ///< The gRPC channel.
std::shared_ptr<grpc::Bridge::Stub> stub_ { nullptr }; ///< The gRPC stub (a.k.a. client).
};
#endif // BRIDGE_QT6_RPC_CLIENT_H

View File

@ -0,0 +1,63 @@
// Copyright (c) 2022 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 "GRPCUtils.h"
#include "QMLBackend.h"
//****************************************************************************************************************************************************
/// \param[in] status The status
/// \param[in] callName The call name.
//****************************************************************************************************************************************************
void logGRPCCallStatus(grpc::Status const& status, QString const &callName)
{
if (status.ok())
app().log().debug(QString("%1()").arg(callName));
else
app().log().error(QString("%1() FAILED").arg(callName));
}
//****************************************************************************************************************************************************
/// \param[in] grpcUser the gRPC user struct
/// \return a user.
//****************************************************************************************************************************************************
SPUser parsegrpcUser(grpc::User const &grpcUser)
{
// As we want to use shared pointers here, we do not want to use the Qt ownership system, so we set parent to nil.
// But: From https://doc.qt.io/qt-5/qtqml-cppintegration-data.html:
// " When data is transferred from C++ to QML, the ownership of the data always remains with C++. The exception to this rule
// is when a QObject is returned from an explicit C++ method call: in this case, the QML engine assumes ownership of the object. "
// This is the case here, so we explicitely indicate that the object is owned by C++.
SPUser user = std::make_shared<User>(nullptr);
QQmlEngine::setObjectOwnership(user.get(), QQmlEngine::CppOwnership);
user->setProperty("username", QString::fromStdString(grpcUser.username()));
user->setProperty("avatarText", QString::fromStdString(grpcUser.avatartext()));
user->setProperty("loggedIn", grpcUser.loggedin());
user->setProperty("splitMode", grpcUser.splitmode());
user->setProperty("setupGuideSeen", grpcUser.setupguideseen());
user->setProperty("usedBytes", float(grpcUser.usedbytes()));
user->setProperty("totalBytes", float(grpcUser.totalbytes()));
user->setProperty("password", QString::fromStdString(grpcUser.password()));
QStringList addresses;
for (int j = 0; j < grpcUser.addresses_size(); ++j)
addresses.append(QString::fromStdString(grpcUser.addresses(j)));
user->setProperty("addresses", addresses);
user->setProperty("id", QString::fromStdString(grpcUser.id()));
return user;
}

View File

@ -0,0 +1,30 @@
// Copyright (c) 2022 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_QT6_GRPCUTILS_H
#define BRIDGE_QT6_GRPCUTILS_H
#include "GRPC/bridge.grpc.pb.h"
#include "grpc++/grpc++.h"
#include "User/User.h"
void logGRPCCallStatus(grpc::Status const& status, QString const &callName); ///< Log the status of a gRPC code.
SPUser parsegrpcUser(grpc::User const& grpcUser); ///< Parse a gRPC user struct and return a User.
#endif // BRIDGE_QT6_GRPCUTILS_H

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,159 @@
// Copyright (c) 2022 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 "Pch.h"
#include "Log.h"
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
Log::Log()
: QObject()
, stdout_(stdout)
, stderr_(stderr)
{
}
//****************************************************************************************************************************************************
/// \param[in] level The log level.
//****************************************************************************************************************************************************
void Log::setLevel(Log::Level level)
{
QMutexLocker locker(&mutex_);
level_ = level;
}
//****************************************************************************************************************************************************
/// \return The log level.
//****************************************************************************************************************************************************
Log::Level Log::level() const
{
QMutexLocker locker(&mutex_);
return Log::Level::Debug;
}
//****************************************************************************************************************************************************
/// \param[in] value Should the log entries be sent to STDOUT/STDERR.
//****************************************************************************************************************************************************
void Log::setEchoInConsole(bool value)
{
QMutexLocker locker(&mutex_);
echoInConsole_ = value;
}
//****************************************************************************************************************************************************
/// \return true iff the log entries be should sent to STDOUT/STDERR.
//****************************************************************************************************************************************************
bool Log::echoInConsole() const
{
QMutexLocker locker(&mutex_);
return echoInConsole_;
}
//****************************************************************************************************************************************************
/// \param[in] message The message
//****************************************************************************************************************************************************
void Log::debug(QString const &message)
{
QMutexLocker locker(&mutex_);
if (qint32(level_) > qint32(Level::Debug))
return;
emit debugEntryAdded(message);
QString const withPrefix = "[DEBUG] " + message;
emit entryAdded(withPrefix);
if (echoInConsole_)
{
stdout_ << withPrefix << "\n";
stdout_.flush();
}
}
//****************************************************************************************************************************************************
/// \param[in] message The message
//****************************************************************************************************************************************************
void Log::info(QString const &message)
{
QMutexLocker locker(&mutex_);
if (qint32(level_) > qint32(Level::Info))
return;
emit infoEntryAdded(message);
QString const withPrefix = "[INFO] " + message;
emit entryAdded(withPrefix);
if (echoInConsole_)
{
stdout_ << withPrefix << "\n";
stdout_.flush();
}
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void Log::warn(QString const &message)
{
QMutexLocker locker(&mutex_);
if (qint32(level_) > qint32(Level::Warn))
return;
emit infoEntryAdded(message);
QString const withPrefix = "[WARNING] " + message;
emit entryAdded(withPrefix);
if (echoInConsole_)
{
stdout_ << withPrefix << "\n";
stdout_.flush();
}
}
//****************************************************************************************************************************************************
/// message
//****************************************************************************************************************************************************
void Log::error(QString const &message)
{
QMutexLocker locker(&mutex_);
if (qint32(level_) > qint32(Level::Error))
return;
emit infoEntryAdded(message);
QString const withPrefix = "[ERROR] " + message;
emit entryAdded(withPrefix);
if (echoInConsole_)
{
stderr_ << withPrefix << "\n";
stderr_.flush();
}
}

View File

@ -0,0 +1,74 @@
// Copyright (c) 2022 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_QT6_LOG_H
#define BRIDGE_QT6_LOG_H
//****************************************************************************************************************************************************
/// \brief Basic log class. No logging to file. Four levels. Rebroadcast received log entries via Qt signals.
//****************************************************************************************************************************************************
class Log : public QObject
{
Q_OBJECT
public: // data types.
enum class Level
{
Debug = 0, ///< Debug
Info = 1, ///< Info
Warn = 2, ///< Warn
Error = 3, ///< Error
}; ///< Log level class
public: // member functions.
Log(); ///< Default constructor.
Log(Log const &) = delete; ///< Disabled copy-constructor.
Log(Log &&) = delete; ///< Disabled assignment copy-constructor.
~Log() override = default; ///< Destructor.
Log &operator=(Log const &) = delete; ///< Disabled assignment operator.
Log &operator=(Log &&) = delete; ///< Disabled move assignment operator.
void setLevel(Level level); ///< Set the log level.
Level level() const; ///< Get the log level.
void setEchoInConsole(bool value); ///< Set if the log entries should be echoed in STDOUT/STDERR.
bool echoInConsole() const; ///< Check if the log entries should be echoed in STDOUT/STDERR.
public slots:
void debug(QString const &message); ///< Adds a debug entry in the log.
void info(QString const &message); ///< Adds an info entry to the log.
void warn(QString const &message); ///< Adds a warning entry to the log.
void error(QString const &message); ///< Adds an error entry to the log.
signals:
void debugEntryAdded(QString const &); ///< Signal for debug entries.
void infoEntryAdded(QString const &message); ///< Signal for info entries.
void warnEntryAdded(QString const &message); ///< Signal for warning entries.
void errorEntryAdded(QString const &message); ///< Signal for error entries.
void entryAdded(QString const &message); ///< Signal emitted when any type of entry is added.
private: // data members
mutable QMutex mutex_; ///< The mutex.
Level level_{Level::Debug}; ///< The log level
bool echoInConsole_{false}; ///< Set if the log messages should be sent to STDOUT/STDERR.
QTextStream stdout_; ///< The stdout stream.
QTextStream stderr_; ///< The stderr stream.
};
#endif //BRIDGE_QT6_LOG_H

View File

@ -0,0 +1,23 @@
// Copyright (c) 2022 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 <QtCore>
#include <QtQuick>
#include <QtQml>
#include <QtQuickControls2>
#include <AppController.h>

View File

@ -0,0 +1,316 @@
// Copyright (c) 2022 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 "Pch.h"
#include "QMLBackend.h"
#include "Exception.h"
#include "GRPC/GRPCClient.h"
#include "Worker/Overseer.h"
#include "EventStreamWorker.h"
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
QMLBackend::QMLBackend()
: QObject()
, users_(new UserList(this))
{
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::init()
{
this->connectGrpcEvents();
QString error;
if (app().grpc().connectToServer(error))
app().log().info("Connected to backend via gRPC service.");
else
throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error));
eventStreamOverseer_ = std::make_unique<Overseer>(new EventStreamReader(nullptr), nullptr);
eventStreamOverseer_->startWorker(true);
this->retrieveUserList();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::connectGrpcEvents()
{
GRPCClient *client = &app().grpc();
// app events
connect(client, &GRPCClient::internetStatus, this, [&](bool isOn) { if (isOn) emit internetOn(); else emit internetOff(); });
connect(client, &GRPCClient::toggleAutostartFinished, this, &QMLBackend::toggleAutostartFinished);
connect(client, &GRPCClient::resetFinished, this, &QMLBackend::resetFinished);
connect(client, &GRPCClient::reportBugFinished, this, &QMLBackend::reportBugFinished);
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError);
// cache events
connect(client, &GRPCClient::isCacheOnDiskEnabledChanged, this, &QMLBackend::isDiskCacheEnabledChanged);
connect(client, &GRPCClient::diskCachePathChanged, this, &QMLBackend::diskCachePathChanged);
connect(client, &GRPCClient::cacheUnavailable, this, &QMLBackend::cacheUnavailable); // _ func() `signal:"cacheUnavailable"`
connect(client, &GRPCClient::cacheCantMove, this, &QMLBackend::cacheCantMove);
connect(client, &GRPCClient::diskFull, this, &QMLBackend::diskFull);
connect(client, &GRPCClient::cacheLocationChangeSuccess, this, &QMLBackend::cacheLocationChangeSuccess);
connect(client, &GRPCClient::changeLocalCacheFinished, this, &QMLBackend::changeLocalCacheFinished);
// login events
connect(client, &GRPCClient::loginUsernamePasswordError, this, &QMLBackend::loginUsernamePasswordError);
connect(client, &GRPCClient::loginFreeUserError, this, &QMLBackend::loginFreeUserError);
connect(client, &GRPCClient::loginConnectionError, this, &QMLBackend::loginConnectionError);
connect(client, &GRPCClient::login2FARequested, this, &QMLBackend::login2FARequested);
connect(client, &GRPCClient::login2FAError, this, &QMLBackend::login2FAError);
connect(client, &GRPCClient::login2FAErrorAbort, this, &QMLBackend::login2FAErrorAbort);
connect(client, &GRPCClient::login2PasswordRequested, this, &QMLBackend::login2PasswordRequested);
connect(client, &GRPCClient::login2PasswordError, this, &QMLBackend::login2PasswordError);
connect(client, &GRPCClient::login2PasswordErrorAbort, this, &QMLBackend::login2PasswordErrorAbort);
connect(client, &GRPCClient::loginFinished, this, [&](QString const &userID) {
qint32 const index = users_->rowOfUserID(userID); emit loginFinished(index); });
connect(client, &GRPCClient::loginAlreadyLoggedIn, this, [&](QString const &userID) {
qint32 const index = users_->rowOfUserID(userID); emit loginAlreadyLoggedIn(index); });
// mail settings events
connect(client, &GRPCClient::portIssueIMAP, this, &QMLBackend::portIssueIMAP);
connect(client, &GRPCClient::portIssueSMTP, this, &QMLBackend::portIssueSMTP);
connect(client, &GRPCClient::toggleUseSSLFinished, this, &QMLBackend::toggleUseSSLFinished);
connect(client, &GRPCClient::changePortFinished, this, &QMLBackend::changePortFinished);
// keychain events
connect(client, &GRPCClient::changeKeychainFinished, this, &QMLBackend::changeKeychainFinished);
connect(client, &GRPCClient::hasNoKeychain, this, &QMLBackend::notifyHasNoKeychain);
connect(client, &GRPCClient::rebuildKeychain, this, &QMLBackend::notifyRebuildKeychain);
// mail events
connect(client, &GRPCClient::noActiveKeyForRecipient, this, &QMLBackend::noActiveKeyForRecipient);
connect(client, &GRPCClient::addressChanged, this, &QMLBackend::addressChanged);
connect(client, &GRPCClient::addressChangedLogout, this, &QMLBackend::addressChangedLogout);
connect(client, &GRPCClient::apiCertIssue, this, &QMLBackend::apiCertIssue);
// user events
connect(client, &GRPCClient::userDisconnected, this, &QMLBackend::userDisconnected);
users_->connectGRPCEvents();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::retrieveUserList()
{
QList<SPUser> users;
logGRPCCallStatus(app().grpc().getUserList(users), "getUserList");
users_->reset(users);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::clearUserList()
{
users_->reset();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
QPoint QMLBackend::getCursorPos()
{
return QCursor::pos();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
bool QMLBackend::isPortFree(int port)
{
bool isFree = false;
logGRPCCallStatus(app().grpc().isPortFree(port, isFree), "isPortFree");
return isFree;
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::guiReady()
{
logGRPCCallStatus(app().grpc().guiReady(), "guiReady");
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::quit()
{
logGRPCCallStatus(app().grpc().quit(), "quit");
qApp->exit(0);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::restart()
{
logGRPCCallStatus(app().grpc().restart(), "restart");
app().log().error("RESTART is not implemented"); /// \todo GODT-1671 implement restart.
}
//****************************************************************************************************************************************************
/// \param[in] active Should we activate autostart.
//****************************************************************************************************************************************************
void QMLBackend::toggleAutostart(bool active)
{
logGRPCCallStatus(app().grpc().setIsAutostartOn(active), "setIsAutostartOn");
emit isAutostartOnChanged(this->isAutostartOn());
}
//****************************************************************************************************************************************************
/// \param[in] active The new state for the beta enabled property.
//****************************************************************************************************************************************************
void QMLBackend::toggleBeta(bool active)
{
logGRPCCallStatus(app().grpc().setisBetaEnabled(active), "setIsBetaEnabled");
emit isBetaEnabledChanged(this->isBetaEnabled());
}
//****************************************************************************************************************************************************
/// \param[in] scheme the scheme name
//****************************************************************************************************************************************************
void QMLBackend::changeColorScheme(QString const &scheme)
{
logGRPCCallStatus(app().grpc().setColorSchemeName(scheme), "setIsBetaEnabled");
emit colorSchemeNameChanged(this->colorSchemeName());
}
//****************************************************************************************************************************************************
/// \param[in] makeItActive Should SSL for SMTP be enabled.
//****************************************************************************************************************************************************
void QMLBackend::toggleUseSSLforSMTP(bool makeItActive)
{
grpc::Status status = app().grpc().setUseSSLForSMTP(makeItActive);
logGRPCCallStatus(status, "setUseSSLForSMTP");
if (status.ok())
emit useSSLforSMTPChanged(makeItActive);
}
//****************************************************************************************************************************************************
/// \param[in] imapPort The IMAP port.
/// \param[in] smtpPort The SMTP port.
//****************************************************************************************************************************************************
void QMLBackend::changePorts(int imapPort, int smtpPort)
{
grpc::Status status = app().grpc().changePorts(imapPort, smtpPort);
logGRPCCallStatus(status, "changePorts");
if (status.ok())
{
emit portIMAPChanged(imapPort);
emit portSMTPChanged(smtpPort);
}
}
//****************************************************************************************************************************************************
/// \param[in] active Should DoH be active.
//****************************************************************************************************************************************************
void QMLBackend::toggleDoH(bool active)
{
grpc::Status status = app().grpc().setIsDoHEnabled(active);
logGRPCCallStatus(status, "toggleDoH");
if (status.ok())
emit isDoHEnabledChanged(active);
}
//****************************************************************************************************************************************************
/// \param[in] keychain The new keychain.
//****************************************************************************************************************************************************
void QMLBackend::changeKeychain(QString const &keychain)
{
grpc::Status status = app().grpc().setCurrentKeychain(keychain);
logGRPCCallStatus(status, "setCurrentKeychain");
if (status.ok())
emit currentKeychainChanged(keychain);
}
//****************************************************************************************************************************************************
/// \param[in] active Should automatic update be turned on.
//****************************************************************************************************************************************************
void QMLBackend::toggleAutomaticUpdate(bool active)
{
grpc::Status status = app().grpc().setIsAutomaticUpdateOn(active);
logGRPCCallStatus(status, "toggleAutomaticUpdate");
if (status.ok())
emit isAutomaticUpdateOnChanged(active);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::checkUpdates()
{
app().log().error(QString("%1() is not implemented.").arg(__FUNCTION__));
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::installUpdate()
{
app().log().error(QString("%1() is not implemented.").arg(__FUNCTION__));
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::triggerReset()
{
app().log().error(QString("%1() is not implemented.").arg(__FUNCTION__));
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::reportBug(QString const &description, QString const &address, QString const &emailClient,
bool includeLogs)
{
Q_UNUSED(includeLogs)
app().log().error(QString("%1() is not implemented.").arg(__FUNCTION__));
}

View File

@ -0,0 +1,222 @@
// Copyright (c) 2022 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_QT6_QMLBACKEND_H
#define BRIDGE_QT6_QMLBACKEND_H
#include <grpcpp/support/status.h>
#include "DockIcon/DockIcon.h"
#include "GRPC/GRPCClient.h"
#include "GRPC/GRPCUtils.h"
#include "Worker/Overseer.h"
#include "User/UserList.h"
//****************************************************************************************************************************************************
/// \brief Bridge C++ backend class.
//****************************************************************************************************************************************************
class QMLBackend: public QObject
{
Q_OBJECT
public: // member functions.
QMLBackend(); ///< Default constructor.
QMLBackend(QMLBackend const &) = delete; ///< Disabled copy-constructor.
QMLBackend(QMLBackend &&) = delete; ///< Disabled assignment copy-constructor.
~QMLBackend() override = default; ///< Destructor.
QMLBackend &operator=(QMLBackend const &) = delete; ///< Disabled assignment operator.
QMLBackend &operator=(QMLBackend &&) = delete; ///< Disabled move assignment operator.
void init(); ///< Initialize the backend.
void clearUserList(); ///< Clear the user list.
// invokable methods can be called from QML. They generally return a value, which slots cannot do.
Q_INVOKABLE static QPoint getCursorPos(); // _ func() *core.QPoint `slot:"getCursorPos"`
Q_INVOKABLE static bool isPortFree(int port); // _ func(port int) bool `slot:"isPortFree"`
public: // Qt/QML properties. Note that the NOTIFY-er signal is required even for read-only properties (QML warning otherwise)
Q_PROPERTY(bool showOnStartup READ showOnStartup NOTIFY showOnStartupChanged) // _ bool `property:showOnStartup`
Q_PROPERTY(bool showSplashScreen READ showSplashScreen NOTIFY showSplashScreenChanged) // _ bool `property:showSplashScreen`
Q_PROPERTY(QString goos READ goos NOTIFY goosChanged) // _ string `property:"goos"`
Q_PROPERTY(QUrl logsPath READ logsPath NOTIFY logsPathChanged) // _ core.QUrl `property:"logsPath"`
Q_PROPERTY(QUrl licensePath READ licensePath NOTIFY licensePathChanged) // _ core.QUrl `property:"licensePath"`
Q_PROPERTY(QUrl releaseNotesLink READ releaseNotesLink WRITE setReleaseNotesLink NOTIFY releaseNotesLinkChanged) // _ core.QUrl `property:"releaseNotesLink"`
Q_PROPERTY(QUrl dependencyLicensesLink READ dependencyLicensesLink NOTIFY dependencyLicensesLinkChanged) // _ core.QUrl `property:"dependencyLicensesLink"`
Q_PROPERTY(QUrl landingPageLink READ landingPageLink WRITE setLandingPageLink NOTIFY landingPageLinkChanged) // _ core.QUrl `property:"landingPageLink"`
Q_PROPERTY(QString version READ version NOTIFY versionChanged) // _ string `property:"version"`
Q_PROPERTY(QString hostname READ hostname NOTIFY hostnameChanged) // _ string `property:"hostname"`
Q_PROPERTY(bool isAutostartOn READ isAutostartOn NOTIFY isAutostartOnChanged) // _ bool `property:"isAutostartOn"`
Q_PROPERTY(bool isBetaEnabled READ isBetaEnabled NOTIFY isBetaEnabledChanged) // _ bool `property:"isBetaEnabled"`
Q_PROPERTY(QString colorSchemeName READ colorSchemeName NOTIFY colorSchemeNameChanged) // _ string `property:"colorSchemeName"`
Q_PROPERTY(bool isDiskCacheEnabled READ isDiskCacheEnabled NOTIFY isDiskCacheEnabledChanged) // _ bool `property:"isDiskCacheEnabled"`
Q_PROPERTY(QUrl diskCachePath READ diskCachePath NOTIFY diskCachePathChanged) // _ core.QUrl `property:"diskCachePath"`
Q_PROPERTY(bool useSSLforSMTP READ useSSLForSMTP NOTIFY useSSLforSMTPChanged) // _ bool `property:"useSSLforSMTP"`
Q_PROPERTY(int portIMAP READ portIMAP NOTIFY portIMAPChanged) // _ int `property:"portIMAP"`
Q_PROPERTY(int portSMTP READ portSMTP NOTIFY portSMTPChanged) // _ int `property:"portSMTP"`
Q_PROPERTY(bool isDoHEnabled READ isDoHEnabled NOTIFY isDoHEnabledChanged) // _ bool `property:"isDoHEnabled"`
Q_PROPERTY(bool isFirstGUIStart READ isFirstGUIStart) // _ bool `property:"isFirstGUIStart"`
Q_PROPERTY(bool isAutomaticUpdateOn READ isAutomaticUpdateOn NOTIFY isAutomaticUpdateOnChanged) // _ bool `property:"isAutomaticUpdateOn"`
Q_PROPERTY(QString currentEmailClient READ currentEmailClient NOTIFY currentEmailClientChanged) // _ string `property:"currentEmailClient"`
Q_PROPERTY(QStringList availableKeychain READ availableKeychain NOTIFY availableKeychainChanged) // _ []string `property:"availableKeychain"`
Q_PROPERTY(QString currentKeychain READ currentKeychain NOTIFY currentKeychainChanged) // _ string `property:"currentKeychain"`
Q_PROPERTY(UserList* users MEMBER users_ NOTIFY usersChanged)
Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged) // _ bool `property:dockIconVisible`
// Qt Property system setters & getters.
bool showOnStartup() const { bool v = false; logGRPCCallStatus(app().grpc().showOnStartup(v), "showOnStartup"); return v; };
bool showSplashScreen() { bool show = false; logGRPCCallStatus(app().grpc().showSplashScreen(show), "showSplashScreen"); return show; }
QString goos() { QString goos; logGRPCCallStatus(app().grpc().goos(goos), "goos"); return goos; }
QUrl logsPath() const { QUrl path; logGRPCCallStatus(app().grpc().logsPath(path), "logsPath"); return path;}
QUrl licensePath() const { QUrl path; logGRPCCallStatus(app().grpc().licensePath(path), "licensePath"); return path; }
QUrl releaseNotesLink() const { return releaseNotesLink_; }
void setReleaseNotesLink(QUrl const& url) { if (url != releaseNotesLink_) { releaseNotesLink_ = url; emit releaseNotesLinkChanged(url); } }
QUrl dependencyLicensesLink() const { QUrl link; logGRPCCallStatus(app().grpc().dependencyLicensesLink(link), "dependencyLicensesLink"); return link; }
QUrl landingPageLink() const { return landingPageLink_; }
void setLandingPageLink(QUrl const& url) { if (url != landingPageLink_) { landingPageLink_ = url; emit landingPageLinkChanged(url); } }
QString version() const { QString version; logGRPCCallStatus(app().grpc().version(version), "version"); return version; }
QString hostname() const { QString hostname; logGRPCCallStatus(app().grpc().hostname(hostname), "hostname"); return hostname; }
bool isAutostartOn() const { bool v; logGRPCCallStatus(app().grpc().isAutostartOn(v), "isAutostartOn"); return v; };
bool isBetaEnabled() const { bool v; logGRPCCallStatus(app().grpc().isBetaEnabled(v), "isBetaEnabled"); return v; }
QString colorSchemeName() const { QString name; logGRPCCallStatus(app().grpc().colorSchemeName(name), "colorSchemeName"); return name; }
bool isDiskCacheEnabled() const { bool enabled; logGRPCCallStatus(app().grpc().isCacheOnDiskEnabled(enabled), "isCacheOnDiskEnabled"); return enabled;}
QUrl diskCachePath() const { QUrl path; logGRPCCallStatus(app().grpc().diskCachePath(path), "diskCachePath"); return path; }
bool useSSLForSMTP() const{ bool useSSL; logGRPCCallStatus(app().grpc().useSSLForSMTP(useSSL), "useSSLForSMTP"); return useSSL; }
int portIMAP() const { int port; logGRPCCallStatus(app().grpc().portIMAP(port), "portIMAP"); return port; }
int portSMTP() const { int port; logGRPCCallStatus(app().grpc().portSMTP(port), "portSMTP"); return port; }
bool isDoHEnabled() const { bool isEnabled; logGRPCCallStatus(app().grpc().isDoHEnabled(isEnabled), "isDoHEnabled"); return isEnabled;}
bool isFirstGUIStart() const { bool v; logGRPCCallStatus(app().grpc().isFirstGUIStart(v), "isFirstGUIStart"); return v; };
bool isAutomaticUpdateOn() const { bool isOn = false; logGRPCCallStatus(app().grpc().isAutomaticUpdateOn(isOn), "isAutomaticUpdateOn"); return isOn; }
QString currentEmailClient() { QString client; logGRPCCallStatus(app().grpc().currentEmailClient(client), "currentEmailClient"); return client;}
QStringList availableKeychain() const { QStringList keychains; logGRPCCallStatus(app().grpc().availableKeychains(keychains), "availableKeychain"); return keychains; }
QString currentKeychain() const { QString keychain; logGRPCCallStatus(app().grpc().currentKeychain(keychain), "currentKeychain"); return keychain; }
bool dockIconVisible() const { return getDockIconVisibleState(); };
void setDockIconVisible(bool visible) { setDockIconVisibleState(visible); emit dockIconVisibleChanged(visible); }
signals: // Signal used by the Qt property system. Many of them are unused but required to avoir warning from the QML engine.
void showSplashScreenChanged(bool value);
void showOnStartupChanged(bool value);
void goosChanged(QString const &value);
void isDiskCacheEnabledChanged(bool value);
void diskCachePathChanged(QUrl const &url);
void useSSLforSMTPChanged(bool value);
void isAutomaticUpdateOnChanged(bool value);
void isBetaEnabledChanged(bool value);
void colorSchemeNameChanged(QString const &scheme);
void isDoHEnabledChanged(bool value);
void logsPathChanged(QUrl const &path);
void licensePathChanged(QUrl const &path);
void releaseNotesLinkChanged(QUrl const &link);
void dependencyLicensesLinkChanged(QUrl const &link);
void landingPageLinkChanged(QUrl const &link);
void versionChanged(QString const &version);
void currentEmailClientChanged(QString const &email);
void currentKeychainChanged(QString const &keychain);
void availableKeychainChanged(QStringList const &keychains);
void hostnameChanged(QString const &hostname);
void isAutostartOnChanged(bool value);
void portIMAPChanged(int port);
void portSMTPChanged(int port);
void usersChanged(UserList* users);
void dockIconVisibleChanged(bool value);
public slots: // slot for signals received from QML -> To be forwarded to Bridge via RPC Client calls.
void toggleAutostart(bool active); // _ func(makeItActive bool) `slot:"toggleAutostart"`
void toggleBeta(bool active); // _ func(makeItActive bool) `slot:"toggleBeta"`
void changeColorScheme(QString const &scheme); // _ func(string) `slot:"changeColorScheme"`
void changeLocalCache(bool enable, QUrl const& path) { logGRPCCallStatus(app().grpc().changeLocalCache(enable, path), "changeLocalCache"); } // _ func(enableDiskCache bool, diskCachePath core.QUrl) `slot:"changeLocalCache"`
void login(QString const& username, QString const& password) { logGRPCCallStatus(app().grpc().login(username, password), "login");} // _ func(username, password string) `slot:"login"`
void login2FA(QString const& username, QString const& code) { logGRPCCallStatus(app().grpc().login2FA(username, code), "login2FA");} // _ func(username, code string) `slot:"login2FA"`
void login2Password(QString const& username, QString const& password) { logGRPCCallStatus(app().grpc().login2Passwords(username, password),
"login2Passwords");} // _ func(username, password string) `slot:"login2Password"`
void loginAbort(QString const& username){ logGRPCCallStatus(app().grpc().loginAbort(username), "loginAbort");} // _ func(username string) `slot:"loginAbort"`
void toggleUseSSLforSMTP(bool makeItActive); // _ func(makeItActive bool) `slot:"toggleUseSSLforSMTP"`
void changePorts(int imapPort, int smtpPort); // _ func(imapPort, smtpPort int) `slot:"changePorts"`
void toggleDoH(bool active); // _ func(makeItActive bool) `slot:"toggleDoH"`
void toggleAutomaticUpdate(bool makeItActive); // _ func(makeItActive bool) `slot:"toggleAutomaticUpdate"`
void updateCurrentMailClient() { emit currentEmailClientChanged(currentEmailClient()); } // _ func() `slot:"updateCurrentMailClient"`
void changeKeychain(QString const &keychain); // _ func(keychain string) `slot:"changeKeychain"`
void guiReady(); // _ func() `slot:"guiReady"`
void quit(); // _ func() `slot:"quit"`
void restart(); // _ func() `slot:"restart"`
void checkUpdates(); // _ func() `slot:"checkUpdates"`
void installUpdate(); // _ func() `slot:"installUpdate"`
void triggerReset(); // _ func() `slot:"triggerReset"`
void reportBug(QString const &description, QString const& address, QString const &emailClient, bool includeLogs); // _ func(description, address, emailClient string, includeLogs bool) `slot:"reportBug"`
signals: // Signals received from the Go backend, to be forwarded to QML
void toggleAutostartFinished(); // _ func() `signal:"toggleAutostartFinished"`
void cacheUnavailable(); // _ func() `signal:"cacheUnavailable"`
void cacheCantMove(); // _ func() `signal:"cacheCantMove"`
void cacheLocationChangeSuccess(); // _ func() `signal:"cacheLocationChangeSuccess"`
void diskFull(); // _ func() `signal:"diskFull"`
void changeLocalCacheFinished(); // _ func() `signal:"changeLocalCacheFinished"`
void loginUsernamePasswordError(QString const &errorMsg); // _ func(errorMsg string) `signal:"loginUsernamePasswordError"`
void loginFreeUserError(); // _ func() `signal:"loginFreeUserError"`
void loginConnectionError(QString const &errorMsg); // _ func(errorMsg string) `signal:"loginConnectionError"`
void login2FARequested(QString const &username); // _ func(username string) `signal:"login2FARequested"`
void login2FAError(QString const& errorMsg); // _ func(errorMsg string) `signal:"login2FAError"`
void login2FAErrorAbort(QString const& errorMsg); // _ func(errorMsg string) `signal:"login2FAErrorAbort"`
void login2PasswordRequested(); // _ func() `signal:"login2PasswordRequested"`
void login2PasswordError(QString const& errorMsg); // _ func(errorMsg string) `signal:"login2PasswordError"`
void login2PasswordErrorAbort(QString const& errorMsg); // _ func(errorMsg string) `signal:"login2PasswordErrorAbort"`
void loginFinished(int index); // _ func(index int) `signal:"loginFinished"`
void loginAlreadyLoggedIn(int index); // _ func(index int) `signal:"loginAlreadyLoggedIn"`
void updateManualReady(QString const& version); // _ func(version string) `signal:"updateManualReady"`
void updateManualRestartNeeded(); // _ func() `signal:"updateManualRestartNeeded"`
void updateManualError(); // _ func() `signal:"updateManualError"`
void updateForce(QString const& version); // _ func(version string) `signal:"updateForce"`
void updateForceError(); // _ func() `signal:"updateForceError"`
void updateSilentRestartNeeded(); // _ func() `signal:"updateSilentRestartNeeded"`
void updateSilentError(); // _ func() `signal:"updateSilentError"`
void updateIsLatestVersion(); // _ func() `signal:"updateIsLatestVersion"`
void checkUpdatesFinished(); // _ func() `signal:"checkUpdatesFinished"`
void toggleUseSSLFinished(); // _ func() `signal:"toggleUseSSLFinished"`
void changePortFinished(); // _ func() `signal:"changePortFinished"`
void portIssueIMAP(); // _ func() `signal:"portIssueIMAP"`
void portIssueSMTP(); // _ func() `signal:"portIssueSMTP"`
void changeKeychainFinished(); // _ func() `signal:"changeKeychainFinished"`
void notifyHasNoKeychain(); // _ func() `signal:"notifyHasNoKeychain"`
void notifyRebuildKeychain(); // _ func() `signal:"notifyRebuildKeychain"`
void noActiveKeyForRecipient(QString const& email); // _ func(email string) `signal:noActiveKeyForRecipient`
void addressChanged(QString const& address); // _ func(address string) `signal:addressChanged`
void addressChangedLogout(QString const& address); // _ func(address string) `signal:addressChangedLogout`
void apiCertIssue(); // _ func() `signal:apiCertIssue`
void userDisconnected(QString const& username); // _ func(username string) `signal:userDisconnected`
void internetOff(); // _ func() `signal:"internetOff"`
void internetOn(); // _ func() `signal:"internetOn"`
void resetFinished(); // _ func() `signal:"resetFinished"`
void reportBugFinished(); // _ func() `signal:"reportBugFinished"`
void bugReportSendSuccess(); // _ func() `signal:"bugReportSendSuccess"`
void bugReportSendError(); // _ func() `signal:"bugReportSendError"`
void showMainWindow(); // _ func() `signal:showMainWindow`
private: // member functions
void retrieveUserList(); ///< Retrieve the list of users via gRPC.
void connectGrpcEvents(); ///< Connect gRPC that need to be forwarded to QML via backend signals
private: // data members
UserList* users_ { nullptr }; ///< The user list. Owned by backend.
std::unique_ptr<Overseer> eventStreamOverseer_; ///< The event stream overseer.
QUrl releaseNotesLink_; /// Release notes is not stored in the backend, it's pushed by the update check so we keep a local copy of it. \todo GODT-1670 Check this is implemented.
QUrl landingPageLink_; /// Landing page link is not stored in the backend, it's pushed by the update check so we keep a local copy of it. \todo GODT-1670 Check this is implemented.
friend class AppController;
};
#endif // BRIDGE_QT6_QMLBACKEND_H

View File

@ -0,0 +1,95 @@
// Copyright (c) 2022 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 "Pch.h"
#include "User.h"
#include "GRPC/GRPCUtils.h"
#include "GRPC/GRPCClient.h"
//****************************************************************************************************************************************************
/// \param[in] parent The parent object.
//****************************************************************************************************************************************************
User::User(QObject *parent)
: QObject(parent)
{
}
//****************************************************************************************************************************************************
/// \param[in] user The user to copy from
//****************************************************************************************************************************************************
void User::update(User const &user)
{
this->setProperty("username", user.username_);
this->setProperty("avatarText", user.avatarText_);
this->setProperty("loggedIn", user.loggedIn_);
this->setProperty("splitMode", user.splitMode_);
this->setProperty("setupGuideSeen", user.setupGuideSeen_);
this->setProperty("usedBytes", user.usedBytes_);
this->setProperty("totalBytes", user.totalBytes_);
this->setProperty("password", user.password_);
this->setProperty("addresses", user.addresses_);
this->setProperty("id", user.id_);
}
//****************************************************************************************************************************************************
/// \param[in] makeItActive Should split mode be made active.
//****************************************************************************************************************************************************
void User::toggleSplitMode(bool makeItActive)
{
logGRPCCallStatus(app().grpc().setUserSplitMode(id_, makeItActive), "toggleSplitMode");
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void User::logout()
{
logGRPCCallStatus(app().grpc().logoutUser(id_), "logoutUser");
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void User::remove()
{
logGRPCCallStatus(app().grpc().removeUser(id_), "removeUser");
}
//****************************************************************************************************************************************************
/// \param[in] address The email address to configure Apple Mail for.
//****************************************************************************************************************************************************
void User::configureAppleMail(QString const &address)
{
logGRPCCallStatus(app().grpc().configureAppleMail(id_, address), "configureAppleMail");
}
//****************************************************************************************************************************************************
// The only purpose of this call is to forward to the QML application the toggleSplitModeFinished(userID) event
// that was received by the UserList model.
//****************************************************************************************************************************************************
void User::emitToggleSplitModeFinished()
{
this->setProperty("splitMode", QVariant::fromValue(!this->property("splitMode").toBool()));
emit toggleSplitModeFinished();
}

View File

@ -0,0 +1,93 @@
// Copyright (c) 2022 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_QT6_USER_H
#define BRIDGE_QT6_USER_H
#include "Log.h"
//****************************************************************************************************************************************************
/// \brief User class.
//****************************************************************************************************************************************************
class User : public QObject
{
Q_OBJECT
public: // member functions.
explicit User(QObject *parent = nullptr); ///< Default constructor.
User(User const &) = delete; ///< Disabled copy-constructor.
User(User &&) = delete; ///< Disabled assignment copy-constructor.
~User() override = default; ///< Destructor.
User &operator=(User const &) = delete; ///< Disabled assignment operator.
User &operator=(User &&) = delete; ///< Disabled move assignment operator.
void update(User const &user); ///< Update the user
public slots:
// slots for QML generated calls
void toggleSplitMode(bool makeItActive); // _ func(makeItActive bool) `slot:"toggleSplitMode"`
void logout(); // _ func() `slot:"logout"`
void remove(); // _ func() `slot:"remove"`
void configureAppleMail(QString const &address); // _ func(address string) `slot:"configureAppleMail"`
// slots for external signals
void emitToggleSplitModeFinished();
public:
Q_PROPERTY(QString username MEMBER username_ NOTIFY usernameChanged) // _ string `property:"username"`
Q_PROPERTY(QString avatarText MEMBER avatarText_ NOTIFY avatarTextChanged) // _ string `property:"avatarText"`
Q_PROPERTY(bool loggedIn MEMBER loggedIn_ NOTIFY loggedInChanged) // _ bool `property:"loggedIn"`
Q_PROPERTY(bool splitMode MEMBER splitMode_ NOTIFY splitModeChanged) // _ bool `property:"splitMode"`
Q_PROPERTY(bool setupGuideSeen MEMBER setupGuideSeen_ NOTIFY setupGuideSeenChanged) // _ bool `property:"setupGuideSeen"`
Q_PROPERTY(float usedBytes MEMBER usedBytes_ NOTIFY usedBytesChanged) // _ float32 `property:"usedBytes"`
Q_PROPERTY(float totalBytes MEMBER totalBytes_ NOTIFY totalBytesChanged) // _ float32 `property:"totalBytes"`
Q_PROPERTY(QString password MEMBER password_ NOTIFY passwordChanged) // _ string `property:"password"`
Q_PROPERTY(QStringList addresses MEMBER addresses_ NOTIFY addressesChanged) // _ []string `property:"addresses"`
Q_PROPERTY(QString id MEMBER id_ NOTIFY idChanged) // _ string ID
signals:
// signals used for Qt properties
void usernameChanged(QString const &username);
void avatarTextChanged(QString const &avatarText);
void loggedInChanged(bool loggedIn);
void splitModeChanged(bool splitMode);
void setupGuideSeenChanged(bool seen);
void usedBytesChanged(float byteCount);
void totalBytesChanged(float byteCount);
void passwordChanged(QString const &);
void addressesChanged(QStringList const &);
void idChanged(QStringList const &id);
void toggleSplitModeFinished();
private:
QString id_; ///< The userID.
QString username_; ///< The username
QString avatarText_; ///< The avatar text (i.e. initials of the user)
bool loggedIn_{false}; ///< Is the user logged in.
bool splitMode_{false}; ///< Is split mode active.
bool setupGuideSeen_{false}; ///< Has the setup guide been seen.
float usedBytes_{0.0f}; ///< The storage used by the user.
float totalBytes_{0.0f}; ///< The storage quota of the user.
QString password_; ///< The IMAP password of the user.
QStringList addresses_; ///< The email address list of the user.
};
typedef std::shared_ptr<User> SPUser;
#endif // BRIDGE_QT6_USER_H

View File

@ -0,0 +1,220 @@
// Copyright (c) 2022 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 "Pch.h"
#include "UserList.h"
#include "GRPC/GRPCClient.h"
//****************************************************************************************************************************************************
/// \param[in] parent The parent object of the user list.
//****************************************************************************************************************************************************
UserList::UserList(QObject *parent)
: QAbstractListModel(parent)
{
/// \todo use mutex to prevent concurrent access
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void UserList::connectGRPCEvents() const
{
GRPCClient* client = &app().grpc();
connect(client, &GRPCClient::userChanged, this, &UserList::onUserChanged);
connect(client, &GRPCClient::toggleSplitModeFinished, this, &UserList::onToggleSplitModeFinished);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
int UserList::rowCount(QModelIndex const &) const
{
return users_.size();
}
//****************************************************************************************************************************************************
/// \param[in] index The index to retrieve data for.
/// \param[in] role The role to retrieve data for.
/// \return The data at the index for the given role.
//****************************************************************************************************************************************************
QVariant UserList::data(QModelIndex const &index, int role) const
{
/// This It does not seem to be used, but the method is required by the base class.
/// From the original QtThe recipe QML backend User model, the User is always returned, regardless of the role.
Q_UNUSED(role)
int const row = index.row();
if ((row < 0) || (row >= users_.size()))
return QVariant();
return QVariant::fromValue(users_[row].get());
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
/// \return the row of the user.
/// \return -1 if the userID is not in the list
//****************************************************************************************************************************************************
int UserList::rowOfUserID(QString const &userID) const
{
for (qint32 row = 0; row < users_.count(); ++row)
if (userID == users_[row]->property("id"))
return row;
return -1;
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void UserList::reset()
{
this->beginResetModel();
users_.clear();
this->endResetModel();
}
//****************************************************************************************************************************************************
/// \param[in] users The new user list.
//****************************************************************************************************************************************************
void UserList::reset(QList<SPUser> const &users)
{
this->beginResetModel();
users_ = users;
this->endResetModel();
}
//****************************************************************************************************************************************************
/// \param[in] user The user.
//****************************************************************************************************************************************************
void UserList::appendUser(SPUser const& user)
{
int const size = users_.size();
this->beginInsertRows(QModelIndex(), size, size);
users_.append(user);
this->endInsertRows();
}
//****************************************************************************************************************************************************
/// \param[in] row The row.
//****************************************************************************************************************************************************
void UserList::removeUserAt(int row)
{
if ((row < 0) && (row >= users_.size()))
return;
this->beginRemoveRows(QModelIndex(), row, row);
users_.removeAt(row);
this->endRemoveRows();
}
//****************************************************************************************************************************************************
/// \param[in] row The row.
/// \param[in] user The user.
//****************************************************************************************************************************************************
void UserList::updateUserAtRow(int row, User const &user)
{
if ((row < 0) || (row >= users_.count()))
{
app().log().error(QString("invalid user at row %2 (user count = %2)").arg(row).arg(users_.count()));
return;
}
users_[row]->update(user);
QModelIndex modelIndex = this->index(row);
emit dataChanged(modelIndex, modelIndex);
}
//****************************************************************************************************************************************************
/// \param[in] row The row.
//****************************************************************************************************************************************************
User *UserList::get(int row) const
{
if ((row < 0) || (row >= users_.count()))
{
app().log().error(QString("Requesting invalid user at row %1 (user count = %2)").arg(row).arg(users_.count()));
return nullptr;
}
app().log().debug(QString("Retrieving user at row %1 (user count = %2)").arg(row).arg(users_.count()));
return users_[row].get();
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
//****************************************************************************************************************************************************
void UserList::onUserChanged(QString const &userID)
{
int const index = this->rowOfUserID(userID);
SPUser user;
grpc::Status status = app().grpc().getUser(userID, user);
if ((!user) || (!status.ok()))
{
if (index >= 0) // user exists here but not in the go backend. we delete it.
{
app().log().debug(QString("Removing user from userlist: %1").arg(userID));
this->removeUserAt(index);
}
return;
}
if (index < 0)
{
app().log().debug(QString("Adding user in userlist: %1").arg(userID));
this->appendUser(user);
return;
}
app().log().debug(QString("Updating user in userlist: %1").arg(userID));
this->updateUserAtRow(index, *user);
}
//****************************************************************************************************************************************************
/// The only purpose of this function is to forward the toggleSplitModeFinished event received from gRPC to the
/// appropriate user.
///
/// \param[in] userID the userID.
//****************************************************************************************************************************************************
void UserList::onToggleSplitModeFinished(QString const &userID)
{
int const index = this->rowOfUserID(userID);
if (index < 0)
{
app().log().error(QString("Received toggleSplitModeFinished event for unknown userID %1").arg(userID));
return;
}
users_[index]->emitToggleSplitModeFinished();
}
//****************************************************************************************************************************************************
/// \return THe number of items in the list.
//****************************************************************************************************************************************************
int UserList::count() const
{
return users_.size();
}

View File

@ -0,0 +1,64 @@
// Copyright (c) 2022 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_QT6_USER_LIST_H
#define BRIDGE_QT6_USER_LIST_H
#include "User.h"
//****************************************************************************************************************************************************
/// \brief User list class.
//****************************************************************************************************************************************************
class UserList: public QAbstractListModel
{
Q_OBJECT
public: // member functions.
explicit UserList(QObject *parent = nullptr); ///< Default constructor.
UserList(UserList const &other) = delete ; ///< Disabled copy-constructor.
UserList& operator=(UserList const& other) = delete; ///< Disabled assignment operator.
~UserList() override = default; ///< Destructor
void connectGRPCEvents() const; ///< Connects gRPC event to the model.
int rowCount(QModelIndex const &parent) const override; ///< Return the number of row in the model
QVariant data(QModelIndex const &index, int role) const override; ///< Retrieve model data.
void reset(); ///< Reset the user list.
void reset(QList<SPUser> const &users); ///< Replace the user list.
int rowOfUserID(QString const &userID) const;
void removeUserAt(int row); ///< Remove the user at a given row
void appendUser(SPUser const& user); ///< Add a new user.
void updateUserAtRow(int row, User const& user); ///< Update the user at given row.
// the count property.
Q_PROPERTY(int count READ count NOTIFY countChanged)
int count() const; ///< The count property getter.
signals:
void countChanged(int count); ///< Signal for the count property.
public:
Q_INVOKABLE User* get(int row) const;
public slots: ///< handler for signals coming from the gRPC service
void onUserChanged(QString const &userID);
void onToggleSplitModeFinished(QString const &userID);
private: // data members
QList<SPUser> users_; ///< The user list.
};
#endif // BRIDGE_QT6_USER_LIST_H

View File

@ -0,0 +1,103 @@
// Copyright (c) 2022 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 "Pch.h"
#include "Overseer.h"
#include "Exception.h"
//****************************************************************************************************************************************************
/// \param[in] worker The worker.
/// \param[in] parent The parent object of the worker.
//****************************************************************************************************************************************************
Overseer::Overseer(Worker *worker, QObject *parent)
: QObject(parent)
, thread_(new QThread(parent))
, worker_(worker)
{
if (!worker_)
throw Exception("Overseer cannot accept a nil worker.");
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
Overseer::~Overseer()
{
this->release();
}
//****************************************************************************************************************************************************
/// \param[in] autorelease Should the overseer automatically release the worker and thread when done.
//****************************************************************************************************************************************************
void Overseer::startWorker(bool autorelease) const
{
if (!worker_)
throw Exception("Cannot start overseer with null worker.");
if (!thread_)
throw Exception("Cannot start overseer with null thread.");
worker_->moveToThread(thread_);
connect(thread_, &QThread::started, worker_, &Worker::run);
connect(worker_, &Worker::finished, thread_, &QThread::quit);
connect(worker_, &Worker::error, thread_, &QThread::quit);
if (autorelease)
{
connect(worker_, &Worker::error, this, &Overseer::release);
connect(worker_, &Worker::finished, this, &Overseer::release);
}
thread_->start();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void Overseer::release()
{
if (worker_)
{
worker_->deleteLater();
worker_ = nullptr;
}
if (thread_)
{
if (!thread_->isFinished())
{
thread_->quit();
thread_->wait();
}
thread_->deleteLater();
thread_ = nullptr;
}
}
//****************************************************************************************************************************************************
/// \return true iff the worker is finished, release
//****************************************************************************************************************************************************
bool Overseer::isFinished() const
{
if ((!worker_) || (!worker_->thread()))
return true;
return worker_->thread()->isFinished();
}

View File

@ -0,0 +1,55 @@
// Copyright (c) 2022 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_QT6_OVERSEER_H
#define BRIDGE_QT6_OVERSEER_H
#include "Worker.h"
//****************************************************************************************************************************************************
/// \brief Overseer used to manager a worker instance and its associated thread.
//****************************************************************************************************************************************************
class Overseer: public QObject
{
Q_OBJECT
public: // member functions.
explicit Overseer(Worker* worker, QObject* parent); ///< Default constructor.
Overseer(Overseer const&) = delete; ///< Disabled copy-constructor.
Overseer(Overseer&&) = delete; ///< Disabled assignment copy-constructor.
~Overseer() override; ///< Destructor.
Overseer& operator=(Overseer const&) = delete; ///< Disabled assignment operator.
Overseer& operator=(Overseer&&) = delete; ///< Disabled move assignment operator.
bool isFinished() const; ///< Check if the worker is finished.
public slots:
void startWorker(bool autorelease) const; ///< Run the worker.
void release(); ///< Delete the worker and its thread.
public: // data members.
QThread *thread_ { nullptr }; ///< The thread.
Worker *worker_ { nullptr }; ///< The worker.
};
typedef std::unique_ptr<Overseer> UPOverseer; ///< Type definition for unique pointer to Overseer.
typedef std::shared_ptr<Overseer> SPOverseer; ///< Type definition for shared pointer to Overseer.
#endif //BRIDGE_QT6_OVERSEER_H

View File

@ -0,0 +1,46 @@
// Copyright (c) 2022 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_QT6_WORKER_H
#define BRIDGE_QT6_WORKER_H
//****************************************************************************************************************************************************
/// \brief Pure virtual class for worker intended to perform a threaded operation.
//****************************************************************************************************************************************************
class Worker: public QObject
{
Q_OBJECT
public: // member functions
explicit Worker(QObject *parent) : QObject(parent) {} ///< Default constructor.
Worker(Worker const&) = delete; ///< Disabled copy-constructor.
Worker(Worker&&) = delete; ///< Disabled assignment copy-constructor.
~Worker() override = default; ///< Destructor.
Worker& operator=(Worker const&) = delete; ///< Disabled assignment operator.
Worker& operator=(Worker&&) = delete; ///< Disabled move assignment operator.
public slots:
virtual void run() = 0; ///< run the worker.
signals:
void started(); ///< Signal for the start of the worker
void finished(); ///< Signal for the end of the worker
void error(QString const& message); ///< Signal for errors. After an error, worker ends and finished is NOT emitted.
};
#endif //BRIDGE_QT6_WORKER_H

22
internal/frontend/qt6/build.sh Executable file
View File

@ -0,0 +1,22 @@
# Copyright (c) 2022 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/>.
#!/bin/bash
BUILD_DIR="./cmake-build-debug"
cmake -DCMAKE_BUILD_TYPE=Debug -G Ninja -S . -B ${BUILD_DIR}
ninja -C ${BUILD_DIR}

View File

@ -0,0 +1,135 @@
// Copyright (c) 2022 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 "Pch.h"
#include "Exception.h"
#include "QMLBackend.h"
#include "Log.h"
#include "EventStreamWorker.h"
//****************************************************************************************************************************************************
/// // initialize the Qt application.
//****************************************************************************************************************************************************
std::shared_ptr<QGuiApplication> initQtApplication(int argc, char *argv[])
{
// Note the two following attributes must be set before instantiating the QCoreApplication/QGuiApplication class.
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling, false);
if (QSysInfo::productType() != "windows")
QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL);
QString const qsgInfo = QProcessEnvironment::systemEnvironment().value("QSG_INFO");
if ((!qsgInfo.isEmpty()) && (qsgInfo != "0"))
QLoggingCategory::setFilterRules("qt.scenegraph.general=true");
auto app = std::make_shared<QGuiApplication>(argc, argv);
/// \todo GODT-1670 Get version from go backend.
QGuiApplication::setApplicationName("Proton Mail Bridge");
QGuiApplication::setApplicationVersion("3.0");
QGuiApplication::setOrganizationName("Proton AG");
QGuiApplication::setOrganizationDomain("proton.ch");
QGuiApplication::setQuitOnLastWindowClosed(false);
return app;
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void initLog()
{
Log &log = app().log();
log.setEchoInConsole(true);
log.setLevel(Log::Level::Debug);
}
//****************************************************************************************************************************************************
/// \param[in] engine The QML engine.
//****************************************************************************************************************************************************
QQmlComponent *createRootQmlComponent(QQmlApplicationEngine &engine)
{
/// \todo GODT-1669 pack QML and resources in QRC resource file.
QDir qmlDir("qml");
qmlRegisterType<QMLBackend>("CppBackend", 1, 0, "QMLBackend");
qmlRegisterType<UserList>("CppBackend", 1, 0, "UserList");
qmlRegisterType<User>("CppBackend", 1, 0, "User");
auto rootComponent = new QQmlComponent(&engine, &engine);
engine.addImportPath(qmlDir.absolutePath());
engine.addPluginPath(qmlDir.absolutePath());
QQuickStyle::addStylePath(qmlDir.absolutePath());
QQuickStyle::setStyle("Proton");
rootComponent->loadUrl(qmlDir.absoluteFilePath("Bridge.qml"));
if (rootComponent->status() != QQmlComponent::Status::Ready)
throw Exception("Could not load QML component");
return rootComponent;
}
//****************************************************************************************************************************************************
/// \param[in] argc The number of command-line arguments.
/// \param[in] argv The list of command-line arguments.
/// \return The exit code for the application.
//****************************************************************************************************************************************************
int main(int argc, char *argv[])
{
try
{
std::shared_ptr<QGuiApplication> guiApp = initQtApplication(argc, argv);
initLog();
/// \todo GODT-1667 Locate & Launch go backend (and wait for it).
app().backend().init();
QQmlApplicationEngine engine;
QQmlComponent *rootComponent = createRootQmlComponent(engine);
QObject *rootObject = rootComponent->beginCreate(engine.rootContext());
if (!rootObject)
throw Exception("Could not create root object.");
rootObject->setProperty("backend", QVariant::fromValue(&app().backend()));
rootComponent->completeCreate();
int result = QGuiApplication::exec();
app().log().info(QString("Exiting app with return code %1").arg(result));
app().grpc().stopEventStream();
app().backend().clearUserList();
/// \todo GODT-1667 shutdown go backend.
return result;
}
catch (Exception const &e)
{
app().log().error(e.qwhat());
return EXIT_FAILURE;
}
}

View File

@ -0,0 +1,190 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
Item {
id: root
property ColorScheme colorScheme
property var user
property var _spacing: 12 * ProtonStyle.px
property color usedSpaceColor : {
if (!root.enabled) return root.colorScheme.text_weak
if (root.type == AccountDelegate.SmallView) return root.colorScheme.text_weak
if (root.usedFraction < .50) return root.colorScheme.signal_success
if (root.usedFraction < .75) return root.colorScheme.signal_warning
return root.colorScheme.signal_danger
}
property real usedFraction: root.user ? reasonableFracion(root.user.usedBytes, root.user.totalBytes) : 0
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
function reasonableFracion(used, total){
var usedSafe = root.reasonableBytes(used)
var totalSafe = root.reasonableBytes(total)
if (totalSafe == 0 || usedSafe == 0) return 0
if (totalSafe <= usedSafe) return 1
return usedSafe / totalSafe
}
function reasonableBytes(bytes){
var safeBytes = bytes+0
if (safeBytes != bytes) return 0
if (safeBytes < 0) return 0
return Math.ceil(safeBytes)
}
function spaceWithUnits(bytes){
if (bytes*1 !== bytes || bytes == 0 ) return "0 kB"
var units = ['B',"kB", "MB", "GB", "TB"];
var i = parseInt(Math.floor(Math.log(bytes)/Math.log(1024)));
return Math.round(bytes*10 / Math.pow(1024, i))/10 + " " + units[i]
}
// width expected to be set by parent object
implicitHeight : children[0].implicitHeight
enum ViewType{
SmallView, LargeView
}
property var type : AccountDelegate.SmallView
RowLayout {
spacing: root._spacing
anchors {
top: root.top
left: root.left
right: root.rigth
}
Rectangle {
id: avatar
Layout.fillHeight: true
Layout.preferredWidth: height
radius: ProtonStyle.avatar_radius
color: root.colorScheme.background_avatar
Label {
colorScheme: root.colorScheme
anchors.fill: parent
text: root.user ? root.user.avatarText.toUpperCase(): ""
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Body
case AccountDelegate.LargeView: return Label.Title
}
}
font.weight: Font.Normal
color: "#FFFFFF"
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
}
}
ColumnLayout {
id: account
Layout.fillHeight: true
Layout.fillWidth: true
spacing: 0
Label {
Layout.maximumWidth: root.width - (
root._spacing + avatar.width
)
colorScheme: root.colorScheme
text: root.user ? user.username : ""
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Body
case AccountDelegate.LargeView: return Label.Title
}
}
elide: Text.ElideMiddle
}
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0 }
RowLayout {
spacing: 0
Label {
colorScheme: root.colorScheme
text: root.user && root.user.loggedIn ? root.usedSpace : qsTr("Signed out")
color: root.usedSpaceColor
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Caption
case AccountDelegate.LargeView: return Label.Body
}
}
}
Label {
colorScheme: root.colorScheme
text: root.user && root.user.loggedIn ? " / " + root.totalSpace : ""
color: root.colorScheme.text_weak
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Caption
case AccountDelegate.LargeView: return Label.Body
}
}
}
}
Rectangle {
id: storage_bar
visible: root.user ? root.type == AccountDelegate.LargeView : false
width: 140 * ProtonStyle.px
height: 4 * ProtonStyle.px
radius: ProtonStyle.storage_bar_radius
color: root.colorScheme.border_weak
Rectangle {
id: storage_bar_filled
radius: ProtonStyle.storage_bar_radius
color: root.usedSpaceColor
visible: root.user ? parent.visible && root.user.loggedIn : false
anchors {
top : parent.top
bottom : parent.bottom
left : parent.left
}
width: Math.min(1,Math.max(0.02,root.usedFraction)) * parent.width
}
}
}
Item {
Layout.fillWidth: true
}
}
}

View File

@ -0,0 +1,251 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
Item {
id: root
property ColorScheme colorScheme
property var backend
property var notifications
property var user
signal showSignIn()
signal showSetupGuide(var user, string address)
property int _leftMargin: 64
property int _rightMargin: 64
property int _topMargin: 32
property int _detailsTopMargin: 25
property int _bottomMargin: 12
property int _spacing: 20
property int _lineWidth: 1
ScrollView {
id: scrollView
clip: true
anchors.fill: parent
Item {
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
width: scrollView.availableWidth
height: scrollView.availableHeight
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
// do not set implicitWidth because implicit width of ColumnLayout will be equal to maximum implicit width of
// internal items. And if one of internal items would be a Text or Label - implicit width of those is always
// equal to non-wrapped text (i.e. one line only). That will lead to enabling horizontal scroll when not needed
implicitWidth: width
ColumnLayout {
spacing: 0
anchors.fill: parent
Rectangle {
id: topRectangle
color: root.colorScheme.background_norm
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
Layout.fillWidth: true
ColumnLayout {
spacing: root._spacing
anchors.fill: parent
anchors.leftMargin: root._leftMargin
anchors.rightMargin: root._rightMargin
anchors.topMargin: root._topMargin
anchors.bottomMargin: root._bottomMargin
RowLayout { // account delegate with action buttons
Layout.fillWidth: true
AccountDelegate {
Layout.fillWidth: true
colorScheme: root.colorScheme
user: root.user
type: AccountDelegate.LargeView
enabled: root.user ? root.user.loggedIn : false
}
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
text: qsTr("Sign out")
secondary: true
visible: root.user ? root.user.loggedIn : false
onClicked: {
if (!root.user) return
root.user.logout()
}
}
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
text: qsTr("Sign in")
secondary: true
visible: root.user ? !root.user.loggedIn : false
onClicked: {
if (!root.user) return
root.showSignIn()
}
}
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
icon.source: "icons/ic-trash.svg"
secondary: true
onClicked: {
if (!root.user) return
root.notifications.askDeleteAccount(root.user)
}
}
}
Rectangle {
Layout.fillWidth: true
height: root._lineWidth
color: root.colorScheme.border_weak
}
SettingsItem {
colorScheme: root.colorScheme
text: qsTr("Email clients")
actionText: qsTr("Configure")
description: qsTr("Using the mailbox details below (re)configure your client.")
type: SettingsItem.Button
enabled: root.user ? root.user.loggedIn : false
visible: root.user ? !root.user.splitMode || root.user.addresses.length==1 : false
showSeparator: splitMode.visible
onClicked: {
if (!root.user) return
root.showSetupGuide(root.user, user.addresses[0])
}
Layout.fillWidth: true
}
SettingsItem {
id: splitMode
colorScheme: root.colorScheme
text: qsTr("Split addresses")
description: qsTr("Setup multiple email addresses individually.")
type: SettingsItem.Toggle
checked: root.user ? root.user.splitMode : false
visible: root.user ? root.user.addresses.length > 1 : false
enabled: root.user ? root.user.loggedIn : false
showSeparator: addressSelector.visible
onClicked: {
if (!splitMode.checked){
root.notifications.askEnableSplitMode(user)
} else {
addressSelector.currentIndex = 0
root.user.toggleSplitMode(!splitMode.checked)
}
}
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
enabled: root.user ? root.user.loggedIn : false
visible: root.user ? root.user.splitMode : false
ComboBox {
id: addressSelector
colorScheme: root.colorScheme
Layout.fillWidth: true
model: root.user ? root.user.addresses : null
}
Button {
colorScheme: root.colorScheme
text: qsTr("Configure")
secondary: true
onClicked: {
if (!root.user) return
root.showSetupGuide(root.user, addressSelector.displayText)
}
}
}
}
}
Rectangle {
color: root.colorScheme.background_weak
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
Layout.fillWidth: true
ColumnLayout {
id: configuration
anchors.fill: parent
anchors.leftMargin: root._leftMargin
anchors.rightMargin: root._rightMargin
anchors.topMargin: root._detailsTopMargin
anchors.bottomMargin: root._spacing
spacing: root._spacing
visible: root.user ? root.user.loggedIn : false
property string currentAddress: addressSelector.displayText
Label {
colorScheme: root.colorScheme
text: qsTr("Mailbox details")
type: Label.Body_semibold
}
Configuration {
colorScheme: root.colorScheme
title: qsTr("IMAP")
hostname: root.backend.hostname
port: root.backend.portIMAP.toString()
username: configuration.currentAddress
password: root.user ? root.user.password : ""
security: "STARTTLS"
}
Configuration {
colorScheme: root.colorScheme
title: qsTr("SMTP")
hostname : root.backend.hostname
port : root.backend.portSMTP.toString()
username : configuration.currentAddress
password : root.user ? root.user.password : ""
security : root.backend.useSSLforSMTP ? "SSL" : "STARTTLS"
}
}
}
}
}
}
}

View File

@ -0,0 +1,234 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import Proton 4.0
import Notifications 1.0
Popup {
id: root
property ColorScheme colorScheme
property Notification notification
property var mainWindow
topMargin: 37
leftMargin: (mainWindow.width - root.implicitWidth)/2
implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin
implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin
popupType: ApplicationWindow.PopupType.Banner
shouldShow: notification ? (notification.active && !notification.dismissed) : false
modal: false
Action {
id: defaultDismissAction
text: qsTr("OK")
onTriggered: {
if (!root.notification) {
return
}
root.notification.dismissed = true
}
}
RowLayout {
id: contentLayout
anchors.fill: parent
spacing: 0
Item {
Layout.fillHeight: true
Layout.fillWidth: true
clip: 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
Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
width: parent.width + 10
radius: ProtonStyle.banner_radius
color: {
if (!root.notification) {
return "transparent"
}
switch (root.notification.type) {
case Notification.NotificationType.Info:
return root.colorScheme.signal_info
case Notification.NotificationType.Success:
return root.colorScheme.signal_success
case Notification.NotificationType.Warning:
return root.colorScheme.signal_warning
case Notification.NotificationType.Danger:
return root.colorScheme.signal_danger
}
}
}
RowLayout {
anchors.fill: parent
anchors.topMargin: 14
anchors.bottomMargin: 14
anchors.leftMargin: 16
spacing: 8
ColorImage {
color: root.colorScheme.text_invert
width: 24
height: 24
sourceSize.width: 24
sourceSize.height: 24
Layout.preferredHeight: 24
Layout.preferredWidth: 24
source: {
if (!root.notification) {
return ""
}
switch (root.notification.type) {
case Notification.NotificationType.Info:
return "./icons/ic-info-circle-filled.svg"
case Notification.NotificationType.Success:
return "./icons/ic-info-circle-filled.svg"
case Notification.NotificationType.Warning:
return "./icons/ic-exclamation-circle-filled.svg"
case Notification.NotificationType.Danger:
return "./icons/ic-exclamation-circle-filled.svg"
}
}
}
Label {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: 16
color: root.colorScheme.text_invert
text: root.notification ? root.notification.description : ""
wrapMode: Text.WordWrap
}
}
}
Rectangle {
Layout.fillHeight: true
width: 1
color: {
if (!root.notification) {
return "transparent"
}
switch (root.notification.type) {
case Notification.NotificationType.Info:
return root.colorScheme.signal_info_active
case Notification.NotificationType.Success:
return root.colorScheme.signal_success_active
case Notification.NotificationType.Warning:
return root.colorScheme.signal_warning_active
case Notification.NotificationType.Danger:
return root.colorScheme.signal_danger_active
}
}
}
Button {
colorScheme: root.colorScheme
Layout.fillHeight: true
id: actionButton
action: (root.notification && root.notification.action.length > 0) ? root.notification.action[0] : defaultDismissAction
background: Item {
clip: true
Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
width: parent.width + 10
radius: ProtonStyle.banner_radius
color: {
if (!root.notification) {
return "transparent"
}
var norm
var hover
var active
switch (root.notification.type) {
case Notification.NotificationType.Info:
norm = root.colorScheme.signal_info
hover = root.colorScheme.signal_info_hover
active = root.colorScheme.signal_info_active
break;
case Notification.NotificationType.Success:
norm = root.colorScheme.signal_success
hover = root.colorScheme.signal_success_hover
active = root.colorScheme.signal_success_active
break;
case Notification.NotificationType.Warning:
norm = root.colorScheme.signal_warning
hover = root.colorScheme.signal_warning_hover
active = root.colorScheme.signal_warning_active
break;
case Notification.NotificationType.Danger:
norm = root.colorScheme.signal_danger
hover = root.colorScheme.signal_danger_hover
active = root.colorScheme.signal_danger_active
break;
}
if (actionButton.down) {
return active
}
if (actionButton.enabled && (actionButton.highlighted || actionButton.hovered || actionButton.checked)) {
return hover
}
if (actionButton.loading) {
return hover
}
return norm
}
}
}
}
}
}

View File

@ -0,0 +1,294 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.13
import QtQuick.Window 2.13
import Qt.labs.platform 1.1
import Proton 4.0
import Notifications 1.0
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))
}
property var backend
property var title: "Proton Mail Bridge"
property Notifications _notifications: Notifications {
id: notifications
backend: root.backend
frontendMain: mainWindow
frontendStatus: statusWindow
frontendTray: trayIcon
}
property MainWindow _mainWindow: MainWindow {
id: mainWindow
visible: false
title: root.title
backend: root.backend
notifications: root._notifications
onVisibleChanged: {
backend.dockIconVisible = visible
}
Connections {
target: root.backend
onCacheUnavailable: {
mainWindow.showAndRise()
}
onColorSchemeNameChanged: root.setColorScheme()
}
}
property StatusWindow _statusWindow: StatusWindow {
id: statusWindow
visible: false
title: root.title
backend: root.backend
notifications: root._notifications
onShowMainWindow: {
mainWindow.showAndRise()
}
onShowHelp: {
mainWindow.showHelp()
mainWindow.showAndRise()
}
onShowSettings: {
mainWindow.showSettings()
mainWindow.showAndRise()
}
onShowSignIn: {
mainWindow.showSignIn(username)
mainWindow.showAndRise()
}
onQuit: {
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 preferebly 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 preferebly 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 preferebly 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 preferebly 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 satatus 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 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 `./icons/systray-${color}-${level}.png`
}
}
Component.onCompleted: {
if (!root.backend) {
console.log("backend not loaded")
}
root.setColorScheme()
if (!root.backend.users) {
console.log("users not loaded")
}
var c = root.backend.users.count
var u = root.backend.users.get(0)
// DEBUG
if (c != 0) {
console.log("users non zero", c)
console.log("first user", u )
}
if (c === 0) {
mainWindow.showAndRise()
}
if (u) {
if (c === 1 && u.loggedIn === false) {
mainWindow.showAndRise()
}
}
if (root.backend.showOnStartup) {
mainWindow.showAndRise()
}
root.backend.guiReady()
}
function setColorScheme() {
if (root.backend.colorSchemeName == "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
if (root.backend.colorSchemeName == "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle
}
}

View File

@ -0,0 +1,316 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import Proton 4.0
ColumnLayout {
id: root
property var user
property var userIndex
property var backend
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 ? user.loggedIn : 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.loggedIn = true
user.resetLoginRequests()
return
} else {
user.loggedIn = false
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: {
root.backend.loginUsernamePasswordError("")
user.resetLoginRequests()
}
}
Button {
colorScheme: root.colorScheme
text: "free user error"
enabled: user !== undefined //&& user.isLoginRequested
onClicked: {
root.backend.loginFreeUserError()
user.resetLoginRequests()
}
}
Button {
colorScheme: root.colorScheme
text: "connection error"
enabled: user !== undefined //&& user.isLoginRequested
onClicked: {
root.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: {
root.backend.login2FARequested(user.username)
user.isLogin2FARequested = true
}
}
Button {
colorScheme: root.colorScheme
text: "error"
enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
onClicked: {
root.backend.login2FAError("")
user.isLogin2FAProvided = false
}
}
Button {
colorScheme: root.colorScheme
text: "Abort"
enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
onClicked: {
root.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: {
root.backend.login2PasswordRequested("")
user.isLogin2PasswordRequested = true
}
}
Button {
colorScheme: root.colorScheme
text: "error"
enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
onClicked: {
root.backend.login2PasswordError("")
user.isLogin2PasswordProvided = false
}
}
Button {
colorScheme: root.colorScheme
text: "Abort"
enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
onClicked: {
root.backend.login2PasswordErrorAbort("")
user.resetLoginRequests()
}
}
}
RowLayout {
Button {
colorScheme: root.colorScheme
text: "Login Finished"
onClicked: {
root.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: {
root.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 imlicitWidth
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
}
}

View File

@ -0,0 +1,102 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import Proton 4.0
ColumnLayout {
id: root
property ColorScheme colorScheme
property var backend
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.loggedIn = true
newUserObject.setupGuideSeen = true // backend.loginUser.setupGuideSeen
backend.loginUser.username = ""
backend.loginUser.loggedIn = false
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()
}
}
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2022 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 2.12
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)
}
}

View File

@ -0,0 +1,982 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.13
import QtQuick.Window 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQml.Models 2.12
import Qt.labs.platform 1.1
import Proton 4.0
import "./BridgeTest"
import BridgePreview 1.0
import Notifications 1.0
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.loggedIn = true
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("confugure 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 = true
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/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 portIssueIMAP()
signal portIssueSMTP()
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://protonmail.com/download/bridge/early_releases.html")
property url dependencyLicensesLink: Qt.resolvedUrl("https://github.com/ProtonMail/proton-bridge/v2/blob/master/COPYING_NOTES.md#dependencies")
property url landingPageLink: Qt.resolvedUrl("https://protonmail.com/bridge")
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()
}
}

View File

@ -0,0 +1,192 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
SettingsView {
id: root
fillHeight: true
property var selectedAddress
Label {
text: qsTr("Report a problem")
colorScheme: root.colorScheme
type: Label.Heading
}
TextArea {
id: description
property int _minLength: 150
property int _maxLength: 800
label: qsTr("Description")
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: heightForLinesVisible(4)
hint: description.text.length + "/" + _maxLength
placeholderText: qsTr("Tell us what went wrong or isn't working (min. %1 characters).").arg(_minLength)
validator: function(text) {
if (description.text.length < description._minLength) {
return qsTr("Enter a problem description (min. %1 characters).").arg(_minLength)
}
if (description.text.length > description._maxLength) {
return qsTr("Enter a problem description (max. %1 characters).").arg(_maxLength)
}
return
}
onTextChanged: {
// Rise max length error imidiatly while typing
if (description.text.length > description._maxLength) {
validate()
}
}
KeyNavigation.priority: KeyNavigation.BeforeItem
KeyNavigation.tab: address
// set implicitHeight to explicit height because se don't
// want TextArea implicitHeight (which is height of all text)
// to be considered in SettingsView internal scroll view
implicitHeight: height
}
TextField {
id: address
label: qsTr("Your contact email")
colorScheme: root.colorScheme
Layout.fillWidth: true
placeholderText: qsTr("e.g. jane.doe@protonmail.com")
validator: function(str) {
if (!isValidEmail(str)) {
return qsTr("Enter valid email address")
}
return
}
}
TextField {
id: emailClient
label: qsTr("Your email client (including version)")
colorScheme: root.colorScheme
Layout.fillWidth: true
placeholderText: qsTr("e.g. Apple Mail 14.0")
validator: function(str) {
if (str.length === 0) {
return qsTr("Enter an email client name and version")
}
return
}
}
RowLayout {
CheckBox {
id: includeLogs
text: qsTr("Include my recent logs")
colorScheme: root.colorScheme
checked: true
}
Button {
Layout.leftMargin: 12
text: qsTr("View logs")
secondary: true
colorScheme: root.colorScheme
onClicked: Qt.openUrlExternally(root.backend.logsPath)
}
}
TextEdit {
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
readOnly: true
Layout.fillWidth: true
color: root.colorScheme.text_weak
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.caption_font_size
font.letterSpacing: ProtonStyle.caption_letter_spacing
// No way to set lineHeight: Style.caption_line_height
selectionColor: root.colorScheme.interaction_norm
selectedTextColor: root.colorScheme.text_invert
wrapMode: Text.WordWrap
selectByMouse: true
}
Button {
id: sendButton
text: qsTr("Send")
colorScheme: root.colorScheme
onClicked: {
description.validate()
address.validate()
emailClient.validate()
if (description.error || address.error || emailClient.error) {
return
}
submit()
}
Connections {target: root.backend; onReportBugFinished: sendButton.loading = false }
}
function setDefaultValue() {
description.text = ""
address.text = root.selectedAddress
emailClient.text = root.backend.currentEmailClient
includeLogs.checked = true
}
function isValidEmail(text){
var reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/
return reEmail.test(text)
}
function submit() {
sendButton.loading = true
root.backend.reportBug(
description.text,
address.text,
emailClient.text,
includeLogs.checked
)
}
onVisibleChanged: {
root.setDefaultValue()
}
}

View File

@ -0,0 +1,73 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import Proton 4.0
Rectangle {
id: root
property ColorScheme colorScheme
property string title
property string hostname
property string port
property string username
property string password
property string security
implicitWidth: 304
implicitHeight: content.height + 2*root._margin
color: root.colorScheme.background_norm
radius: ProtonStyle.card_radius
property int _margin: 24
ColumnLayout {
id: content
width: root.width - 2*root._margin
anchors{
top: root.top
left: root.left
leftMargin : root._margin
rightMargin : root._margin
topMargin : root._margin
bottomMargin : root._margin
}
spacing: 12
Label {
colorScheme: root.colorScheme
text: root.title
type: Label.Body_semibold
}
Item{}
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Hostname") ; value: root.hostname }
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Port") ; value: root.port }
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Username") ; value: root.username }
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Password") ; value: root.password }
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Security") ; value: root.security }
}
}

View File

@ -0,0 +1,89 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import Proton 4.0
Item {
id: root
Layout.fillWidth: true
property var colorScheme
property string label
property string value
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
ColumnLayout {
width: root.width
RowLayout {
Layout.fillWidth: true
ColumnLayout {
Label {
colorScheme: root.colorScheme
text: root.label
type: Label.Body
}
TextEdit {
id: valueText
text: root.value
color: root.colorScheme.text_weak
readOnly: true
selectByMouse: true
selectByKeyboard: true
selectionColor: root.colorScheme.text_weak
}
}
Item {
Layout.fillWidth: true
}
ColorImage {
source: "icons/ic-copy.svg"
color: root.colorScheme.text_norm
height: root.colorScheme.body_font_size
sourceSize.height: root.colorScheme.body_font_size
MouseArea {
anchors.fill: parent
onClicked : {
valueText.select(0, valueText.length)
valueText.copy()
valueText.deselect()
}
onPressed: parent.scale = 0.90
onReleased: parent.scale = 1
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.colorScheme.border_norm
}
}
}

View File

@ -0,0 +1,408 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
import Notifications 1.0
Item {
id: root
property ColorScheme colorScheme
property var backend
property var notifications
signal showSetupGuide(var user, string address)
RowLayout {
anchors.fill: parent
spacing: 0
Rectangle {
id: leftBar
property ColorScheme colorScheme: root.colorScheme.prominent
Layout.minimumWidth: 264
Layout.maximumWidth: 320
Layout.preferredWidth: 320
Layout.fillHeight: true
color: colorScheme.background_norm
ColumnLayout {
anchors.fill: parent
spacing: 0
RowLayout {
id:topLeftBar
Layout.fillWidth: true
Layout.minimumHeight: 60
Layout.maximumHeight: 60
Layout.preferredHeight: 60
spacing: 0
Status {
Layout.leftMargin: 16
Layout.topMargin: 24
Layout.bottomMargin: 17
Layout.alignment: Qt.AlignHCenter
colorScheme: leftBar.colorScheme
backend: root.backend
notifications: root.notifications
notificationWhitelist: Notifications.Group.Connection | Notifications.Group.ForceUpdate
}
// just a placeholder
Item {
Layout.fillHeight: true
Layout.fillWidth: true
}
Button {
colorScheme: leftBar.colorScheme
Layout.minimumHeight: 36
Layout.maximumHeight: 36
Layout.preferredHeight: 36
Layout.minimumWidth: 36
Layout.maximumWidth: 36
Layout.preferredWidth: 36
Layout.topMargin: 16
Layout.bottomMargin: 9
Layout.rightMargin: 4
horizontalPadding: 0
icon.source: "./icons/ic-question-circle.svg"
onClicked: rightContent.showHelpView()
}
Button {
colorScheme: leftBar.colorScheme
Layout.minimumHeight: 36
Layout.maximumHeight: 36
Layout.preferredHeight: 36
Layout.minimumWidth: 36
Layout.maximumWidth: 36
Layout.preferredWidth: 36
Layout.topMargin: 16
Layout.bottomMargin: 9
Layout.rightMargin: 16
horizontalPadding: 0
icon.source: "./icons/ic-cog-wheel.svg"
onClicked: rightContent.showGeneralSettings()
}
}
Item {implicitHeight:10}
// Separator line
Rectangle {
Layout.fillWidth: true
Layout.minimumHeight: 1
Layout.maximumHeight: 1
color: leftBar.colorScheme.border_weak
}
ListView {
id: accounts
property var _topBottomMargins: 24
property var _leftRightMargins: 16
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: accounts._leftRightMargins
Layout.rightMargin: accounts._leftRightMargins
Layout.topMargin: accounts._topBottomMargins
Layout.bottomMargin: accounts._topBottomMargins
spacing: 12
clip: true
boundsBehavior: Flickable.StopAtBounds
header: Rectangle {
height: headerLabel.height+16
// color: ProtonStyle.transparent
Label{
colorScheme: leftBar.colorScheme
id: headerLabel
text: qsTr("Accounts")
type: Label.LabelType.Body
}
}
highlight: Rectangle {
color: leftBar.colorScheme.interaction_default_active
radius: ProtonStyle.account_row_radius
}
model: root.backend.users
delegate: Item {
width: leftBar.width - 2*accounts._leftRightMargins
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
AccountDelegate {
id: accountDelegate
anchors.fill: parent
anchors.topMargin: 8
anchors.bottomMargin: 8
anchors.leftMargin: 12
anchors.rightMargin: 12
colorScheme: leftBar.colorScheme
user: root.backend.users.get(index)
}
MouseArea {
anchors.fill: parent
onClicked: {
var user = root.backend.users.get(index)
accounts.currentIndex = index
if (!user) return
if (user.loggedIn) {
rightContent.showAccount()
} else {
signIn.username = user.username
rightContent.showSignIn()
}
}
}
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.minimumHeight: 1
Layout.maximumHeight: 1
color: leftBar.colorScheme.border_weak
}
Item {
id: bottomLeftBar
Layout.fillWidth: true
Layout.minimumHeight: 52
Layout.maximumHeight: 52
Layout.preferredHeight: 52
Button {
colorScheme: leftBar.colorScheme
width: 36
height: 36
anchors.left: parent.left
anchors.top: parent.top
anchors.leftMargin: 16
anchors.topMargin: 7
horizontalPadding: 0
icon.source: "./icons/ic-plus.svg"
onClicked: {
signIn.username = ""
rightContent.showSignIn()
}
}
}
}
}
Rectangle { // right content background
Layout.fillWidth: true
Layout.fillHeight: true
color: colorScheme.background_norm
StackLayout {
id: rightContent
anchors.fill: parent
AccountView { // 0
colorScheme: root.colorScheme
backend: root.backend
notifications: root.notifications
user: {
if (accounts.currentIndex < 0) return undefined
if (root.backend.users.count == 0) return undefined
return root.backend.users.get(accounts.currentIndex)
}
onShowSignIn: {
signIn.username = this.user.username
rightContent.showSignIn()
}
onShowSetupGuide: {
root.showSetupGuide(user,address)
}
}
GridLayout { // 1 Sign In
columns: 2
Button {
id: backButton
Layout.leftMargin: 18
Layout.topMargin: 10
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
onClicked: {
signIn.abort()
rightContent.showAccount()
}
icon.source: "icons/ic-arrow-left.svg"
secondary: true
horizontalPadding: 8
}
SignIn {
id: signIn
Layout.topMargin: 68
Layout.leftMargin: 80 - backButton.width - 18
Layout.rightMargin: 80
Layout.bottomMargin: 68
Layout.preferredWidth: 320
Layout.fillWidth: true
Layout.fillHeight: true
colorScheme: root.colorScheme
backend: root.backend
}
}
GeneralSettings { // 2
colorScheme: root.colorScheme
backend: root.backend
notifications: root.notifications
onBack: {
rightContent.showAccount()
}
}
KeychainSettings { // 3
colorScheme: root.colorScheme
backend: root.backend
onBack: {
rightContent.showGeneralSettings()
}
}
PortSettings { // 4
colorScheme: root.colorScheme
backend: root.backend
onBack: {
rightContent.showGeneralSettings()
}
}
SMTPSettings { // 5
colorScheme: root.colorScheme
backend: root.backend
onBack: {
rightContent.showGeneralSettings()
}
}
LocalCacheSettings { // 6
colorScheme: root.colorScheme
backend: root.backend
notifications: root.notifications
onBack: {
rightContent.showGeneralSettings()
}
}
HelpView { // 7
colorScheme: root.colorScheme
backend: root.backend
onBack: {
rightContent.showAccount()
}
}
BugReportView { // 8
colorScheme: root.colorScheme
backend: root.backend
selectedAddress: {
if (accounts.currentIndex < 0) return ""
if (root.backend.users.count == 0) return ""
var user = root.backend.users.get(accounts.currentIndex)
if (!user) return ""
return user.addresses[0]
}
onBack: {
rightContent.showHelpView()
}
}
function showAccount(index) {
if (index !== undefined && index >= 0){
accounts.currentIndex = index
}
rightContent.currentIndex = 0
}
function showSignIn () { rightContent.currentIndex = 1 }
function showGeneralSettings () { rightContent.currentIndex = 2 }
function showKeychainSettings () { rightContent.currentIndex = 3 }
function showPortSettings () { rightContent.currentIndex = 4 }
function showSMTPSettings () { rightContent.currentIndex = 5 }
function showLocalCacheSettings () { rightContent.currentIndex = 6 }
function showHelpView () { rightContent.currentIndex = 7 }
function showBugReport () { rightContent.currentIndex = 8 }
Connections {
target: root.backend
onLoginFinished: rightContent.showAccount(index)
onLoginAlreadyLoggedIn: rightContent.showAccount(index)
}
}
}
}
function showLocalCacheSettings(){rightContent.showLocalCacheSettings() }
function showSettings(){rightContent.showGeneralSettings() }
function showHelp(){rightContent.showHelpView() }
function showSignIn(username){
signIn.username = username
rightContent.showSignIn()
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Controls 2.12
import "."
import "./Proton"
Rectangle {
property var target: parent
x: target.x
y: target.y
width: target.width
height: target.height
color: "transparent"
border.color: "red"
border.width: 1
//z: parent.z - 1
z: 10000000
Label {
text: parent.width + "x" + parent.height
anchors.centerIn: parent
color: "black"
colorScheme: ProtonStyle.currentStyle
}
Rectangle {
width: target.implicitWidth
height: target.implicitHeight
color: "transparent"
border.color: "green"
border.width: 1
//z: parent.z - 1
z: 10000000
}
}

View File

@ -0,0 +1,225 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
import Proton 4.0
SettingsView {
id: root
property bool _isAdvancedShown: false
property var notifications
fillHeight: false
Label {
colorScheme: root.colorScheme
text: qsTr("Settings")
type: Label.Heading
Layout.fillWidth: true
}
SettingsItem {
id: autoUpdate
colorScheme: root.colorScheme
text: qsTr("Automatic updates")
description: qsTr("Bridge will automatically update in the background.")
type: SettingsItem.Toggle
checked: root.backend.isAutomaticUpdateOn
onClicked: root.backend.toggleAutomaticUpdate(!autoUpdate.checked)
Layout.fillWidth: true
}
SettingsItem {
id: autostart
colorScheme: root.colorScheme
text: qsTr("Open on startup")
description: qsTr("Bridge will open upon startup.")
type: SettingsItem.Toggle
checked: root.backend.isAutostartOn
onClicked: {
autostart.loading = true
root.backend.toggleAutostart(!autostart.checked)
}
Connections{
target: root.backend
onToggleAutostartFinished: {
autostart.loading = false
}
}
Layout.fillWidth: true
}
SettingsItem {
id: beta
colorScheme: root.colorScheme
text: qsTr("Beta access")
description: qsTr("Be among the first to try new features.")
type: SettingsItem.Toggle
checked: root.backend.isBetaEnabled
onClicked: {
if (!beta.checked) {
root.notifications.askEnableBeta()
} else {
root.backend.toggleBeta(false)
}
}
Layout.fillWidth: true
}
RowLayout {
ColorImage {
Layout.alignment: Qt.AlignTop
source: root._isAdvancedShown ? "icons/ic-chevron-up.svg" : "icons/ic-chevron-down.svg"
color: root.colorScheme.interaction_norm
height: root.colorScheme.body_font_size
sourceSize.height: root.colorScheme.body_font_size
MouseArea {
anchors.fill: parent
onClicked: root._isAdvancedShown = !root._isAdvancedShown
}
}
Label {
id: advSettLabel
colorScheme: root.colorScheme
text: qsTr("Advanced settings")
color: root.colorScheme.interaction_norm
type: Label.Body
MouseArea {
anchors.fill: parent
onClicked: root._isAdvancedShown = !root._isAdvancedShown
}
}
}
SettingsItem {
id: keychains
visible: root._isAdvancedShown && root.backend.availableKeychain.length > 1
colorScheme: root.colorScheme
text: qsTr("Change keychain")
description: qsTr("Change which keychain Bridge uses as default")
actionText: qsTr("Change")
type: SettingsItem.Button
checked: root.backend.isDoHEnabled
onClicked: root.parent.showKeychainSettings()
Layout.fillWidth: true
}
SettingsItem {
id: doh
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Alternative routing")
description: qsTr("If Protons servers are blocked in your location, alternative network routing will be used to reach Proton.")
type: SettingsItem.Toggle
checked: root.backend.isDoHEnabled
onClicked: root.backend.toggleDoH(!doh.checked)
Layout.fillWidth: true
}
SettingsItem {
id: darkMode
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Dark mode")
description: qsTr("Choose dark color theme.")
type: SettingsItem.Toggle
checked: root.backend.colorSchemeName == "dark"
onClicked: root.backend.changeColorScheme( darkMode.checked ? "light" : "dark")
Layout.fillWidth: true
}
SettingsItem {
id: allMail
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Show All Mail")
description: qsTr("Choose to list the All Mail folder in your local client.")
type: SettingsItem.Toggle
checked: root.backend.isAllMailVisible
onClicked: root.notifications.askChangeAllMailVisibility(root.backend.isAllMailVisible)
Layout.fillWidth: true
}
SettingsItem {
id: ports
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Default ports")
actionText: qsTr("Change")
description: qsTr("Choose which ports are used by default.")
type: SettingsItem.Button
onClicked: root.parent.showPortSettings()
Layout.fillWidth: true
}
SettingsItem {
id: smtp
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("SMTP connection mode")
actionText: qsTr("Change")
description: qsTr("Change the protocol Bridge and your client use to connect.")
type: SettingsItem.Button
onClicked: root.parent.showSMTPSettings()
Layout.fillWidth: true
}
SettingsItem {
id: cache
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Local cache")
actionText: qsTr("Configure")
description: qsTr("Configure Bridge's local cache.")
type: SettingsItem.Button
onClicked: root.parent.showLocalCacheSettings()
Layout.fillWidth: true
}
SettingsItem {
id: reset
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Reset Bridge")
actionText: qsTr("Reset")
description: qsTr("Remove all accounts, clear cached data, and restore the original settings.")
type: SettingsItem.Button
onClicked: {
root.notifications.askResetBridge()
}
Layout.fillWidth: true
}
}

View File

@ -0,0 +1,116 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
SettingsView {
id: root
fillHeight: true
Label {
colorScheme: root.colorScheme
text: qsTr("Help")
type: Label.Heading
Layout.fillWidth: true
}
SettingsItem {
id: setupPage
colorScheme: root.colorScheme
text: qsTr("Installation and setup")
actionText: qsTr("Go to help topics")
actionIcon: "./icons/ic-external-link.svg"
description: qsTr("Get help setting up your client with our instructions and FAQs.")
type: SettingsItem.PrimaryButton
onClicked: {Qt.openUrlExternally("https://protonmail.com/support/categories/bridge/")}
Layout.fillWidth: true
}
SettingsItem {
id: checkUpdates
colorScheme: root.colorScheme
text: qsTr("Updates")
actionText: qsTr("Check now")
description: qsTr("Check that you're using the latest version of Bridge. To stay up to date, enable auto-updates in settings.")
type: SettingsItem.Button
onClicked: {
checkUpdates.loading = true
root.backend.checkUpdates()
}
Connections {target: root.backend; onCheckUpdatesFinished: checkUpdates.loading = false}
Layout.fillWidth: true
}
SettingsItem {
id: logs
colorScheme: root.colorScheme
text: qsTr("Logs")
actionText: qsTr("View logs")
description: qsTr("Open and review logs to troubleshoot.")
type: SettingsItem.Button
onClicked: Qt.openUrlExternally(root.backend.logsPath)
Layout.fillWidth: true
}
SettingsItem {
id: reportBug
colorScheme: root.colorScheme
text: qsTr("Report a problem")
actionText: qsTr("Report a problem")
description: qsTr("Something not working as expected? Let us know.")
type: SettingsItem.Button
onClicked: {
root.backend.updateCurrentMailClient()
root.parent.showBugReport()
}
Layout.fillWidth: true
}
// fill height so the footer label will be allways attached to the bottom
Item {
Layout.fillHeight: true
Layout.fillWidth: true
}
Label {
Layout.alignment: Qt.AlignHCenter
colorScheme: root.colorScheme
type: Label.Caption
color: root.colorScheme.text_weak
textFormat: Text.StyledText
horizontalAlignment: Text.AlignHCenter
text: qsTr("Proton Mail Bridge v%1<br>© 2021 Proton AG<br>%2 %3<br>%4").
arg(root.backend.version).
arg(link(root.backend.licensePath, qsTr("License"))).
arg(link(root.backend.dependencyLicensesLink, qsTr("Dependencies"))).
arg(link(root.backend.releaseNotesLink, qsTr("Release notes")))
onLinkActivated: Qt.openUrlExternally(link)
}
}

View File

@ -0,0 +1,116 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
import Proton 4.0
SettingsView {
id: root
fillHeight: false
property bool _valuesChanged: keychainSelection.checkedButton && keychainSelection.checkedButton.text != root.backend.currentKeychain
Label {
colorScheme: root.colorScheme
text: qsTr("Default keychain")
type: Label.Heading
Layout.fillWidth: true
}
Label {
colorScheme: root.colorScheme
text: qsTr("Change which keychain Bridge uses as default")
type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
ColumnLayout {
spacing: 16
ButtonGroup{ id: keychainSelection }
Repeater {
model: root.backend.availableKeychain
RadioButton {
colorScheme: root.colorScheme
ButtonGroup.group: keychainSelection
text: modelData
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak
}
RowLayout {
spacing: 12
Button {
id: submitButton
colorScheme: root.colorScheme
text: qsTr("Save and restart")
enabled: root._valuesChanged
onClicked: {
root.backend.changeKeychain(keychainSelection.checkedButton.text)
}
}
Button {
colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true
}
Connections {
target: root.backend
onChangeKeychainFinished: {
submitButton.loading = false
root.back()
}
}
}
onBack: {
root.setDefaultValues()
}
function setDefaultValues(){
for (var bi in keychainSelection.buttons){
var button = keychainSelection.buttons[bi]
if (button.text == root.backend.currentKeychain) {
button.checked = true
break;
}
}
}
Component.onCompleted: root.setDefaultValues()
}

View File

@ -0,0 +1,149 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
import QtQuick.Dialogs 1.1
import Proton 4.0
SettingsView {
id: root
fillHeight: false
property var notifications
property bool _diskCacheEnabled: true
property url _diskCachePath: pathDialog.shortcuts.home
Label {
colorScheme: root.colorScheme
text: qsTr("Local cache")
type: Label.Heading
Layout.fillWidth: true
}
Label {
colorScheme: root.colorScheme
text: qsTr("Bridge stores your encrypted messages locally to optimize communication with your client.")
type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
Layout.maximumWidth: this.parent.Layout.maximumWidth
wrapMode: Text.WordWrap
}
SettingsItem {
colorScheme: root.colorScheme
text: qsTr("Enable local cache")
description: qsTr("Recommended for optimal performance.")
type: SettingsItem.Toggle
checked: root._diskCacheEnabled
onClicked: root._diskCacheEnabled = !root._diskCacheEnabled
Layout.fillWidth: true
}
SettingsItem {
colorScheme: root.colorScheme
text: qsTr("Current cache location")
actionText: qsTr("Change location")
description: root.backend.goos === "windows" ?
root._diskCachePath.toString().replace("file:///", "").replace(new RegExp("/", 'g'), "\\") + "\\" :
root._diskCachePath.toString().replace("file://", "") + "/"
descriptionWrap: Text.WrapAnywhere
type: SettingsItem.Button
enabled: root._diskCacheEnabled
onClicked: {
pathDialog.open()
}
Layout.fillWidth: true
FileDialog {
id: pathDialog
title: qsTr("Select cache location")
folder: root._diskCachePath
onAccepted: root._diskCachePath = pathDialog.fileUrl
selectFolder: true
}
}
RowLayout {
spacing: 12
Button {
id: submitButton
colorScheme: root.colorScheme
text: qsTr("Save and restart")
enabled: (
root.backend.diskCachePath != root._diskCachePath ||
root.backend.isDiskCacheEnabled != root._diskCacheEnabled
)
onClicked: {
root.submit()
}
}
Button {
colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true
}
Connections {
target: root.backend
onChangeLocalCacheFinished: {
submitButton.loading = false
root.setDefaultValues()
}
}
}
onBack: {
root.setDefaultValues()
}
function submit(){
if (!root._diskCacheEnabled && root.backend.isDiskCacheEnabled) {
root.notifications.askDisableLocalCache()
return
}
if (root._diskCacheEnabled && !root.backend.isDiskCacheEnabled) {
root.notifications.askEnableLocalCache(root._diskCachePath)
return
}
// Not asking, only changing path
submitButton.loading = true
root.backend.changeLocalCache(root.backend.isDiskCacheEnabled, root._diskCachePath)
}
function setDefaultValues(){
root._diskCacheEnabled = root.backend.isDiskCacheEnabled
root._diskCachePath = root.backend.diskCachePath
}
onVisibleChanged: {
root.setDefaultValues()
}
}

View File

@ -0,0 +1,212 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.13
import QtQuick.Window 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
import Notifications 1.0
import CppBackend 1.0
import "tests"
ApplicationWindow {
id: root
width: 960
height: 576
visible: true
minimumHeight: contentLayout.implicitHeight
minimumWidth: contentLayout.implicitWidth
colorScheme: ProtonStyle.currentStyle
property var backend
property var notifications
// 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 MainWindow will be
// shown before StatusWindow - StatusWindow will not have transparent corners.
color: "transparent"
// show Setup Guide on every new user
Connections {
target: root.backend.users
onRowsInserted: {
// considerring that users are added one-by-one
var user = root.backend.users.get(first)
if (!user.loggedIn) {
return
}
if (user.setupGuideSeen) {
return
}
root.showSetup(user,user.addresses[0])
}
onRowsAboutToBeRemoved: {
for (var i = first; i <= last; i++ ) {
var user = root.backend.users.get(i)
if (setupGuide.user === user) {
setupGuide.user = null
contentLayout._showSetup = false
return
}
}
}
}
Connections {
target: root.backend
onShowMainWindow: {
root.showAndRise()
}
onLoginFinished: {
console.debug("Login finished", index)
}
}
StackLayout {
id: contentLayout
anchors.fill: parent
property bool _showSetup: false
currentIndex: {
// show welcome when there are no users or only one non-logged-in user is present
if (backend.users.count === 0) {
return 1
}
var u = backend.users.get(0)
if (!u) {
console.trace()
console.log("empty user")
return 1
}
if (backend.users.count === 1 && u.loggedIn === false) {
return 1
}
if (contentLayout._showSetup) {
return 2
}
return 0
}
ContentWrapper { // 0
id: contentWrapper
colorScheme: root.colorScheme
backend: root.backend
notifications: root.notifications
Layout.fillHeight: true
Layout.fillWidth: true
onShowSetupGuide: {
root.showSetup(user,address)
}
}
WelcomeGuide { // 1
colorScheme: root.colorScheme
backend: root.backend
Layout.fillHeight: true
Layout.fillWidth: true
}
SetupGuide { // 2
id: setupGuide
colorScheme: root.colorScheme
backend: root.backend
Layout.fillHeight: true
Layout.fillWidth: true
onDismissed: {
root.showSetup(null,"")
}
onFinished: {
// TODO: Do not close window. Trigger backend to check that
// there is a successfully connected client. Then backend
// should send another signal to close the setup guide.
root.showSetup(null,"")
}
}
}
NotificationPopups {
colorScheme: root.colorScheme
notifications: root.notifications
mainWindow: root
backend: root.backend
}
SplashScreen {
id: splashScreen
colorScheme: root.colorScheme
backend: root.backend
}
function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() }
function showSettings() { contentWrapper.showSettings() }
function showHelp() { contentWrapper.showHelp() }
function showSignIn(username) {
if (contentLayout.currentIndex == 1) return
contentWrapper.showSignIn(username)
}
function showSetup(user, address) {
setupGuide.user = user
setupGuide.address = address
setupGuide.reset()
if (setupGuide.user) {
contentLayout._showSetup = true
} else {
contentLayout._showSetup = false
}
}
function showAndRise() {
root.show()
root.raise()
if (!root.active) {
root.requestActivate()
}
}
}

View File

@ -0,0 +1,119 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
import Notifications 1.0
Dialog {
id: root
property var notification
shouldShow: notification && notification.active && !notification.dismissed
modal: true
default property alias data: additionalChildrenContainer.children
ColumnLayout {
spacing: 0
Image {
Layout.alignment: Qt.AlignHCenter
sourceSize.width: 64
sourceSize.height: 64
Layout.preferredHeight: 64
Layout.preferredWidth: 64
Layout.bottomMargin: 16
visible: source != ""
source: {
if (!root.notification) {
return ""
}
switch (root.notification.type) {
case Notification.NotificationType.Info:
return "./icons/ic-info.svg"
case Notification.NotificationType.Success:
return "./icons/ic-success.svg"
case Notification.NotificationType.Warning:
case Notification.NotificationType.Danger:
return "./icons/ic-alert.svg"
}
}
}
Label {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 8
colorScheme: root.colorScheme
text: root.notification.title
type: Label.LabelType.Title
}
Label {
Layout.fillWidth: true
Layout.preferredWidth: 240
Layout.bottomMargin: 16
colorScheme: root.colorScheme
text: root.notification.description
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
type: Label.LabelType.Body
onLinkActivated: Qt.openUrlExternally(link)
}
Item {
id: additionalChildrenContainer
Layout.fillWidth: true
Layout.bottomMargin: 16
visible: children.length > 0
implicitHeight: additionalChildrenContainer.childrenRect.height
implicitWidth: additionalChildrenContainer.childrenRect.width
}
ColumnLayout {
spacing: 8
Repeater {
model: root.notification.action
delegate: Button {
Layout.fillWidth: true
colorScheme: root.colorScheme
action: modelData
secondary: index > 0
loading: modelData.loading
}
}
}
}
}

View File

@ -0,0 +1,132 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
import Notifications 1.0
Item {
id: root
property var backend
property ColorScheme colorScheme
property var notifications
property var mainWindow
property int notificationWhitelist: NotificationFilter.FilterConsts.All
property int notificationBlacklist: NotificationFilter.FilterConsts.None
NotificationFilter {
id: bannerNotificationFilter
source: root.notifications.all
blacklist: Notifications.Group.Dialogs
}
Banner {
colorScheme: root.colorScheme
notification: bannerNotificationFilter.topmost
mainWindow: root.mainWindow
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.updateManualReady
Switch {
id:autoUpdate
colorScheme: root.colorScheme
text: qsTr("Update automatically in the future")
checked: root.backend.isAutomaticUpdateOn
onClicked: root.backend.toggleAutomaticUpdate(autoUpdate.checked)
}
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.updateForce
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.updateForceError
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.enableBeta
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.cacheUnavailable
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.cacheCantMove
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.diskFull
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.enableSplitMode
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.disableLocalCache
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.enableLocalCache
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.resetBridge
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.changeAllMailVisibility
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.deleteAccount
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.noKeychain
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.rebuildKeychain
}
}

View File

@ -0,0 +1,54 @@
// Copyright (c) 2022 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 2.12
import QtQuick.Controls 2.12
QtObject {
id: root
default property var children
enum NotificationType {
Info = 0,
Success = 1,
Warning = 2,
Danger = 3
}
// title is used in dialogs only
property string title
// description is used in banners and in dialogs as description
property string description
// brief is used in status view only
property string brief
property string icon
property list<Action> action
property int type
property int group
property bool dismissed: false
property bool active: false
readonly property var occurred: active ? new Date() : undefined
property var data
onActiveChanged: {
dismissed = false
}
}

View File

@ -0,0 +1,114 @@
// Copyright (c) 2022 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 2.12
import QtQml.Models 2.12
// contains notifications that satisfy black- and whitelist and are sorted in time-occurred order
ListModel {
id: root
enum FilterConsts {
None = 0,
All = 255
}
property int whitelist: NotificationFilter.FilterConsts.All
property int blacklist: NotificationFilter.FilterConsts.None
property Notification topmost
property var source
property bool componentCompleted: false
Component.onCompleted: {
root.componentCompleted = true
root.rebuildList()
}
// 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)
}
function rebuildList() {
// avoid evaluation of the list before Component.onCompleted
if (!root.componentCompleted) {
return
}
for (var i = 0; i < root.count; i++) {
root.get(i).onActiveChanged.disconnect( root.updateList )
}
root.clear()
if (!root.source) {
return
}
for (i = 0; i < root.source.length; i++) {
var obj = root.source[i]
if (obj.group & root.blacklist) {
continue
}
if (!(obj.group & root.whitelist)) {
continue
}
root.append({obj})
obj.onActiveChanged.connect( root.updateList )
}
}
function updateList() {
var topmost = null
for (var i = 0; i < root.count; i++) {
var obj = root.get(i)
if (!obj.active) {
continue
}
if (topmost && (topmost.type > obj.type)) {
continue
}
if (topmost && (topmost.type === obj.type) && (topmost.occurred > obj.occurred)) {
continue
}
topmost = obj
}
root.topmost = topmost
}
onWhitelistChanged: {
root.rebuildList()
}
onBlacklistChanged: {
root.rebuildList()
}
onSourceChanged: {
root.rebuildList()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
module Notifications
depends QtQml 2.12
Notifications 1.0 Notifications.qml
NotificationFilter 1.0 NotificationFilter.qml
Notification 1.0 Notification.qml

View File

@ -0,0 +1,164 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
import Proton 4.0
SettingsView {
id: root
fillHeight: false
property bool _valuesChanged: (
imapField.text*1 !== root.backend.portIMAP ||
smtpField.text*1 !== root.backend.portSMTP
)
Label {
colorScheme: root.colorScheme
text: qsTr("Default ports")
type: Label.Heading
Layout.fillWidth: true
}
Label {
colorScheme: root.colorScheme
text: qsTr("Changes require reconfiguration of your email client. Bridge will automatically restart.")
type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
RowLayout {
spacing: 16
TextField {
id: imapField
colorScheme: root.colorScheme
label: qsTr("IMAP port")
Layout.preferredWidth: 160
validator: root.validate
}
TextField {
id: smtpField
colorScheme: root.colorScheme
label: qsTr("SMTP port")
Layout.preferredWidth: 160
validator: root.validate
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak
}
RowLayout {
spacing: 12
Button {
id: submitButton
colorScheme: root.colorScheme
text: qsTr("Save and restart")
enabled: root._valuesChanged
onClicked: {
// removing error here because we may have set it manually (port occupied)
imapField.error = false
smtpField.error = false
// checking errors seperatly because we want to display "same port" error only once
imapField.validate()
if (imapField.error) {
return
}
smtpField.validate()
if (smtpField.error) {
return
}
submitButton.loading = true
// check both ports before returning an error
var err = false
err |= !isPortFree(imapField)
err |= !isPortFree(smtpField)
if (err) {
submitButton.loading = false
return
}
root.backend.changePorts(imapField.text, smtpField.text)
}
}
Button {
colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true
}
Connections {
target: root.backend
onChangePortFinished: submitButton.loading = false
}
}
onBack: {
root.setDefaultValues()
}
function validate(port) {
var num = port*1
if (! (num > 1 && num < 65536) ) {
return qsTr("Invalid port number")
}
if (imapField.text == smtpField.text) {
return qsTr("Port numbers must be different")
}
return
}
function isPortFree(field) {
var num = field.text*1
if (num === root.backend.portIMAP) return true
if (num === root.backend.portSMTP) return true
if (!root.backend.isPortFree(num)) {
field.error = true
field.errorString = qsTr("Port occupied")
return false
}
return true
}
function setDefaultValues(){
imapField.text = backend.portIMAP
smtpField.text = backend.portSMTP
}
Component.onCompleted: root.setDefaultValues()
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2022 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 2.12
import QtQuick.Templates 2.12 as T
T.Action {
property bool loading
}

View File

@ -0,0 +1,134 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
T.ApplicationWindow {
id: root
property ColorScheme colorScheme
// popup priority based on types
enum PopupType {
Banner = 0,
Dialog = 1
}
// contains currently visible popup
property var popupVisible: null
// list of all popups within ApplicationWindow
property ListModel popups: 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)
}
onRowsInserted: {
for (var i = first; i <= last; i++) {
var obj = popups.get(i)
obj.onShouldShowChanged.connect( root.processPopups )
}
processPopups()
}
onRowsAboutToBeRemoved: {
for (var i = first; i <= last; i++ ) {
var obj = popups.get(i)
obj.onShouldShowChanged.disconnect( root.processPopups )
// if currently visible popup was removed
if (root.popupVisible === obj) {
root.popupVisible.visible = false
root.popupVisible = null
}
}
processPopups()
}
}
function processPopups() {
if ((root.popupVisible) && (!root.popupVisible.shouldShow)) {
root.popupVisible.visible = false
}
var topmost = null
for (var i = 0; i < popups.count; i++) {
var obj = popups.get(i)
if (obj.shouldShow === false) {
continue
}
if (topmost && (topmost.popupType > obj.popupType)) {
continue
}
if (topmost && (topmost.popupType === obj.popupType) && (topmost.occurred > obj.occurred)) {
continue
}
topmost = obj
}
if (root.popupVisible !== topmost) {
if (root.popupVisible) {
root.popupVisible.visible = false
}
root.popupVisible = topmost
}
if (!root.popupVisible) {
return
}
root.popupVisible.visible = true
}
Connections {
target: root.popupVisible
onVisibleChanged: {
if (root.popupVisible.visible) {
return
}
root.popupVisible = null
root.processPopups()
}
}
color: root.colorScheme.background_norm
overlay.modal: Rectangle {
color: root.colorScheme.backdrop_norm
}
overlay.modeless: Rectangle {
color: "transparent"
}
}

View File

@ -0,0 +1,245 @@
// Copyright (c) 2022 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 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
import QtQuick.Layouts 1.12
import "." as Proton
T.Button {
property ColorScheme colorScheme
property alias secondary: control.flat
readonly property bool primary: !secondary
readonly property bool isIcon: control.text === ""
property bool loading: false
property bool borderless: false
property int labelType: Proton.Label.LabelType.Body
property alias textVerticalAlignment: label.verticalAlignment
property alias textHorizontalAlignment: label.horizontalAlignment
// TODO: store previous enabled state and restore it?
// For now assuming that only enabled buttons could have loading state
onLoadingChanged: {
if (loading) {
enabled = false
} else {
enabled = true
}
}
id: control
implicitWidth: Math.max(
implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding
)
implicitHeight: Math.max(
implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding
)
padding: 8
horizontalPadding: 16
spacing: 10
font: label.font
icon.width: 16
icon.height: 16
icon.color: {
if (primary && !isIcon) {
return "#FFFFFF"
} else {
return control.colorScheme.text_norm
}
}
contentItem: RowLayout {
id: _contentItem
spacing: control.spacing
Proton.Label {
colorScheme: root.colorScheme
id: label
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
elide: Text.ElideRight
horizontalAlignment: Qt.AlignHCenter
visible: !control.isIcon
text: control.text
color: {
if (primary && !isIcon) {
return "#FFFFFF"
} else {
return control.colorScheme.text_norm
}
}
opacity: control.enabled || control.loading ? 1.0 : 0.5
type: labelType
}
ColorImage {
id: iconImage
Layout.alignment: Qt.AlignCenter
width: {
// special case for loading since we want icon to be square for rotation animation
if (control.loading) {
return Math.min(control.icon.width, availableWidth, control.icon.height, availableHeight)
}
return Math.min(control.icon.width, availableWidth)
}
height: {
if (control.loading) {
return width
}
Math.min(control.icon.height, availableHeight)
}
sourceSize.width: control.icon.width
sourceSize.height: control.icon.height
color: control.icon.color
source: control.loading ? "../icons/Loader_16.svg" : control.icon.source
visible: control.loading || control.icon.source
RotationAnimation {
target: iconImage
loops: Animation.Infinite
duration: 1000
from: 0
to: 360
direction: RotationAnimation.Clockwise
running: control.loading
}
}
}
background: Rectangle {
implicitWidth: 36
implicitHeight: 36
radius: Style.button_radius
visible: true
color: {
if (!isIcon) {
if (primary) {
// Primary colors
if (control.down) {
return control.colorScheme.interaction_norm_active
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_norm_hover
}
if (control.loading) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.interaction_norm
} else {
// Secondary colors
if (control.down) {
return control.colorScheme.interaction_default_active
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover
}
if (control.loading) {
return control.colorScheme.interaction_default_hover
}
return control.colorScheme.interaction_default
}
} else {
if (primary) {
// Primary icon colors
if (control.down) {
return control.colorScheme.interaction_default_active
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover
}
if (control.loading) {
return control.colorScheme.interaction_default_hover
}
return control.colorScheme.interaction_default
} else {
// Secondary icon colors
if (control.down) {
return control.colorScheme.interaction_default_active
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover
}
if (control.loading) {
return control.colorScheme.interaction_default_hover
}
return control.colorScheme.interaction_default
}
}
}
border.color: {
return control.colorScheme.border_norm
}
border.width: secondary && !borderless ? 1 : 0
opacity: control.enabled || control.loading ? 1.0 : 0.5
}
Component.onCompleted: {
if (!control.colorScheme) {
console.trace()
var next = root
for (var i = 0; i<1000; i++) {
console.log(i, next, "colorscheme", next.colorScheme)
next = next.parent
if (!next) break
}
console.error("ColorScheme not defined")
}
}
}

View File

@ -0,0 +1,134 @@
// Copyright (c) 2022 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 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
T.CheckBox {
property ColorScheme colorScheme
property bool error: false
id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
padding: 0
spacing: 8
indicator: Rectangle {
implicitWidth: 20
implicitHeight: 20
radius: Style.checkbox_radius
x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
y: control.topPadding + (control.availableHeight - height) / 2
color: {
if (!checked) {
return control.colorScheme.background_norm
}
if (!control.enabled) {
return control.colorScheme.field_disabled
}
if (control.error) {
return control.colorScheme.signal_danger
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.interaction_norm
}
border.width: control.checked ? 0 : 1
border.color: {
if (!control.enabled) {
return control.colorScheme.field_disabled
}
if (control.error) {
return control.colorScheme.signal_danger
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.field_norm
}
ColorImage {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: parent.width - 4
height: parent.height - 4
sourceSize.width: parent.width - 4
sourceSize.height: parent.height - 4
color: "#FFFFFF"
source: "../icons/ic-check.svg"
visible: control.checkState === Qt.Checked
}
// TODO: do we need PartiallyChecked state?
// Rectangle {
// x: (parent.width - width) / 2
// y: (parent.height - height) / 2
// width: 16
// height: 3
// color: control.palette.text
// visible: control.checkState === Qt.PartiallyChecked
//}
}
contentItem: CheckLabel {
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
color: {
if (!enabled) {
return control.colorScheme.text_disabled
}
if (error) {
return control.colorScheme.signal_danger
}
return control.colorScheme.text_norm
}
font.family: Style.font_family
font.weight: Style.fontWeight_400
font.pixelSize: Style.body_font_size
lineHeight: Style.body_line_height
lineHeightMode: Text.FixedHeight
font.letterSpacing: Style.body_letter_spacing
}
}

View File

@ -0,0 +1,92 @@
// Copyright (c) 2022 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 2.13
QtObject {
// should be a pointer to ColorScheme object
property var prominent
// Primary
property color primay_norm
// Interaction-norm
property color interaction_norm
property color interaction_norm_hover
property color interaction_norm_active
// Text
property color text_norm
property color text_weak
property color text_hint
property color text_disabled
property color text_invert
// Field
property color field_norm
property color field_hover
property color field_disabled
// Border
property color border_norm
property color border_weak
// Background
property color background_norm
property color background_weak
property color background_strong
property color background_avatar
// Interaction-weak
property color interaction_weak
property color interaction_weak_hover
property color interaction_weak_active
// Interaction-default
property color interaction_default
property color interaction_default_hover
property color interaction_default_active
// Scrollbar
property color scrollbar_norm
property color scrollbar_hover
// Signal
property color signal_danger
property color signal_danger_hover
property color signal_danger_active
property color signal_warning
property color signal_warning_hover
property color signal_warning_active
property color signal_success
property color signal_success_hover
property color signal_success_active
property color signal_info
property color signal_info_hover
property color signal_info_active
// Shadows
property color shadow_norm
property color shadow_lifted
// Backdrop
property color backdrop_norm
// Images
property string welcome_img
property string logo_img
}

View File

@ -0,0 +1,195 @@
// Copyright (c) 2022 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 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
T.ComboBox {
id: root
property ColorScheme colorScheme
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
leftPadding: 12 + (!root.mirrored || !indicator || !indicator.visible ? 0 : indicator.width + spacing)
rightPadding: 12 + (root.mirrored || !indicator || !indicator.visible ? 0 : indicator.width + spacing)
topPadding: 5
bottomPadding: 5
spacing: 8
font.family: Style.font_family
font.weight: Style.fontWeight_400
font.pixelSize: Style.body_font_size
font.letterSpacing: Style.body_letter_spacing
contentItem: T.TextField {
padding: 5
text: root.editable ? root.editText : root.displayText
font: root.font
enabled: root.editable
autoScroll: root.editable
readOnly: root.down
inputMethodHints: root.inputMethodHints
validator: root.validator
verticalAlignment: TextInput.AlignVCenter
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
selectionColor: root.colorScheme.interaction_norm
selectedTextColor: root.colorScheme.text_invert
placeholderTextColor: root.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
background: Rectangle {
radius: Style.context_item_radius
visible: root.enabled && root.editable && !root.flat
border.color: {
if (root.activeFocus) {
return root.colorScheme.interaction_norm
}
if (root.hovered || root.activeFocus) {
return root.colorScheme.field_hover
}
return root.colorScheme.field_norm
}
border.width: 1
color: root.colorScheme.background_norm
}
}
background: Rectangle {
implicitWidth: 140
implicitHeight: 36
radius: Style.context_item_radius
color: {
if (root.down) {
return root.colorScheme.interaction_default_active
}
if (root.enabled && root.hovered || root.activeFocus) {
return root.colorScheme.interaction_default_hover
}
if (!root.enabled) {
return root.colorScheme.interaction_default
}
return root.colorScheme.background_norm
}
border.color: root.colorScheme.border_norm
border.width: 1
}
indicator: ColorImage {
x: root.mirrored ? 12 : root.width - width - 12
y: root.topPadding + (root.availableHeight - height) / 2
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
source: popup.visible ? "../icons/ic-chevron-up.svg" : "../icons/ic-chevron-down.svg"
sourceSize.width: 16
sourceSize.height: 16
}
delegate: ItemDelegate {
width: parent.width
text: root.textRole ? (Array.isArray(root.model) ? modelData[root.textRole] : model[root.textRole]) : modelData
palette.text: {
if (!root.enabled) {
return root.colorScheme.text_disabled
}
if (selected) {
return root.colorScheme.text_invert
}
return root.colorScheme.text_norm
}
font: root.font
hoverEnabled: root.hoverEnabled
property bool selected: root.currentIndex === index
highlighted: root.highlightedIndex === index
palette.highlightedText: selected ? root.colorScheme.text_invert : root.colorScheme.text_norm
background: PaddedRectangle {
radius: Style.context_item_radius
color: {
if (parent.down) {
return root.colorScheme.interaction_default_active
}
if (parent.selected) {
return root.colorScheme.interaction_norm
}
if (parent.hovered || parent.highlighted) {
return root.colorScheme.interaction_default_hover
}
return root.colorScheme.interaction_default
}
}
}
popup: T.Popup {
y: root.height
width: root.width
height: Math.min(contentItem.implicitHeight, root.Window.height - topMargin - bottomMargin)
topMargin: 8
bottomMargin: 8
contentItem: Item {
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
ListView {
anchors.fill: parent
anchors.margins: 8
implicitHeight: contentHeight
model: root.delegateModel
currentIndex: root.highlightedIndex
spacing: 4
T.ScrollIndicator.vertical: ScrollIndicator { }
}
}
background: Rectangle {
color: root.colorScheme.background_norm
radius: Style.dialog_radius
border.color: root.colorScheme.border_weak
border.width: 1
}
}
}

View File

@ -0,0 +1,80 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.12
import QtQuick.Templates 2.12 as T
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
T.Dialog {
id: root
property ColorScheme colorScheme
Component.onCompleted: {
if (!ApplicationWindow.window) {
return
}
if (ApplicationWindow.window.popups === undefined) {
return
}
var obj = this
ApplicationWindow.window.popups.append( { obj } )
}
readonly property int popupType: ApplicationWindow.PopupType.Dialog
property bool shouldShow: false
readonly property var occurred: shouldShow ? new Date() : undefined
function open() {
root.shouldShow = true
}
function close() {
root.shouldShow = false
}
anchors.centerIn: Overlay.overlay
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
contentWidth + leftPadding + rightPadding,
implicitHeaderWidth,
implicitFooterWidth)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding
+ (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0)
+ (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0))
padding: 24
background: Rectangle {
color: root.colorScheme.background_norm
radius: Style.dialog_radius
}
// TODO: Add DropShadow here
T.Overlay.modal: Rectangle {
color: root.colorScheme.backdrop_norm
}
T.Overlay.modeless: Rectangle {
color: "transparent"
}
}

View File

@ -0,0 +1,142 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
import "." as Proton
T.Label {
id: root
property ColorScheme colorScheme
enum LabelType {
// weight 700, size 28, height 36
Heading,
// weight 700, size 20, height 24
Title,
// weight 400, size 18, height 26
Lead,
// weight 400, size 14, height 20, spacing 0.2
Body,
// weight 600, size 14, height 20, spacing 0.2
Body_semibold,
// weight 700, size 14, height 20, spacing 0.2
Body_bold,
// weight 400, size 12, height 16, spacing 0.4
Caption,
// weight 600, size 12, height 16, spacing 0.4
Caption_semibold,
// weight 700, size 12, height 16, spacing 0.4
Caption_bold
}
property int type: Proton.Label.LabelType.Body
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
linkColor: root.colorScheme.interaction_norm
palette.link: linkColor
font.family: Style.font_family
lineHeightMode: Text.FixedHeight
font.weight: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return Style.fontWeight_700
case Proton.Label.LabelType.Title:
return Style.fontWeight_700
case Proton.Label.LabelType.Lead:
return Style.fontWeight_400
case Proton.Label.LabelType.Body:
return Style.fontWeight_400
case Proton.Label.LabelType.Body_semibold:
return Style.fontWeight_600
case Proton.Label.LabelType.Body_bold:
return Style.fontWeight_700
case Proton.Label.LabelType.Caption:
return Style.fontWeight_400
case Proton.Label.LabelType.Caption_semibold:
return Style.fontWeight_600
case Proton.Label.LabelType.Caption_bold:
return Style.fontWeight_700
}
}
font.pixelSize: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return Style.heading_font_size
case Proton.Label.LabelType.Title:
return Style.title_font_size
case Proton.Label.LabelType.Lead:
return Style.lead_font_size
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return Style.body_font_size
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return Style.caption_font_size
}
}
lineHeight: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return Style.heading_line_height
case Proton.Label.LabelType.Title:
return Style.title_line_height
case Proton.Label.LabelType.Lead:
return Style.lead_line_height
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return Style.body_line_height
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return Style.caption_line_height
}
}
font.letterSpacing: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
case Proton.Label.LabelType.Title:
case Proton.Label.LabelType.Lead:
return 0
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return Style.body_letter_spacing
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return Style.caption_letter_spacing
}
}
verticalAlignment: Text.AlignBottom
function link(url, text) {
return `<a href="${url}">${text}</a>`
}
}

View File

@ -0,0 +1,72 @@
// Copyright (c) 2022 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 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
import QtQuick.Window 2.12
import "."
T.Menu {
id: control
property ColorScheme colorScheme
implicitWidth: Math.max(
implicitBackgroundWidth + leftInset + rightInset,
contentWidth + leftPadding + rightPadding
)
implicitHeight: Math.max(
implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding
)
margins: 0
overlap: 1
delegate: MenuItem {
colorScheme: control.colorScheme
}
contentItem: Item {
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
ListView {
anchors.fill: parent
anchors.margins: 8
implicitHeight: contentHeight
model: control.contentModel
interactive: Window.window ? contentHeight > Window.window.height : false
clip: true
currentIndex: control.currentIndex
ScrollIndicator.vertical: ScrollIndicator {}
}
}
background: Rectangle {
implicitWidth: 200
implicitHeight: 40
color: colorScheme.background_norm
border.width: 1
border.color: colorScheme.border_weak
radius: Style.account_row_radius
}
}

View File

@ -0,0 +1,72 @@
// Copyright (c) 2022 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 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
import "."
T.MenuItem {
id: control
property ColorScheme colorScheme
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
padding: 6
spacing: 6
icon.width: 24
icon.height: 24
icon.color: control.enabled ? control.colorScheme.text_norm : control.colorScheme.text_disabled
font.family: Style.font_family
font.weight: Style.fontWeight_400
font.pixelSize: Style.body_font_size
font.letterSpacing: Style.body_letter_spacing
contentItem: IconLabel {
id: iconLabel
readonly property real arrowPadding: control.subMenu && control.arrow ? control.arrow.width + control.spacing : 0
readonly property real indicatorPadding: control.checkable && control.indicator ? control.indicator.width + control.spacing : 0
leftPadding: !control.mirrored ? indicatorPadding : arrowPadding
rightPadding: control.mirrored ? indicatorPadding : arrowPadding
spacing: control.spacing
mirrored: control.mirrored
display: control.display
alignment: Qt.AlignLeft
icon: control.icon
text: control.text
font: control.font
color: control.enabled ? control.colorScheme.text_norm : control.colorScheme.text_disabled
}
background: Rectangle {
implicitWidth: 164
implicitHeight: 36
radius: Style.button_radius
color: control.down ? control.colorScheme.interaction_default_active : control.highlighted ? control.colorScheme.interaction_default_hover : control.colorScheme.interaction_default
}
}

View File

@ -0,0 +1,67 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
T.Popup {
id: root
property ColorScheme colorScheme
Component.onCompleted: {
if (!ApplicationWindow.window) {
return
}
if (ApplicationWindow.window.popups === undefined) {
return
}
var obj = this
ApplicationWindow.window.popups.append( { obj } )
}
property int popupType: ApplicationWindow.PopupType.Banner
property bool shouldShow: false
readonly property var occurred: shouldShow ? new Date() : undefined
function open() {
root.shouldShow = true
}
function close() {
root.shouldShow = false
}
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
contentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding)
// TODO: Add DropShadow here
T.Overlay.modal: Rectangle {
color: root.colorScheme.backdrop_norm
}
T.Overlay.modeless: Rectangle {
color: "transparent"
}
}

View File

@ -0,0 +1,115 @@
// Copyright (c) 2022 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 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
T.RadioButton {
property ColorScheme colorScheme
property bool error: false
id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
padding: 0
spacing: 8
indicator: Rectangle {
implicitWidth: 20
implicitHeight: 20
radius: width / 2
x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
y: control.topPadding + (control.availableHeight - height) / 2
color: control.colorScheme.background_norm
border.width: 1
border.color: {
if (!control.enabled) {
return control.colorScheme.field_disabled
}
if (control.error) {
return control.colorScheme.signal_danger
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.field_norm
}
Rectangle {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: 8
height: 8
radius: width / 2
color: {
if (!control.enabled) {
return control.colorScheme.field_disabled
}
if (control.error) {
return control.colorScheme.signal_danger
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.interaction_norm
}
visible: control.checked
}
}
contentItem: CheckLabel {
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
color: {
if (!enabled) {
return control.colorScheme.text_disabled
}
if (error) {
return control.colorScheme.signal_danger
}
return control.colorScheme.text_norm
}
font.family: Style.font_family
font.weight: Style.fontWeight_400
font.pixelSize: Style.body_font_size
lineHeight: Style.body_line_height
lineHeightMode: Text.FixedHeight
font.letterSpacing: Style.body_letter_spacing
}
}

View File

@ -0,0 +1,394 @@
// Copyright (c) 2022 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/>.
pragma Singleton
import QtQml 2.13
import QtQuick 2.12
import "./"
// https://wiki.qt.io/Qml_Styling
// http://imaginativethinking.ca/make-qml-component-singleton/
QtObject {
id: root
// TODO: Once we will use Qt >=5.15 this should be refactored with inline components as follows:
// https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components
// component ColorScheme: QtObject {
// property color primay_norm
// ...
// }
property ColorScheme lightStyle: ColorScheme {
id: _lightStyle
prominent: lightProminentStyle
// Primary
primay_norm: "#6D4AFF"
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_hover: "#4D34B3"
interaction_norm_active: "#372580"
// Text
text_norm: "#0C0C14"
text_weak: "#706D6B"
text_hint: "#8F8D8A"
text_disabled: "#C2BFBC"
text_invert: "#FFFFFF"
// Field
field_norm: "#ADABA8"
field_hover: "#8F8D8A"
field_disabled: "#D1CFCD"
// Border
border_norm: "#D1CFCD"
border_weak: "#EAE7E4"
// Background
background_norm: "#FFFFFF"
background_weak: "#F5F4F2"
background_strong: "#EAE7E4"
background_avatar: "#C2BFBC"
// Interaction-weak
interaction_weak: "#D1CFCD"
interaction_weak_hover: "#C2BFBC"
interaction_weak_active: "#A8A6A3"
// Interaction-default
interaction_default: Qt.rgba(0,0,0,0)
interaction_default_hover: Qt.rgba(194./255., 191./255., 188./255., 0.2)
interaction_default_active: Qt.rgba(194./255., 191./255., 188./255., 0.4)
// Scrollbar
scrollbar_norm: "#D1CFCD"
scrollbar_hover: "#C2BFBC"
// Signal
signal_danger: "#DC3251"
signal_danger_hover: "#F74F6D"
signal_danger_active: "#B72346"
signal_warning: "#FF9900"
signal_warning_hover: "#FFB800"
signal_warning_active: "#FF851A"
signal_success: "#1EA885"
signal_success_hover: "#23C299"
signal_success_active: "#198F71"
signal_info: "#239ECE"
signal_info_hover: "#27B1E8"
signal_info_active: "#1F83B5"
// Shadows
shadow_norm: Qt.rgba(0,0,0, 0.1) // #000000 10% x:0 y:1 blur:4
shadow_lifted: Qt.rgba(0,0,0, 0.16) // #000000 16% x:0 y:8 blur:24
// Backdrop
backdrop_norm: Qt.rgba(12./255., 12./255., 20./255., 0.32)
// Images
welcome_img: "icons/img-welcome.png"
logo_img: "icons/product_logos.svg"
}
property ColorScheme lightProminentStyle: ColorScheme {
id: _lightProminentStyle
prominent: this
// Primary
primay_norm: "#8A6EFF"
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_hover: "#7C5CFF"
interaction_norm_active: "#8A6EFF"
// Text
text_norm: "#FFFFFF"
text_weak: "#9282D4"
text_hint: "#544399"
text_disabled: "#4A398F"
text_invert: "#1B1340"
// Field
field_norm: "#9282D4"
field_hover: "#7C5CFF"
field_disabled: "#38277A"
// Border
border_norm: "#413085"
border_weak: "#3C2B80"
// Background
background_norm: "#1B1340"
background_weak: "#271C57"
background_strong: "#38277A"
background_avatar: "#6D4AFF"
// Interaction-weak
interaction_weak: "#4A398F"
interaction_weak_hover: "#6D4AFF"
interaction_weak_active: "#8A6EFF"
// Interaction-default
interaction_default: Qt.rgba(0,0,0,0)
interaction_default_hover: Qt.rgba(68./255., 78./255., 114./255., 0.2)
interaction_default_active: Qt.rgba(68./255., 78./255., 114./255., 0.3)
// Scrollbar
scrollbar_norm: "#413085"
scrollbar_hover: "#4A398F"
// Signal
signal_danger: "#F5385A"
signal_danger_hover: "#FF5473"
signal_danger_active: "#DC3251"
signal_warning: "#FF9900"
signal_warning_hover: "#FFB800"
signal_warning_active: "#FF8419"
signal_success: "#1EA885"
signal_success_hover: "#23C299"
signal_success_active: "#198F71"
signal_info: "#2C89DB"
signal_info_hover: "#3491E3"
signal_info_active: "#1F83B5"
// Shadows
shadow_norm: Qt.rgba(0,0,0, 0.32) // #000000 32% x:0 y:1 blur:4
shadow_lifted: Qt.rgba(0,0,0, 0.40) // #000000 40% x:0 y:8 blur:24
// Backdrop
backdrop_norm: Qt.rgba(0,0,0, 0.32)
// Images
welcome_img: "icons/img-welcome-dark.png"
logo_img: "icons/product_logos_dark.svg"
}
property ColorScheme darkStyle: ColorScheme {
id: _darkStyle
prominent: darkProminentStyle
// Primary
primay_norm: "#8A6EFF"
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_hover: "#7C5CFF"
interaction_norm_active: "#8A6EFF"
// Text
text_norm: "#FFFFFF"
text_weak: "#A7A4B5"
text_hint: "#6D697D"
text_disabled: "#5B576B"
text_invert: "#1C1B24"
// Field
field_norm: "#5B576B"
field_hover: "#6D697D"
field_disabled: "#3F3B4C"
// Border
border_norm: "#4A4658"
border_weak: "#343140"
// Background
background_norm: "#1C1B24"
background_weak: "#292733"
background_strong: "#3F3B4C"
background_avatar: "#6D4AFF"
// Interaction-weak
interaction_weak: "#4A4658"
interaction_weak_hover: "#5B576B"
interaction_weak_active: "#6D697D"
// Interaction-default
interaction_default: "#00000000"
interaction_default_hover: Qt.rgba(91./255.,87./255.,107./255.,0.2)
interaction_default_active: Qt.rgba(91./255.,87./255.,107./255.,0.4)
// Scrollbar
scrollbar_norm: "#4A4658"
scrollbar_hover: "#5B576B"
// Signal
signal_danger: "#F5385A"
signal_danger_hover: "#FF5473"
signal_danger_active: "#DC3251"
signal_warning: "#FF9900"
signal_warning_hover: "#FFB800"
signal_warning_active: "#FF8419"
signal_success: "#1EA885"
signal_success_hover: "#23C299"
signal_success_active: "#198F71"
signal_info: "#239ECE"
signal_info_hover: "#27B1E8"
signal_info_active: "#1F83B5"
// Shadows
shadow_norm: Qt.rgba(0,0,0,0.4) // #000000 40% x+0 y+1 blur:4
shadow_lifted: Qt.rgba(0,0,0,0.48) // #000000 48% x+0 y+8 blur:24
// Backdrop
backdrop_norm: Qt.rgba(0,0,0,0.32)
// Images
welcome_img: "icons/img-welcome-dark.png"
logo_img: "icons/product_logos_dark.svg"
}
property ColorScheme darkProminentStyle: ColorScheme {
id: _darkProminentStyle
prominent: this
// Primary
primay_norm: "#8A6EFF"
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_hover: "#7C5CFF"
interaction_norm_active: "#8A6EFF"
// Text
text_norm: "#FFFFFF"
text_weak: "#A7A4B5"
text_hint: "#6D697D"
text_disabled: "#5B576B"
text_invert: "#1C1B24"
// Field
field_norm: "#5B576B"
field_hover: "#6D697D"
field_disabled: "#3F3B4C"
// Border
border_norm: "#4A4658"
border_weak: "#343140"
// Background
background_norm: "#16141c"
background_weak: "#292733"
background_strong: "#3F3B4C"
background_avatar: "#6D4AFF"
// Interaction-weak
interaction_weak: "#4A4658"
interaction_weak_hover: "#5B576B"
interaction_weak_active: "#6D697D"
// Interaction-default
interaction_default: "#00000000"
interaction_default_hover: Qt.rgba(91./255.,87./255.,107./255.,0.2)
interaction_default_active: Qt.rgba(91./255.,87./255.,107./255.,0.4)
// Scrollbar
scrollbar_norm: "#4A4658"
scrollbar_hover: "#5B576B"
// Signal
signal_danger: "#F5385A"
signal_danger_hover: "#FF5473"
signal_danger_active: "#DC3251"
signal_warning: "#FF9900"
signal_warning_hover: "#FFB800"
signal_warning_active: "#FF8419"
signal_success: "#1EA885"
signal_success_hover: "#23C299"
signal_success_active: "#198F71"
signal_info: "#239ECE"
signal_info_hover: "#27B1E8"
signal_info_active: "#1F83B5"
// Shadows
shadow_norm: Qt.rgba(0,0,0,0.4) // #000000 40% x+0 y+1 blur:4
shadow_lifted: Qt.rgba(0,0,0,0.48) // #000000 48% x+0 y+8 blur:24
// Backdrop
backdrop_norm: Qt.rgba(0,0,0,0.32)
// Images
welcome_img: "icons/img-welcome-dark.png"
logo_img: "icons/product_logos_dark.svg"
}
property ColorScheme currentStyle: lightStyle
property string font_family: {
switch (Qt.platform.os) {
case "windows":
return "Segoe UI"
case "osx":
return "SF Pro Display"
case "linux":
return "Ubuntu"
default:
console.error("Unknown platform")
}
}
property real px : 1.00 // px
property real input_radius : 8 * root.px // px
property real button_radius : 8 * root.px // px
property real checkbox_radius : 4 * root.px // px
property real avatar_radius : 8 * root.px // px
property real big_avatar_radius : 12 * root.px // px
property real account_hover_radius : 12 * root.px // px
property real account_row_radius : 12 * root.px // px
property real context_item_radius : 8 * root.px // px
property real banner_radius : 12 * root.px // px
property real dialog_radius : 12 * root.px // px
property real card_radius : 12 * root.px // px
property real storage_bar_radius : 3 * root.px // px
property real tooltip_radius : 8 * root.px // px
property int heading_font_size: 28
property int heading_line_height: 36
property int title_font_size: 20
property int title_line_height: 24
property int lead_font_size: 18
property int lead_line_height: 26
property int body_font_size: 14
property int body_line_height: 20
property real body_letter_spacing: 0.2 * root.px
property int caption_font_size: 12
property int caption_line_height: 16
property real caption_letter_spacing: 0.4 * root.px
property int fontWeight_100: Font.Thin
property int fontWeight_200: Font.Light
property int fontWeight_300: Font.ExtraLight
property int fontWeight_400: Font.Normal
property int fontWeight_500: Font.Medium
property int fontWeight_600: Font.DemiBold
property int fontWeight_700: Font.Bold
property int fontWeight_800: Font.ExtraBold
property int fontWeight_900: Font.Black
}

View File

@ -0,0 +1,150 @@
// Copyright (c) 2022 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 2.12
import QtQuick.Templates 2.12 as T
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
T.Switch {
property ColorScheme colorScheme
property bool loading: false
// TODO: store previous enabled state and restore it?
// For now assuming that only enabled buttons could have loading state
onLoadingChanged: {
if (loading) {
enabled = false
} else {
enabled = true
}
}
id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
padding: 0
spacing: 7
indicator: Rectangle {
implicitWidth: 40
implicitHeight: 24
x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
y: control.topPadding + (control.availableHeight - height) / 2
radius: height / 2.
color: control.enabled || control.loading ? control.colorScheme.background_norm : control.colorScheme.background_strong
border.width: control.enabled && !loading ? 1 : 0
border.color: control.hovered ? control.colorScheme.field_hover : control.colorScheme.field_norm
Rectangle {
x: Math.max(0, Math.min(parent.width - width, control.visualPosition * parent.width - (width / 2)))
y: (parent.height - height) / 2
width: 24
height: 24
radius: parent.radius
visible: !loading
color: {
if (!control.enabled) {
return control.colorScheme.field_disabled
}
if (control.checked) {
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.interaction_norm
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.field_hover
}
return control.colorScheme.field_norm
}
ColorImage {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: 16
height: 16
sourceSize.width: 16
sourceSize.height: 16
color: "#FFFFFF"
source: "../icons/ic-check.svg"
visible: control.checked
}
Behavior on x {
enabled: !control.down
SmoothedAnimation { velocity: 200 }
}
}
ColorImage {
id: loadingImage
x: parent.width - width
y: (parent.height - height) / 2
width: 18
height: 18
sourceSize.width: 18
sourceSize.height: 18
color: control.colorScheme.interaction_norm_hover
source: "../icons/Loader_16.svg"
visible: control.loading
RotationAnimation {
target: loadingImage
loops: Animation.Infinite
duration: 1000
from: 0
to: 360
direction: RotationAnimation.Clockwise
running: control.loading
}
}
}
contentItem: CheckLabel {
id: label
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
color: control.enabled || control.loading ? control.colorScheme.text_norm : control.colorScheme.text_disabled
font.family: Style.font_family
font.weight: Style.fontWeight_400
font.pixelSize: Style.body_font_size
lineHeight: Style.body_line_height
lineHeightMode: Text.FixedHeight
font.letterSpacing: Style.body_letter_spacing
}
}

View File

@ -0,0 +1,370 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
import QtQuick.Layouts 1.12
import "." as Proton
FocusScope {
id: root
property ColorScheme colorScheme
property alias background: control.background
property alias bottomInset: control.bottomInset
//property alias flickable: control.flickable
property alias focusReason: control.focusReason
property alias hoverEnabled: control.hoverEnabled
property alias hovered: control.hovered
property alias implicitBackgroundHeight: control.implicitBackgroundHeight
property alias implicitBackgroundWidth: control.implicitBackgroundWidth
property alias leftInset: control.leftInset
property alias palette: control.palette
property alias placeholderText: control.placeholderText
property alias placeholderTextColor: control.placeholderTextColor
property alias rightInset: control.rightInset
property alias topInset: control.topInset
property alias activeFocusOnPress: control.activeFocusOnPress
property alias baseUrl: control.baseUrl
property alias bottomPadding: control.bottomPadding
property alias canPaste: control.canPaste
property alias canRedo: control.canRedo
property alias canUndo: control.canUndo
property alias color: control.color
property alias contentHeight: control.contentHeight
property alias contentWidth: control.contentWidth
property alias cursorDelegate: control.cursorDelegate
property alias cursorPosition: control.cursorPosition
property alias cursorRectangle: control.cursorRectangle
property alias cursorVisible: control.cursorVisible
property alias effectiveHorizontalAlignment: control.effectiveHorizontalAlignment
property alias font: control.font
property alias horizontalAlignment: control.horizontalAlignment
property alias hoveredLink: control.hoveredLink
property alias inputMethodComposing: control.inputMethodComposing
property alias inputMethodHints: control.inputMethodHints
property alias leftPadding: control.leftPadding
property alias length: control.length
property alias lineCount: control.lineCount
property alias mouseSelectionMode: control.mouseSelectionMode
property alias overwriteMode: control.overwriteMode
property alias padding: control.padding
property alias persistentSelection: control.persistentSelection
property alias preeditText: control.preeditText
property alias readOnly: control.readOnly
property alias renderType: control.renderType
property alias rightPadding: control.rightPadding
property alias selectByKeyboard: control.selectByKeyboard
property alias selectByMouse: control.selectByMouse
property alias selectedText: control.selectedText
property alias selectedTextColor: control.selectedTextColor
property alias selectionColor: control.selectionColor
property alias selectionEnd: control.selectionEnd
property alias selectionStart: control.selectionStart
property alias tabStopDistance: control.tabStopDistance
property alias text: control.text
property alias textDocument: control.textDocument
property alias textFormat: control.textFormat
property alias textMargin: control.textMargin
property alias topPadding: control.topPadding
// We are using our own type of validators. It should be a function
// returning an error string in case of error and undefined if no error
property var validator
property alias verticalAlignment: control.verticalAlignment
property alias wrapMode: control.wrapMode
implicitWidth: children[0].implicitWidth
implicitHeight: children[0].implicitHeight
property alias label: label.text
property alias hint: hint.text
property string assistiveText
property string errorString
property bool error: false
signal editingFinished()
function append(text) { return control.append(text) }
function clear() { return control.clear() }
function copy() { return control.copy() }
function cut() { return control.cut() }
function deselect() { return control.deselect() }
function getFormattedText(start, end) { return control.getFormattedText(start, end) }
function getText(start, end) { return control.getText(start, end) }
function insert(position, text) { return control.insert(position, text) }
function isRightToLeft(start, end) { return control.isRightToLeft(start, end) }
function linkAt(x, y) { return control.linkAt(x, y) }
function moveCursorSelection(position, mode) { return control.moveCursorSelection(position, mode) }
function paste() { return control.paste() }
function positionAt(x, y) { return control.positionAt(x, y) }
function positionToRectangle(position) { return control.positionToRectangle(position) }
function redo() { return control.redo() }
function remove(start, end) { return control.remove(start, end) }
function select(start, end) { return control.select(start, end) }
function selectAll() { return control.selectAll() }
function selectWord() { return control.selectWord() }
function undo() { return control.undo() }
// Calculates the height of the component to make exactly lineNum visible in edit area
function heightForLinesVisible(lineNum) {
var totalHeight = 0
totalHeight += headerLayout.height
totalHeight += footerLayout.height
totalHeight += control.topPadding + control.bottomPadding
totalHeight += lineNum * fontMetrics.height
return totalHeight
}
FontMetrics {
id: fontMetrics
font: control.font
}
ColumnLayout {
anchors.fill: parent
spacing: 0
RowLayout {
id: headerLayout
Layout.fillWidth: true
spacing: 0
Proton.Label {
colorScheme: root.colorScheme
id: label
Layout.fillWidth: true
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
type: Proton.Label.LabelType.Body_semibold
}
Proton.Label {
colorScheme: root.colorScheme
id: hint
Layout.fillWidth: true
color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled
horizontalAlignment: Text.AlignRight
type: Proton.Label.LabelType.Caption
}
}
ScrollView {
id: controlView
Layout.fillHeight: true
Layout.fillWidth: true
clip: true
T.TextArea {
id: control
implicitWidth: Math.max(
contentWidth + leftPadding + rightPadding,
implicitBackgroundWidth + leftInset + rightInset,
placeholder.implicitWidth + leftPadding + rightPadding
)
implicitHeight: Math.max(
contentHeight + topPadding + bottomPadding,
implicitBackgroundHeight + topInset + bottomInset,
placeholder.implicitHeight + topPadding + bottomPadding
)
topPadding: 8
bottomPadding: 8
leftPadding: 12
rightPadding: 12
font.family: Style.font_family
font.weight: Style.fontWeight_400
font.pixelSize: Style.body_font_size
font.letterSpacing: Style.body_letter_spacing
color: control.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
selectionColor: control.palette.highlight
selectedTextColor: control.palette.highlightedText
onEditingFinished: root.editingFinished()
wrapMode: TextInput.Wrap
// enforcing default focus here within component
focus: root.focus
KeyNavigation.priority: root.KeyNavigation.priority
KeyNavigation.backtab: root.KeyNavigation.backtab
KeyNavigation.tab: root.KeyNavigation.tab
KeyNavigation.up: root.KeyNavigation.up
KeyNavigation.down: root.KeyNavigation.down
KeyNavigation.left: root.KeyNavigation.left
KeyNavigation.right: root.KeyNavigation.right
selectByMouse: true
cursorDelegate: Rectangle {
id: cursor
width: 1
color: root.colorScheme.interaction_norm
visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd
Connections {
target: control
onCursorPositionChanged: {
// keep a moving cursor visible
cursor.opacity = 1
timer.restart()
}
}
Timer {
id: timer
running: control.activeFocus && !control.readOnly
repeat: true
interval: Qt.styleHints.cursorFlashTime / 2
onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0
// force the cursor visible when gaining focus
onRunningChanged: cursor.opacity = 1
}
}
PlaceholderText {
id: placeholder
x: control.leftPadding
y: control.topPadding
width: control.width - (control.leftPadding + control.rightPadding)
height: control.height - (control.topPadding + control.bottomPadding)
text: control.placeholderText
font: control.font
color: control.placeholderTextColor
verticalAlignment: control.verticalAlignment
visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter)
elide: Text.ElideRight
renderType: control.renderType
}
background: Rectangle {
anchors.fill: parent
radius: Style.input_radius
visible: true
color: root.colorScheme.background_norm
border.color: {
if (!control.enabled) {
return root.colorScheme.field_disabled
}
if (control.activeFocus) {
return root.colorScheme.interaction_norm
}
if (root.error) {
return root.colorScheme.signal_danger
}
if (control.hovered) {
return root.colorScheme.field_hover
}
return root.colorScheme.field_norm
}
border.width: 1
}
}
}
RowLayout {
id: footerLayout
Layout.fillWidth: true
spacing: 0
ColorImage {
id: errorIcon
Layout.rightMargin: 4
visible: root.error && (assistiveText.text.length > 0)
source: "../icons/ic-exclamation-circle-filled.svg"
color: root.colorScheme.signal_danger
height: assistiveText.height
sourceSize.height: assistiveText.height
}
Proton.Label {
colorScheme: root.colorScheme
id: assistiveText
Layout.fillWidth: true
text: root.error ? root.errorString : root.assistiveText
color: {
if (!root.enabled) {
return root.colorScheme.text_disabled
}
if (root.error) {
return root.colorScheme.signal_danger
}
return root.colorScheme.text_weak
}
type: root.error ? Proton.Label.LabelType.Caption_semibold : Proton.Label.LabelType.Caption
}
}
}
property bool validateOnEditingFinished: true
onEditingFinished: {
if (!validateOnEditingFinished) {
return
}
validate()
}
function validate() {
if (validator === undefined) {
return
}
var error = validator(text)
if (error) {
root.error = true
root.errorString = error
} else {
root.error = false
root.errorString = ""
}
}
onTextChanged: {
root.error = false
root.errorString = ""
}
}

View File

@ -0,0 +1,381 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
import QtQuick.Layouts 1.12
import "." as Proton
FocusScope {
id: root
property ColorScheme colorScheme
property alias background: control.background
property alias bottomInset: control.bottomInset
property alias focusReason: control.focusReason
property alias hoverEnabled: control.hoverEnabled
property alias hovered: control.hovered
property alias implicitBackgroundHeight: control.implicitBackgroundHeight
property alias implicitBackgroundWidth: control.implicitBackgroundWidth
property alias leftInset: control.leftInset
property alias palette: control.palette
property alias placeholderText: control.placeholderText
property alias placeholderTextColor: control.placeholderTextColor
property alias rightInset: control.rightInset
property alias topInset: control.topInset
property alias acceptableInput: control.acceptableInput
property alias activeFocusOnPress: control.activeFocusOnPress
property alias autoScroll: control.autoScroll
property alias bottomPadding: control.bottomPadding
property alias canPaste: control.canPaste
property alias canRedo: control.canRedo
property alias canUndo: control.canUndo
property alias color: control.color
//property alias contentHeight: control.contentHeight
//property alias contentWidth: control.contentWidth
property alias cursorDelegate: control.cursorDelegate
property alias cursorPosition: control.cursorPosition
property alias cursorRectangle: control.cursorRectangle
property alias cursorVisible: control.cursorVisible
property alias displayText: control.displayText
property alias effectiveHorizontalAlignment: control.effectiveHorizontalAlignment
property alias font: control.font
property alias horizontalAlignment: control.horizontalAlignment
property alias inputMask: control.inputMask
property alias inputMethodComposing: control.inputMethodComposing
property alias inputMethodHints: control.inputMethodHints
property alias leftPadding: control.leftPadding
property alias length: control.length
property alias maximumLength: control.maximumLength
property alias mouseSelectionMode: control.mouseSelectionMode
property alias overwriteMode: control.overwriteMode
property alias padding: control.padding
property alias passwordCharacter: control.passwordCharacter
property alias passwordMaskDelay: control.passwordMaskDelay
property alias persistentSelection: control.persistentSelection
property alias preeditText: control.preeditText
property alias readOnly: control.readOnly
property alias renderType: control.renderType
property alias rightPadding: control.rightPadding
property alias selectByMouse: control.selectByMouse
property alias selectedText: control.selectedText
property alias selectedTextColor: control.selectedTextColor
property alias selectionColor: control.selectionColor
property alias selectionEnd: control.selectionEnd
property alias selectionStart: control.selectionStart
property alias text: control.text
// We are using our own type of validators. It should be a function
// returning an error string in case of error and undefined if no error
property var validator
property alias verticalAlignment: control.verticalAlignment
property alias wrapMode: control.wrapMode
implicitWidth: children[0].implicitWidth
implicitHeight: children[0].implicitHeight
property alias label: label.text
property alias hint: hint.text
property string assistiveText
property string errorString
property int echoMode: TextInput.Normal
property bool error: false
signal accepted()
signal editingFinished()
signal textEdited()
function clear() { control.clear() }
function copy() { control.copy() }
function cut() { control.cut() }
function deselect() { control.deselect() }
function ensureVisible(position) { control.ensureVisible(position) }
function getText(start, end) { control.getText(start, end) }
function insert(position, text) { control.insert(position, text) }
function isRightToLeft(start, end) { control.isRightToLeft(start, end) }
function moveCursorSelection(position, mode) { control.moveCursorSelection(position, mode) }
function paste() { control.paste() }
function positionAt(x, y, position) { control.positionAt(x, y, position) }
function positionToRectangle(pos) { control.positionToRectangle(pos) }
function redo() { control.redo() }
function remove(start, end) { control.remove(start, end) }
function select(start, end) { control.select(start, end) }
function selectAll() { control.selectAll() }
function selectWord() { control.selectWord() }
function undo() { control.undo() }
function forceActiveFocus() { control.forceActiveFocus() }
ColumnLayout {
anchors.fill: parent
spacing: 0
RowLayout {
Layout.fillWidth: true
spacing: 0
Proton.Label {
colorScheme: root.colorScheme
id: label
Layout.fillHeight: true
Layout.fillWidth: true
type: Proton.Label.LabelType.Body_semibold
}
Proton.Label {
colorScheme: root.colorScheme
id: hint
Layout.fillHeight: true
Layout.fillWidth: true
color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled
horizontalAlignment: Text.AlignRight
type: Proton.Label.LabelType.Caption
}
}
// Background is moved away from within control to cover eye button as well.
// In case it will remain as control background property - control's width
// will be adjusted to background's width making text field and eye button overlap
Rectangle {
id: background
Layout.fillHeight: true
Layout.fillWidth: true
radius: Style.input_radius
visible: true
color: root.colorScheme.background_norm
border.color: {
if (!control.enabled) {
return root.colorScheme.field_disabled
}
if (control.activeFocus) {
return root.colorScheme.interaction_norm
}
if (root.error) {
return root.colorScheme.signal_danger
}
if (control.hovered) {
return root.colorScheme.field_hover
}
return root.colorScheme.field_norm
}
border.width: 1
implicitWidth: children[0].implicitWidth
implicitHeight: children[0].implicitHeight
RowLayout {
anchors.fill: parent
spacing: 0
T.TextField {
id: control
Layout.fillHeight: true
Layout.fillWidth: true
implicitWidth: implicitBackgroundWidth + leftInset + rightInset
|| Math.max(contentWidth, placeholder.implicitWidth) + leftPadding + rightPadding
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding,
placeholder.implicitHeight + topPadding + bottomPadding)
topPadding: 8
bottomPadding: 8
leftPadding: 12
rightPadding: 12
font.family: Style.font_family
font.weight: Style.fontWeight_400
font.pixelSize: Style.body_font_size
font.letterSpacing: Style.body_letter_spacing
color: control.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
selectionColor: control.palette.highlight
selectedTextColor: control.palette.highlightedText
verticalAlignment: TextInput.AlignVCenter
echoMode: eyeButton.checked ? TextInput.Normal : root.echoMode
// enforcing default focus here within component
focus: true
KeyNavigation.priority: root.KeyNavigation.priority
KeyNavigation.backtab: root.KeyNavigation.backtab
KeyNavigation.tab: root.KeyNavigation.tab
KeyNavigation.up: root.KeyNavigation.up
KeyNavigation.down: root.KeyNavigation.down
KeyNavigation.left: root.KeyNavigation.left
KeyNavigation.right: root.KeyNavigation.right
selectByMouse: true
cursorDelegate: Rectangle {
id: cursor
width: 1
color: root.colorScheme.interaction_norm
visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd
Connections {
target: control
onCursorPositionChanged: {
// keep a moving cursor visible
cursor.opacity = 1
timer.restart()
}
}
Timer {
id: timer
running: control.activeFocus && !control.readOnly
repeat: true
interval: Qt.styleHints.cursorFlashTime / 2
onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0
// force the cursor visible when gaining focus
onRunningChanged: cursor.opacity = 1
}
}
PlaceholderText {
id: placeholder
x: control.leftPadding
y: control.topPadding
width: control.width - (control.leftPadding + control.rightPadding)
height: control.height - (control.topPadding + control.bottomPadding)
text: control.placeholderText
font: control.font
color: control.placeholderTextColor
verticalAlignment: control.verticalAlignment
visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter)
elide: Text.ElideRight
renderType: control.renderType
}
background: Item {
implicitWidth: 80
implicitHeight: 36
visible: false
}
onAccepted: {
root.accepted()
}
onEditingFinished: {
root.editingFinished()
}
onTextEdited: {
root.textEdited()
}
}
Proton.Button {
colorScheme: root.colorScheme
id: eyeButton
Layout.fillHeight: true
visible: root.echoMode === TextInput.Password
icon.color: control.color
checkable: true
icon.source: checked ? "../icons/ic-eye-slash.svg" : "../icons/ic-eye.svg"
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 0
ColorImage {
id: errorIcon
Layout.rightMargin: 4
visible: root.error && (assistiveText.text.length > 0)
source: "../icons/ic-exclamation-circle-filled.svg"
color: root.colorScheme.signal_danger
height: assistiveText.height
sourceSize.height: assistiveText.height
}
Proton.Label {
colorScheme: root.colorScheme
id: assistiveText
Layout.fillHeight: true
Layout.fillWidth: true
text: root.error ? root.errorString : root.assistiveText
color: {
if (!root.enabled) {
return root.colorScheme.text_disabled
}
if (root.error) {
return root.colorScheme.signal_danger
}
return root.colorScheme.text_weak
}
type: root.error ? Proton.Label.LabelType.Caption_semibold : Proton.Label.LabelType.Caption
}
}
}
property bool validateOnEditingFinished: true
onEditingFinished: {
if (!validateOnEditingFinished) {
return
}
validate()
}
function validate() {
if (validator === undefined) {
return
}
var error = validator(text)
if (error) {
root.error = true
root.errorString = error
} else {
root.error = false
root.errorString = ""
}
}
onTextChanged: {
root.error = false
root.errorString = ""
}
}

View File

@ -0,0 +1,113 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
Item {
id: root
property var colorScheme
property bool checked
property bool hovered
property bool loading
signal clicked
property bool _disabled: !enabled
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
Rectangle {
id: indicator
implicitWidth: 40
implicitHeight: 24
radius: width/2
color: {
if (root.loading) return "transparent"
if (root._disabled) return root.colorScheme.background_strong
return root.colorScheme.background_norm
}
border {
width: 1
color: (root._disabled || root.loading) ? "transparent" : colorScheme.field_norm
}
Rectangle {
anchors.verticalCenter: indicator.verticalCenter
anchors.left: indicator.left
anchors.leftMargin: root.checked ? 16 : 0
width: 24
height: 24
radius: width/2
color: {
if (root.loading) return "transparent"
if (root._disabled) return root.colorScheme.field_disabled
if (root.checked) {
if (root.hovered) return root.colorScheme.interaction_norm_hover
return root.colorScheme.interaction_norm
} else {
if (root.hovered) return root.colorScheme.field_hover
return root.colorScheme.field_norm
}
}
ColorImage {
anchors.centerIn: parent
source: "../icons/ic-check.svg"
color: root.colorScheme.background_norm
height: root.colorScheme.body_font_size
sourceSize.height: root.colorScheme.body_font_size
visible: root.checked
}
}
ColorImage {
id: loader
anchors.centerIn: parent
source: "../icons/Loader_16.svg"
color: root.colorScheme.text_norm
height: root.colorScheme.body_font_size
sourceSize.height: root.colorScheme.body_font_size
visible: root.loading
RotationAnimation {
target: loader
loops: Animation.Infinite
duration: 1000
from: 0
to: 360
direction: RotationAnimation.Clockwise
running: root.loading
}
}
MouseArea {
anchors.fill: indicator
hoverEnabled: true
onEntered: {root.hovered = true }
onExited: {root.hovered = false }
onClicked: { if (root.enabled) root.clicked();}
onPressed: {root.hovered = true }
onReleased: { root.hovered = containsMouse }
}
}
}

View File

@ -0,0 +1,37 @@
# Copyright (c) 2021 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/>.
module QQtQuick.Controls.Proton
depends QtQuick.Controls 2.12
singleton ProtonStyle 4.0 Style.qml
ColorScheme 4.0 ColorScheme.qml
ApplicationWindow 4.0 ApplicationWindow.qml
Button 4.0 Button.qml
CheckBox 4.0 CheckBox.qml
ComboBox 4.0 ComboBox.qml
Dialog 4.0 Dialog.qml
Label 4.0 Label.qml
Menu 4.0 Menu.qml
MenuItem 4.0 MenuItem.qml
Popup 4.0 Popup.qml
RadioButton 4.0 RadioButton.qml
Switch 4.0 Switch.qml
TextArea 4.0 TextArea.qml
TextField 4.0 TextField.qml
Toggle 4.0 Toggle.qml

View File

@ -0,0 +1,120 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
import Proton 4.0
SettingsView {
id: root
fillHeight: false
Label {
colorScheme: root.colorScheme
text: qsTr("SMTP connection mode")
type: Label.Heading
Layout.fillWidth: true
}
Label {
colorScheme: root.colorScheme
text: qsTr("Changes require reconfiguration of email client. Bridge will automatically restart.")
type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
Layout.maximumWidth: this.parent.Layout.maximumWidth
wrapMode: Text.WordWrap
}
ColumnLayout {
spacing: 16
ButtonGroup{ id: protocolSelection }
Label {
colorScheme: root.colorScheme
text: qsTr("SMTP connection security")
}
RadioButton {
id: sslButton
colorScheme: root.colorScheme
ButtonGroup.group: protocolSelection
text: qsTr("SSL")
}
RadioButton {
id: starttlsButton
colorScheme: root.colorScheme
ButtonGroup.group: protocolSelection
text: qsTr("STARTTLS")
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak
}
RowLayout {
spacing: 12
Button {
id: submitButton
colorScheme: root.colorScheme
text: qsTr("Save and restart")
onClicked: {
submitButton.loading = true
root.submit()
}
enabled: sslButton.checked !== root.backend.useSSLforSMTP
}
Button {
colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true
}
Connections {
target: root.backend
onToggleUseSSLFinished: submitButton.loading = false
}
}
function submit(){
submitButton.loading = true
root.backend.toggleUseSSLforSMTP(sslButton.checked)
}
function setDefaultValues(){
sslButton.checked = root.backend.useSSLforSMTP
starttlsButton.checked = !root.backend.useSSLforSMTP
}
onVisibleChanged: {
root.setDefaultValues()
}
}

View File

@ -0,0 +1,118 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
Item {
id: root
property var colorScheme
property string text: "Text"
property string actionText: "Action"
property string actionIcon: ""
property string description: "Lorem ipsum dolor sit amet"
property alias descriptionWrap: descriptionLabel.wrapMode
property var type: SettingsItem.ActionType.Toggle
property bool checked: true
property bool loading: false
property bool showSeparator: true
property var _bottomMargin: 20
property var _lineWidth: 1
property var _toggleTopMargin: 6
signal clicked
enum ActionType {
Toggle = 1, Button = 2, PrimaryButton = 3
}
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
RowLayout {
anchors.fill: parent
spacing: 16
ColumnLayout {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.bottomMargin: root._bottomMargin
spacing: 4
Label {
id: mainLabel
colorScheme: root.colorScheme
text: root.text
type: Label.Body_semibold
}
Label {
id: descriptionLabel
Layout.fillHeight: true
Layout.fillWidth: true
Layout.preferredWidth: parent.width
wrapMode: Text.WordWrap
colorScheme: root.colorScheme
text: root.description
color: root.colorScheme.text_weak
}
}
Toggle {
Layout.alignment: Qt.AlignTop
Layout.topMargin: root._toggleTopMargin
id: toggle
colorScheme: root.colorScheme
visible: root.type === SettingsItem.ActionType.Toggle
checked: root.checked
loading: root.loading
onClicked: { if (!root.loading) root.clicked() }
}
Button {
Layout.alignment: Qt.AlignTop
id: button
colorScheme: root.colorScheme
visible: root.type === SettingsItem.Button || root.type === SettingsItem.PrimaryButton
text: root.actionText + (root.actionIcon != "" ? " " : "")
loading: root.loading
icon.source: root.actionIcon
onClicked: { if (!root.loading) root.clicked() }
secondary: root.type !== SettingsItem.PrimaryButton
}
}
Rectangle {
anchors.left: root.left
anchors.right: root.right
anchors.bottom: root.bottom
color: colorScheme.border_weak
height: root._lineWidth
visible: root.showSeparator
}
}

View File

@ -0,0 +1,99 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
import Proton 4.0
Item {
id: root
property var colorScheme
property var backend
default property alias items: content.children
signal back()
property int _leftMargin: 64
property int _rightMargin: 64
property int _topMargin: 32
property int _bottomMargin: 32
property int _spacing: 20
// fillHeight indicates whether the SettingsView should fill all available explicit height set
property bool fillHeight: false
ScrollView {
id: scrollView
clip: true
anchors.fill: parent
Item {
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
width: scrollView.availableWidth
height: scrollView.availableHeight
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
// do not set implicitWidth because implicit width of ColumnLayout will be equal to maximum implicit width of
// internal items. And if one of internal items would be a Text or Label - implicit width of those is always
// equal to non-wrapped text (i.e. one line only). That will lead to enabling horizontal scroll when not needed
implicitWidth: width
ColumnLayout {
anchors.fill: parent
spacing: 0
ColumnLayout {
id: content
spacing: root._spacing
Layout.fillWidth: true
Layout.topMargin: root._topMargin
Layout.bottomMargin: root._bottomMargin
Layout.leftMargin: root._leftMargin
Layout.rightMargin: root._rightMargin
}
Item {
id: filler
Layout.fillHeight: true
visible: !root.fillHeight
}
}
}
}
Button {
id: backButton
anchors {
top: parent.top
left: parent.left
topMargin: root._topMargin
leftMargin: (root._leftMargin-backButton.width) / 2
}
colorScheme: root.colorScheme
onClicked: root.back()
icon.source: "icons/ic-arrow-left.svg"
secondary: true
horizontalPadding: 8
}
}

View File

@ -0,0 +1,317 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import Proton 4.0
Item {
id:root
property ColorScheme colorScheme
property var backend
property var user
property string address
signal dismissed()
signal finished()
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
ListModel {
id: clients
property string name : "Apple Mail"
property string iconSource : "./icons/ic-apple-mail.svg"
property bool haveAutoSetup: true
property string link: "https://protonmail.com/bridge/applemail"
Component.onCompleted : {
if (root.backend.goos == "darwin") {
append({
"name" : "Apple Mail",
"iconSource" : "./icons/ic-apple-mail.svg",
"haveAutoSetup" : true,
"link" : "https://protonmail.com/bridge/applemail"
})
append({
"name" : "Microsoft Outlook",
"iconSource" : "./icons/ic-microsoft-outlook.svg",
"haveAutoSetup" : false,
"link" : "https://protonmail.com/bridge/outlook2019-mac"
})
}
if (root.backend.goos == "windows") {
append({
"name" : "Microsoft Outlook",
"iconSource" : "./icons/ic-microsoft-outlook.svg",
"haveAutoSetup" : false,
"link" : "https://protonmail.com/bridge/outlook2019"
})
}
append({
"name" : "Mozilla Thunderbird",
"iconSource" : "./icons/ic-mozilla-thunderbird.svg",
"haveAutoSetup" : false,
"link" : "https://protonmail.com/bridge/thunderbird"
})
append({
"name" : "Other",
"iconSource" : "./icons/ic-other-mail-clients.svg",
"haveAutoSetup" : false,
"link" : "https://protonmail.com/bridge/clients"
})
}
}
Rectangle {
anchors.fill: root
color: root.colorScheme.background_norm
}
StackLayout {
id: guidePages
anchors.fill: parent
anchors.leftMargin: 80
anchors.rightMargin: 80
anchors.topMargin: 30
anchors.bottomMargin: 70
ColumnLayout { // 0: Client selection
id: clientView
Layout.fillHeight: true
property int columnWidth: 268
spacing: 8
Label {
colorScheme: root.colorScheme
text: qsTr("Setting up email client")
type: Label.LabelType.Heading
}
Label {
colorScheme: root.colorScheme
text: address
color: root.colorScheme.text_weak
type: Label.LabelType.Lead
}
RowLayout {
Layout.topMargin: 32-clientView.spacing
spacing: 24
ColumnLayout {
id: clientColumn
Layout.alignment: Qt.AlignTop
Label {
id: labelA
colorScheme: root.colorScheme
text: qsTr("Choose an email client")
type: Label.LabelType.Body_semibold
}
ListView {
id: clientList
Layout.fillHeight: true
width: clientView.columnWidth
model: clients
highlight: Rectangle {
color: root.colorScheme.interaction_default_active
radius: ProtonStyle.context_item_radius
}
delegate: Item {
implicitWidth: clientRow.width
implicitHeight: clientRow.height
ColumnLayout {
id: clientRow
width: clientList.width
RowLayout {
Layout.topMargin: 12
Layout.bottomMargin: 12
Layout.leftMargin: 16
Layout.rightMargin: 16
ColorImage {
source: model.iconSource
height: 36
sourceSize.height: 36
}
Label {
colorScheme: root.colorScheme
Layout.leftMargin: 12
text: model.name
type: Label.LabelType.Body
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: root.colorScheme.border_weak
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
clientList.currentIndex = index
if (!model.haveAutoSetup) {
root.setupAction(1,index)
}
}
}
}
}
}
ColumnLayout {
id: actionColumn
visible: clientList.currentIndex >= 0 && clients.get(clientList.currentIndex).haveAutoSetup
Layout.alignment: Qt.AlignTop
Label {
colorScheme: root.colorScheme
text: qsTr("Choose configuration mode")
type: Label.LabelType.Body_semibold
}
ListView {
id: actionList
Layout.fillHeight: true
width: clientView.columnWidth
model: [
qsTr("Configure automatically"),
qsTr("Configure manually"),
]
highlight: Rectangle {
color: root.colorScheme.interaction_default_active
radius: ProtonStyle.context_item_radius
}
delegate: Item {
implicitWidth: children[0].width
implicitHeight: children[0].height
ColumnLayout {
width: actionList.width
Label {
Layout.topMargin: 20
Layout.bottomMargin: 20
Layout.leftMargin: 16
Layout.rightMargin: 16
colorScheme: root.colorScheme
text: modelData
type: Label.LabelType.Body
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: root.colorScheme.border_weak
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
actionList.currentIndex = index
root.setupAction(index,clientList.currentIndex)
}
}
}
}
}
}
Item { Layout.fillHeight: true }
Button {
colorScheme: root.colorScheme
text: qsTr("Set up later")
flat: true
onClicked: {
root.setupAction(-1,-1)
if (user) {
user.setupGuideSeen = true
}
root.dismissed()
}
}
}
}
function setupAction(actionID,clientID){
if (user) {
user.setupGuideSeen = true
}
switch (actionID) {
case -1: root.dismissed(); break; // dismiss
case 0: // automatic
if (user) {
switch (clientID) {
case 0:
root.user.configureAppleMail(root.address)
break;
}
}
root.finished()
break;
case 1: // manual
var clientObj = clients.get(clientID)
if (clientObj != undefined && clientObj.link != "" ) {
Qt.openUrlExternally(clientObj.link)
} else {
console.log("unexpected client index", actionID, clientID)
}
root.finished();
break;
default:
console.log("unexpected client setup action", actionID, clientID)
}
}
function reset(){
guidePages.currentIndex = 0
clientList.currentIndex = -1
actionList.currentIndex = -1
}
}

View File

@ -0,0 +1,457 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import Proton 4.0
Item {
id: root
property ColorScheme colorScheme
function reset() {
stackLayout.currentIndex = 0
loginNormalLayout.reset()
login2FALayout.reset()
login2PasswordLayout.reset()
}
function abort() {
root.reset()
root.backend.loginAbort(usernameTextField.text)
}
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
property var backend
property alias username: usernameTextField.text
state: "Page 1"
property alias currentIndex: stackLayout.currentIndex
StackLayout {
id: stackLayout
anchors.fill: parent
function loginFailed() {
signInButton.loading = false
usernameTextField.enabled = true
usernameTextField.error = true
passwordTextField.enabled = true
passwordTextField.error = true
}
Connections {
target: root.backend
onLoginUsernamePasswordError: {
console.assert(stackLayout.currentIndex == 0, "Unexpected loginUsernamePasswordError")
console.assert(signInButton.loading == true, "Unexpected loginUsernamePasswordError")
stackLayout.loginFailed()
if (errorMsg!="") errorLabel.text = errorMsg
else errorLabel.text = qsTr("Incorrect login credentials")
}
onLoginFreeUserError: {
console.assert(stackLayout.currentIndex == 0, "Unexpected loginFreeUserError")
stackLayout.loginFailed()
}
onLoginConnectionError: {
if (stackLayout.currentIndex == 0 ) {
stackLayout.loginFailed()
}
}
onLogin2FARequested: {
console.assert(stackLayout.currentIndex == 0, "Unexpected login2FARequested")
twoFactorUsernameLabel.text = username
stackLayout.currentIndex = 1
}
onLogin2FAError: {
console.assert(stackLayout.currentIndex == 1, "Unexpected login2FAError")
twoFAButton.loading = false
twoFactorPasswordTextField.enabled = true
twoFactorPasswordTextField.error = true
twoFactorPasswordTextField.errorString = qsTr("Your code is incorrect")
}
onLogin2FAErrorAbort: {
console.assert(stackLayout.currentIndex == 1, "Unexpected login2FAErrorAbort")
root.reset()
errorLabel.text = qsTr("Incorrect login credentials. Please try again.")
}
onLogin2PasswordRequested: {
console.assert(stackLayout.currentIndex == 0 || stackLayout.currentIndex == 1, "Unexpected login2PasswordRequested")
stackLayout.currentIndex = 2
}
onLogin2PasswordError: {
console.assert(stackLayout.currentIndex == 2, "Unexpected login2PasswordError")
secondPasswordButton.loading = false
secondPasswordTextField.enabled = true
secondPasswordTextField.error = true
secondPasswordTextField.errorString = qsTr("Your mailbox password is incorrect")
}
onLogin2PasswordErrorAbort: {
console.assert(stackLayout.currentIndex == 2, "Unexpected login2PasswordErrorAbort")
root.reset()
errorLabel.text = qsTr("Incorrect login credentials. Please try again.")
}
onLoginFinished: {
stackLayout.currentIndex = 0
root.reset()
}
}
ColumnLayout {
id: loginNormalLayout
function reset() {
signInButton.loading = false
errorLabel.text = ""
usernameTextField.enabled = true
usernameTextField.error = false
usernameTextField.errorString = ""
passwordTextField.enabled = true
passwordTextField.error = false
passwordTextField.errorString = ""
passwordTextField.text = ""
}
spacing: 0
Label {
colorScheme: root.colorScheme
text: qsTr("Sign in")
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 16
type: Label.LabelType.Title
}
Label {
colorScheme: root.colorScheme
id: subTitle
text: qsTr("Enter your Proton Account details.")
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 8
color: root.colorScheme.text_weak
type: Label.LabelType.Body
}
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 36
spacing: 0
visible: errorLabel.text.length > 0
ColorImage {
color: root.colorScheme.signal_danger
source: "./icons/ic-exclamation-circle-filled.svg"
height: errorLabel.height
sourceSize.height: errorLabel.height
}
Label {
colorScheme: root.colorScheme
id: errorLabel
Layout.leftMargin: 4
color: root.colorScheme.signal_danger
type: root.error ? Label.LabelType.Caption_semibold : Label.LabelType.Caption
}
}
TextField {
colorScheme: root.colorScheme
id: usernameTextField
label: qsTr("Username or email")
Layout.fillWidth: true
Layout.topMargin: 24
onTextChanged: {
// remove "invalid username / password error"
if (error || errorLabel.text.length > 0) {
errorLabel.text = ""
usernameTextField.error = false
passwordTextField.error = false
}
}
validator: function(str) {
if (str.length === 0) {
return qsTr("Enter username or email")
}
return
}
onAccepted: passwordTextField.forceActiveFocus()
}
TextField {
colorScheme: root.colorScheme
id: passwordTextField
label: qsTr("Password")
Layout.fillWidth: true
Layout.topMargin: 8
echoMode: TextInput.Password
onTextChanged: {
// remove "invalid username / password error"
if (error || errorLabel.text.length > 0) {
errorLabel.text = ""
usernameTextField.error = false
passwordTextField.error = false
}
}
validator: function(str) {
if (str.length === 0) {
return qsTr("Enter password")
}
return
}
onAccepted: signInButton.checkAndSignIn()
}
Button {
colorScheme: root.colorScheme
id: signInButton
text: qsTr("Sign in")
Layout.fillWidth: true
Layout.topMargin: 24
onClicked: checkAndSignIn()
function checkAndSignIn() {
usernameTextField.validate()
passwordTextField.validate()
if (usernameTextField.error || passwordTextField.error) {
return
}
usernameTextField.enabled = false
passwordTextField.enabled = false
enabled = false
loading = true
root.backend.login(usernameTextField.text, Qt.btoa(passwordTextField.text))
}
}
Label {
colorScheme: root.colorScheme
textFormat: Text.StyledText
text: link("https://protonmail.com/signup", qsTr("Create or upgrade your account"))
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 24
type: Label.LabelType.Body
onLinkActivated: {
Qt.openUrlExternally(link)
}
}
}
ColumnLayout {
id: login2FALayout
function reset() {
twoFAButton.loading = false
twoFactorPasswordTextField.enabled = true
twoFactorPasswordTextField.error = false
twoFactorPasswordTextField.errorString = ""
twoFactorPasswordTextField.text = ""
}
spacing: 0
Label {
colorScheme: root.colorScheme
text: qsTr("Two-factor authentication")
Layout.topMargin: 16
Layout.alignment: Qt.AlignCenter
type: Label.LabelType.Heading
}
Label {
colorScheme: root.colorScheme
id: twoFactorUsernameLabel
Layout.alignment: Qt.AlignCenter
Layout.topMargin: 8
type: Label.LabelType.Lead
color: root.colorScheme.text_weak
}
TextField {
colorScheme: root.colorScheme
id: twoFactorPasswordTextField
label: qsTr("Two-factor code")
assistiveText: qsTr("Enter the 6-digit code")
Layout.fillWidth: true
Layout.topMargin: 32
validator: function(str) {
if (str.length === 0) {
return qsTr("Enter username or email")
}
return
}
}
Button {
colorScheme: root.colorScheme
id: twoFAButton
text: loading ? qsTr("Authenticating") : qsTr("Authenticate")
Layout.fillWidth: true
Layout.topMargin: 24
onClicked: {
twoFactorPasswordTextField.validate()
if (twoFactorPasswordTextField.error) {
return
}
twoFactorPasswordTextField.enabled = false
enabled = false
loading = true
root.backend.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text))
}
}
}
ColumnLayout {
id: login2PasswordLayout
function reset() {
secondPasswordButton.loading = false
secondPasswordTextField.enabled = true
secondPasswordTextField.error = false
secondPasswordTextField.errorString = ""
secondPasswordTextField.text = ""
}
spacing: 0
Label {
colorScheme: root.colorScheme
text: qsTr("Unlock your mailbox")
Layout.topMargin: 16
Layout.alignment: Qt.AlignCenter
type: Label.LabelType.Heading
}
TextField {
colorScheme: root.colorScheme
id: secondPasswordTextField
label: qsTr("Mailbox password")
Layout.fillWidth: true
Layout.topMargin: 8 + implicitHeight + 24 + subTitle.implicitHeight
echoMode: TextInput.Password
validator: function(str) {
if (str.length === 0) {
return qsTr("Enter username or email")
}
return
}
}
Button {
colorScheme: root.colorScheme
id: secondPasswordButton
text: loading ? qsTr("Unlocking") : qsTr("Unlock")
Layout.fillWidth: true
Layout.topMargin: 24
onClicked: {
secondPasswordTextField.validate()
if (secondPasswordTextField.error) {
return
}
secondPasswordTextField.enabled = false
enabled = false
loading = true
root.backend.login2Password(usernameTextField.text, Qt.btoa(secondPasswordTextField.text))
}
}
}
}
states: [
State {
name: "Page 1"
PropertyChanges {
target: stackLayout
currentIndex: 0
}
},
State {
name: "Page 2"
PropertyChanges {
target: stackLayout
currentIndex: 1
}
},
State {
name: "Page 3"
PropertyChanges {
target: stackLayout
currentIndex: 2
}
}
]
}

View File

@ -0,0 +1,108 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import Proton 4.0
Dialog {
id: root
property var backend
shouldShow: root.backend.showSplashScreen
modal: true
topPadding : 0
leftPadding : 0
rightPadding : 0
ColumnLayout {
spacing: 20
Image {
Layout.alignment: Qt.AlignHCenter
sourceSize.width: 400
sourceSize.height: 225
Layout.preferredWidth: 400
Layout.preferredHeight: 225
source: "./icons/img-splash.png"
}
Label {
colorScheme: root.colorScheme;
Layout.alignment: Qt.AlignHCenter;
Layout.leftMargin: 24
Layout.rightMargin: 24
Layout.preferredWidth: 336
type: Label.Title
horizontalAlignment: Text.AlignHCenter
text: qsTr("Updated Proton, unified protection")
}
Label {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter;
Layout.preferredWidth: 336
Layout.leftMargin: 24
Layout.rightMargin: 24
wrapMode: Text.WordWrap
type: Label.Body
horizontalAlignment: Text.AlignHCenter
textFormat: Text.StyledText
text: qsTr("Introducing Protons refreshed look.<br/>") +
qsTr("Many services, one mission. Welcome to an Internet where privacy is the default. ") +
link("https://proton.me/news/updated-proton",qsTr("Learn More"))
onLinkActivated: Qt.openUrlExternally(link)
}
Button {
Layout.fillWidth: true
Layout.leftMargin: 24
Layout.rightMargin: 24
colorScheme: root.colorScheme
text: "Got it"
onClicked: root.backend.showSplashScreen = false
}
Image {
Layout.alignment: Qt.AlignHCenter
sourceSize.width: 164
sourceSize.height: 32
Layout.preferredWidth: 164
Layout.preferredHeight: 32
source: "./icons/img-proton-logos.svg"
}
}
}

View File

@ -0,0 +1,112 @@
// Copyright (c) 2022 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 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import Proton 4.0
import Notifications 1.0
Item {
id: root
property var backend
property var notifications
property ColorScheme colorScheme
property int notificationWhitelist: NotificationFilter.FilterConsts.All
property int notificationBlacklist: NotificationFilter.FilterConsts.None
readonly property Notification activeNotification: notificationFilter.topmost
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
NotificationFilter {
id: notificationFilter
source: root.notifications ? root.notifications.all : undefined
whitelist: root.notificationWhitelist
blacklist: root.notificationBlacklist
onTopmostChanged: {
if (!topmost) {
image.source = "./icons/ic-connected.svg"
image.color = root.colorScheme.signal_success
label.text = qsTr("Connected")
label.color = root.colorScheme.signal_success
return;
}
image.source = topmost.icon
label.text = topmost.brief
switch (topmost.type) {
case Notification.NotificationType.Danger:
image.color = root.colorScheme.signal_danger
label.color = root.colorScheme.signal_danger
break;
case Notification.NotificationType.Warning:
image.color = root.colorScheme.signal_warning
label.color = root.colorScheme.signal_warning
break;
case Notification.NotificationType.Success:
image.color = root.colorScheme.signal_success
label.color = root.colorScheme.signal_success
break;
case Notification.NotificationType.Info:
image.color = root.colorScheme.signal_info
label.color = root.colorScheme.signal_info
break;
}
}
}
RowLayout {
anchors.fill: parent
spacing: 8
ColorImage {
id: image
width: 16
height: 16
sourceSize.width: width
sourceSize.height: height
source: "./icons/ic-connected.svg"
color: root.colorScheme.signal_success
}
Label {
colorScheme: root.colorScheme
id: label
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
text: qsTr("Connected")
color: root.colorScheme.signal_success
}
}
}

View File

@ -0,0 +1,328 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.13
import QtQuick.Window 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import Proton 4.0
import Notifications 1.0
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 backend
property var notifications
signal showMainWindow()
signal showHelp()
signal showSettings()
signal showSignIn(string username)
signal quit()
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
backend: root.backend
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: root.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: root.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.loggedIn : false
text: qsTr("Sign in")
onClicked: {
root.showSignIn(viewItem.username)
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 {
colorScheme: root.colorScheme
secondary: true
text: qsTr("Open Bridge")
borderless: true
labelType: Label.LabelType.Caption_semibold
onClicked: {
root.showMainWindow()
root.close()
}
}
Item {
Layout.fillWidth: true
}
Button {
colorScheme: root.colorScheme
secondary: true
icon.source: "./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.quit()
root.close()
}
}
onClosed: {
parent.checked = false
}
onOpened: {
parent.checked = true
}
}
}
}
}
}
onActiveChanged: {
if (!active) root.close()
}
function showAndRise() {
root.show()
root.raise()
if (!root.active) {
root.requestActivate()
}
}
}

View File

@ -0,0 +1,278 @@
// Copyright (c) 2022 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 2.12
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
Item {
id: root
property ColorScheme colorScheme
property var backend
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
RowLayout {
anchors.fill: parent
spacing: 0
Rectangle {
color: root.colorScheme.background_norm
Layout.fillHeight: true
Layout.fillWidth: true
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
visible: signInItem.currentIndex == 0
GridLayout {
anchors.fill: parent
columnSpacing: 0
rowSpacing: 0
columns: 3
// top margin
Item {
Layout.columnSpan: 3
Layout.fillWidth: true
// Using binding component here instead of direct binding to avoid binding loop during construction of element
Binding on Layout.preferredHeight {
value: (parent.height - welcomeContentItem.height) / 4
}
}
// left margin
Item {
Layout.minimumWidth: 48
Layout.maximumWidth: 80
Layout.fillWidth: true
Layout.preferredHeight: welcomeContentItem.height
}
ColumnLayout {
id: welcomeContentItem
Layout.fillWidth: true
spacing: 0
Image {
source: colorScheme.welcome_img
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 16
sourceSize.height: 148
sourceSize.width: 264
}
Label {
colorScheme: root.colorScheme
text: qsTr("Welcome to\nProton Mail Bridge")
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.topMargin: 16
horizontalAlignment: Text.AlignHCenter
type: Label.LabelType.Heading
}
Label {
colorScheme: root.colorScheme
id: longTextLabel
text: qsTr("Add your Proton Mail account to securely access and manage your messages in your favorite email client. Bridge runs in the background and encrypts and decrypts your messages seamlessly.")
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.topMargin: 16
Layout.preferredWidth: 320
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
type: Label.LabelType.Body
}
}
// Right margin
Item {
Layout.minimumWidth: 48
Layout.maximumWidth: 80
Layout.fillWidth: true
Layout.preferredHeight: welcomeContentItem.height
}
// bottom margin
Item {
Layout.columnSpan: 3
Layout.fillWidth: true
Layout.fillHeight: true
implicitHeight: children[0].implicitHeight + children[0].anchors.bottomMargin + children[0].anchors.topMargin
implicitWidth: children[0].implicitWidth
Image {
id: logoImage
source: colorScheme.logo_img
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.topMargin: 48
anchors.bottomMargin: 48
sourceSize.height: 25
sourceSize.width: 200
}
}
}
}
Rectangle {
color: (signInItem.currentIndex == 0) ? root.colorScheme.background_weak : root.colorScheme.background_norm
Layout.fillHeight: true
Layout.fillWidth: true
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
RowLayout {
anchors.fill: parent
spacing: 0
Item {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.preferredWidth: signInItem.currentIndex == 0 ? 0 : parent.width / 4
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
Button {
colorScheme: root.colorScheme
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.leftMargin: 80
anchors.rightMargin: 80
anchors.topMargin: 80
anchors.bottomMargin: 80
visible: signInItem.currentIndex != 0
secondary: true
text: qsTr("Back")
onClicked: {
signInItem.abort()
}
}
}
GridLayout {
Layout.fillHeight: true
Layout.fillWidth: true
columnSpacing: 0
rowSpacing: 0
columns: 3
// top margin
Item {
Layout.columnSpan: 3
Layout.fillWidth: true
// Using binding component here instead of direct binding to avoid binding loop during construction of element
Binding on Layout.preferredHeight {
value: (parent.height - signInItem.height) / 4
}
}
// left margin
Item {
Layout.minimumWidth: 48
Layout.maximumWidth: 80
Layout.fillWidth: true
Layout.preferredHeight: signInItem.height
}
SignIn {
id: signInItem
colorScheme: root.colorScheme
Layout.preferredWidth: 320
Layout.fillWidth: true
username: root.backend.users.count === 1 && root.backend.users.get(0) && root.backend.users.get(0).loggedIn === false ? root.backend.users.get(0).username : ""
backend: root.backend
}
// Right margin
Item {
Layout.minimumWidth: 48
Layout.maximumWidth: 80
Layout.fillWidth: true
Layout.preferredHeight: signInItem.height
}
// bottom margin
Item {
Layout.columnSpan: 3
Layout.fillWidth: true
Layout.fillHeight: true
}
}
Item {
Layout.fillHeight: true
Layout.preferredWidth: signInItem.currentIndex == 0 ? 0 : parent.width / 4
}
}
}
states: [
State {
name: "Page 1"
PropertyChanges {
target: signInItem
currentIndex: 0
}
},
State {
name: "Page 2"
PropertyChanges {
target: signInItem
currentIndex: 1
}
},
State {
name: "Page 3"
PropertyChanges {
target: signInItem
currentIndex: 2
}
}
]
}
}

View File

@ -0,0 +1,37 @@
// Copyright (c) 2022 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 QmlProject 1.1
Project {
mainFile: "./MainWindow.qml"
/* Include .qml, .js, and image files from current directory and subdirectories */
QmlFiles {
directory: "./"
}
JavaScriptFiles {
directory: "./"
}
ImageFiles {
directory: "./"
}
/* List of plugin directories passed to QML runtime */
importPaths: [
"./"
]
}

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.2" d="M8 2.75C8.68944 2.75 9.37213 2.8858 10.0091 3.14963C10.646 3.41347 11.2248 3.80018 11.7123 4.28769C12.1998 4.7752 12.5865 5.35395 12.8504 5.99091C13.1142 6.62787 13.25 7.31056 13.25 8C13.25 8.68944 13.1142 9.37213 12.8504 10.0091C12.5865 10.646 12.1998 11.2248 11.7123 11.7123C11.2248 12.1998 10.646 12.5865 10.0091 12.8504C9.37213 13.1142 8.68944 13.25 8 13.25C7.31056 13.25 6.62787 13.1142 5.99091 12.8504C5.35395 12.5865 4.7752 12.1998 4.28769 11.7123C3.80018 11.2248 3.41347 10.646 3.14963 10.0091C2.88579 9.37213 2.75 8.68944 2.75 8C2.75 7.31056 2.8858 6.62787 3.14963 5.99091C3.41347 5.35395 3.80018 4.77519 4.28769 4.28769C4.7752 3.80018 5.35396 3.41347 5.99092 3.14963C6.62787 2.88579 7.31056 2.75 8 2.75L8 2.75Z" stroke="#5064B6" stroke-width="1.5"/>
<path d="M11.7123 11.7123C11.1018 12.3228 10.3502 12.7733 9.52399 13.0239C8.6978 13.2746 7.82255 13.3176 6.97578 13.1491C6.129 12.9807 5.33683 12.606 4.66944 12.0583C4.00204 11.5106 3.48003 10.8067 3.14963 10.0091C2.81924 9.21144 2.69066 8.34462 2.77528 7.48541C2.85991 6.6262 3.15512 5.80112 3.63478 5.08326C4.11445 4.36539 4.76374 3.7769 5.52517 3.36991C6.28659 2.96292 7.13663 2.75 8 2.75" stroke="#5064B6" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,4 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.2" d="M24 1C27.0204 1 30.0112 1.59491 32.8017 2.75077C35.5922 3.90663 38.1277 5.6008 40.2635 7.73655C42.3992 9.87229 44.0934 12.4078 45.2492 15.1983C46.4051 17.9888 47 20.9796 47 24C47 27.0204 46.4051 30.0112 45.2492 32.8017C44.0934 35.5922 42.3992 38.1277 40.2635 40.2635C38.1277 42.3992 35.5922 44.0934 32.8017 45.2492C30.0112 46.4051 27.0204 47 24 47C20.9796 47 17.9888 46.4051 15.1983 45.2492C12.4078 44.0934 9.87228 42.3992 7.73654 40.2634C5.60079 38.1277 3.90662 35.5922 2.75077 32.8017C1.59491 30.0112 0.999998 27.0204 1 24C1 20.9796 1.59492 17.9888 2.75078 15.1983C3.90664 12.4078 5.6008 9.87228 7.73655 7.73653C9.8723 5.60079 12.4078 3.90662 15.1983 2.75077C17.9888 1.59491 20.9796 0.999998 24 1L24 1Z" stroke="#5064B6" stroke-width="2"/>
<path d="M40.2635 40.2635C37.5889 42.938 34.2961 44.9117 30.6765 46.0096C27.057 47.1076 23.2226 47.296 19.5129 46.5581C15.8032 45.8202 12.3328 44.1788 9.40895 41.7792C6.48514 39.3797 4.19822 36.2962 2.75077 32.8017C1.30332 29.3073 0.740014 25.5098 1.11075 21.7456C1.48149 17.9814 2.77483 14.3668 4.8762 11.2219C6.97757 8.07696 9.82212 5.49881 13.1579 3.71581C16.4936 1.93281 20.2176 1 24 1" stroke="#5064B6" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,11 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.9729 8.06214C28.6889 3.51461 35.2762 3.51461 37.9923 8.06214L62.464 49.0356C65.2508 53.7015 61.8891 59.625 56.4543 59.625H7.51078C2.07602 59.625 -1.28569 53.7015 1.50106 49.0356L25.9729 8.06214Z" fill="url(#paint0_linear)"/>
<path d="M29.0775 24.3888C29.0864 22.7908 30.3843 21.5 31.9824 21.5C33.5805 21.5 34.8785 22.7908 34.8874 24.3888L34.9658 38.5C34.9749 40.1542 33.6366 41.5 31.9824 41.5C30.3283 41.5 28.9899 40.1542 28.9991 38.5L29.0775 24.3888Z" fill="white"/>
<path d="M34.9824 47.5C34.9824 49.1569 33.6393 50.5 31.9824 50.5C30.3256 50.5 28.9824 49.1569 28.9824 47.5C28.9824 45.8431 30.3256 44.5 31.9824 44.5C33.6393 44.5 34.9824 45.8431 34.9824 47.5Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear" x1="31.9827" y1="4.18421" x2="31.9827" y2="84.2072" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF7171"/>
<stop offset="1" stop-color="#E63757"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1013 B

View File

@ -0,0 +1,10 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.29416 0.0598755H27.7073C32.2688 0.0598755 35.9409 3.73205 35.9409 8.29347V27.7067C35.9409 32.2681 32.2688 35.9403 27.7073 35.9403H8.29416C3.73273 35.9403 0.0605469 32.2681 0.0605469 27.7067V8.29347C0.0605469 3.73205 3.73273 0.0598755 8.29416 0.0598755Z" fill="url(#paint0_linear)"/>
<path d="M7.92234 10.2633C7.73329 10.2633 7.55519 10.2959 7.38662 10.363L10.7629 13.8389L14.1765 17.3771L14.2388 17.4518L14.3385 17.5515L14.4381 17.6512L14.6375 17.863L17.5652 20.8655C17.6139 20.8958 17.7552 21.0266 17.8656 21.0818C18.0077 21.1529 18.1618 21.2183 18.3207 21.224C18.492 21.2302 18.6672 21.1811 18.8215 21.106C18.937 21.0498 18.9883 20.9693 19.1225 20.8655L22.5112 17.3646L25.9373 13.8389L29.2388 10.4377C29.0268 10.3229 28.7921 10.2633 28.5411 10.2633H7.92234ZM6.88828 10.6869C6.52824 11.028 6.30273 11.5409 6.30273 12.1196V23.5316C6.30273 24.0002 6.45323 24.4258 6.70141 24.7525L7.17483 24.304L10.7006 20.8779L13.8277 17.8505L13.7654 17.7758L10.3393 14.25L6.9132 10.7118L6.88828 10.6869ZM29.6873 10.799L26.3484 14.25L22.9348 17.7758L22.8725 17.8381L26.1242 20.9901L29.6499 24.4161L29.8617 24.6155C30.0516 24.3109 30.1607 23.9357 30.1607 23.5316V12.1196C30.1607 11.6036 29.9819 11.1357 29.6873 10.799ZM14.2263 18.2617L11.1117 21.2891L7.5735 24.7151L7.12499 25.1512C7.36144 25.3035 7.63053 25.4004 7.92234 25.4004H28.5411C28.8919 25.4004 29.2101 25.2654 29.4755 25.0515L29.2513 24.8273L25.713 21.4012L22.4614 18.2617L19.5336 21.2766C19.3752 21.3817 19.2693 21.4982 19.1146 21.5697C18.8656 21.6849 18.5926 21.7823 18.3183 21.7781C18.0433 21.7739 17.7736 21.6662 17.5267 21.5448C17.4028 21.4839 17.3368 21.4233 17.1914 21.3015L14.2263 18.2617Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear" x1="18.252" y1="35.7962" x2="18.2869" y2="0.493036" gradientUnits="userSpaceOnUse">
<stop stop-color="#70EFFF"/>
<stop offset="1" stop-color="#5770FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ic-arrow-left">
<path id="icon" fill-rule="evenodd" clip-rule="evenodd" d="M14 8H3.20998L7.68998 3.52L6.97998 2.81L1.28998 8.5L6.97998 14.19L7.68998 13.48L3.20998 9H14V8Z" fill="#17181C"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
version="1.1"
id="svg12"
sodipodi:docname="ic-card-identity.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs16" />
<sodipodi:namedview
id="namedview14"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
showgrid="false"
inkscape:zoom="40.25"
inkscape:cx="7.9875776"
inkscape:cy="8"
inkscape:window-width="2556"
inkscape:window-height="1401"
inkscape:window-x="0"
inkscape:window-y="18"
inkscape:window-maximized="1"
inkscape:current-layer="svg12" />
<g
id="ic-card-identity"
transform="translate(0,-3.9095641)">
<g
id="icon">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 5.32,9.64 C 5.96894,9.64404 6.55605,9.25571 6.80626,8.65693 7.05647,8.05815 6.92022,7.36754 6.46134,6.90866 6.00246,6.44978 5.31185,6.31353 4.71307,6.56374 4.11429,6.81395 3.72596,7.40106 3.73,8.05 c 0,0.87813 0.71187,1.59 1.59,1.59 z m 0,-2.19 C 5.61898,7.48884 5.8427,7.74351 5.8427,8.045 5.8427,8.34649 5.61898,8.60116 5.32,8.64 5.02102,8.60116 4.7973,8.34649 4.7973,8.045 4.7973,7.74351 5.02102,7.48884 5.32,7.45 Z"
fill="#0c0c14"
id="path2" />
<path
d="m 9,8 h 4 V 9 H 9 Z"
fill="#0c0c14"
id="path4" />
<path
d="M 13,10 H 9 v 1 h 4 z"
fill="#0c0c14"
id="path6" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 1,14 V 4 H 15 V 14 Z M 3.64,12.49 V 13 H 7 V 12.49 L 6.1,11.57 H 4.54 Z M 14,13 H 8 V 12.08 L 6.52,10.57 H 4.12 L 2.64,12.08 V 13 H 2 V 5 h 12 z"
fill="#0c0c14"
id="path8" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 8.5L2.7 7.8L6.05 11.14L13.2 4L13.9 4.7L6.05 12.56L2 8.5Z" fill="#17181C"/>
</svg>

After

Width:  |  Height:  |  Size: 230 B

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ic-chevron-down">
<path id="icon" fill-rule="evenodd" clip-rule="evenodd" d="M2.3 6.30001L8 12L13.7 6.30001L13 5.60001L8 10.58L3 5.60001L2.3 6.30001Z" fill="#17181C"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ic-chevron-up">
<path id="icon" fill-rule="evenodd" clip-rule="evenodd" d="M13.7 9.7L7.99999 4L2.29999 9.7L2.99999 10.4L7.99999 5.42L13 10.4L13.7 9.7Z" fill="#17181C"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.7166 1H9.29278L9.83484 2.72079C10.0427 2.79404 10.2448 2.87886 10.4428 2.97503L12.04 2.14318L13.8662 3.97057L13.035 5.56869C13.1312 5.7671 13.2161 5.96972 13.2894 6.178L15 6.72052V9.29956L13.2799 9.83276C13.2068 10.0403 13.1222 10.2423 13.0262 10.4401L13.8574 12.0382L12.0312 13.8656L10.434 13.0338C10.2359 13.13 10.0336 13.2149 9.82558 13.2882L9.2834 15H6.70782L6.16564 13.2882C5.95765 13.2149 5.75532 13.13 5.55718 13.0338L3.95998 13.8656L2.13382 12.0382L2.96503 10.4401C2.86877 10.2417 2.78388 10.0391 2.71059 9.83078L1 9.28826V6.71174L2.71059 6.16922C2.78388 5.96094 2.86877 5.75832 2.96503 5.55991L2.13323 3.96064L3.96989 2.13499L5.56596 2.96625C5.76409 2.87 5.96643 2.78513 6.17441 2.71184L6.7166 1ZM7.44883 2L6.97047 3.51034L6.71736 3.58632C6.40057 3.68142 6.09845 3.8085 5.80203 3.96884L5.56858 4.09511L4.15637 3.35961L3.35968 4.15152L4.09358 5.56254L3.9676 5.79574C3.80736 6.0924 3.68034 6.39477 3.58529 6.71183L3.5094 6.96496L2 7.44367V8.55633L3.5094 9.03504L3.58529 9.28817C3.68034 9.60523 3.80736 9.9076 3.9676 10.2043L4.09358 10.4375L3.35909 11.8496L4.14873 12.6398L5.5598 11.9049L5.79325 12.0312C6.08967 12.1915 6.39179 12.3186 6.70858 12.4137L6.96169 12.4897L7.44005 14H8.55117L9.02953 12.4897L9.28264 12.4137C9.59943 12.3186 9.90155 12.1915 10.198 12.0312L10.4314 11.9049L11.8425 12.6398L12.6321 11.8496L11.8976 10.4375L12.0236 10.2043C12.1839 9.9076 12.3109 9.60523 12.4059 9.28817L12.4824 9.03304L14 8.5626V7.45245L12.4906 6.97374L12.4147 6.72061C12.3197 6.40355 12.1926 6.10118 12.0324 5.80453L11.9064 5.57132L12.6409 4.15916L11.8513 3.36898L10.4402 4.1039L10.2067 3.97762C9.91033 3.81729 9.60821 3.6902 9.29142 3.5951L9.03783 3.51898L8.55935 2H7.44883ZM7.99561 5.81216C6.78861 5.81216 5.80959 6.79136 5.80959 8C5.80959 9.20864 6.78861 10.1878 7.99561 10.1878C9.20261 10.1878 10.1816 9.20864 10.1816 8C10.1816 6.79136 9.20261 5.81216 7.99561 5.81216ZM4.80959 8C4.80959 6.23972 6.23569 4.81216 7.99561 4.81216C9.75553 4.81216 11.1816 6.23972 11.1816 8C11.1816 9.76028 9.75553 11.1878 7.99561 11.1878C6.23569 11.1878 4.80959 9.76028 4.80959 8Z" fill="#17181C"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.9257 5.48656C13.8921 3.271 11.0022 2 8 2C4.99776 2 2.10791 3.271 0.0743047 5.48656C-0.0273754 5.59626 -0.0246996 5.76751 0.0823321 5.87455L1.67175 7.4637C1.78146 7.57341 1.95806 7.57341 2.06777 7.4637C2.0865 7.44524 2.09988 7.42383 2.11058 7.39975C3.61705 5.74584 5.76036 4.7962 8 4.7962C10.2396 4.7962 12.3829 5.74611 13.8894 7.39975C13.9001 7.42383 13.9135 7.44524 13.9322 7.4637C13.9857 7.51989 14.058 7.54692 14.1302 7.54692C14.2025 7.54692 14.2747 7.51989 14.3282 7.4637L15.9177 5.87455C16.0247 5.76751 16.0274 5.59653 15.9257 5.48656Z" fill="#349172"/>
<path d="M12.7547 8.56633C11.5533 7.20168 9.82473 6.41794 7.99984 6.41794C6.17495 6.41794 4.44639 7.20195 3.24228 8.56633C3.2289 8.57436 3.21285 8.58265 3.19947 8.59577C3.08976 8.70547 3.08976 8.88208 3.19947 8.99151L4.72735 10.5194C4.7541 10.5488 4.78621 10.5678 4.821 10.5809C4.83705 10.589 4.85311 10.589 4.86649 10.5916C4.88789 10.5943 4.90662 10.6023 4.92535 10.6023C4.92803 10.6023 4.93071 10.5997 4.93338 10.5997C4.93606 10.5997 4.93873 10.6023 4.94141 10.6023C4.95211 10.6023 4.96281 10.5943 4.97352 10.5916C4.9976 10.589 5.02168 10.5836 5.04309 10.5729C5.05914 10.5676 5.06985 10.5569 5.08323 10.5462C5.0966 10.5381 5.11266 10.5328 5.12336 10.5191C5.12871 10.5164 5.12871 10.5087 5.13407 10.5057C5.13674 10.5004 5.14209 10.5004 5.14477 10.495C5.83512 9.62006 6.87601 9.11969 7.99984 9.11969C9.12367 9.11969 10.1646 9.62006 10.8549 10.495C10.8576 10.5007 10.8629 10.5007 10.8656 10.5057C10.871 10.5114 10.871 10.5164 10.8763 10.5191C10.887 10.5328 10.9031 10.5379 10.9165 10.5462C10.9298 10.5569 10.9405 10.5676 10.9566 10.5729C10.978 10.5836 11.0021 10.5892 11.0262 10.5916C11.0369 10.5943 11.0476 10.6023 11.0583 10.6023H11.0743C11.1118 10.6023 11.1466 10.5943 11.1814 10.5809C11.2161 10.5676 11.2456 10.5461 11.2723 10.5194L12.8002 8.99151C12.9099 8.88208 12.9099 8.70547 12.8002 8.59577C12.7868 8.58265 12.7708 8.57436 12.7547 8.56633Z" fill="#349172"/>
<path d="M7.99991 10.2626C7.17309 10.2626 6.50146 10.9371 6.50146 11.7637C6.50146 12.5902 7.17309 13.2621 7.99991 13.2621C8.82673 13.2621 9.49835 12.5905 9.49835 11.7637C9.49835 10.9369 8.82673 10.2626 7.99991 10.2626Z" fill="#349172"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,4 @@
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 2V0H12V10H9V9H11V1H5V2H4Z" fill="#262A33"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 3V13H8V3H0ZM7 4V12H1V4H7Z" fill="#262A33"/>
</svg>

After

Width:  |  Height:  |  Size: 255 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.85 2.8499L13.15 2.1499L8 7.2899L2.85 2.1499L2.15 2.8499L7.29 7.9999L2.15 13.1499L2.85 13.8499L8 8.7099L13.15 13.8499L13.85 13.1499L8.71 7.9999L13.85 2.8499Z" fill="#17181C"/>
</svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8561 3.06675L13.9015 7.66688H2.09846L4.14391 3.06675H11.8561ZM14 8.68913H2V12.267H14V8.68913ZM12.5 2.04449H3.5L1 7.66688V13.2893H15V7.66688L12.5 2.04449ZM12 10.9381V9.91584H13V10.9381H12ZM10 10.9381H11V9.91584H10V10.9381Z" fill="#0C0C14"/>
</svg>

After

Width:  |  Height:  |  Size: 396 B

Some files were not shown because too many files have changed in this diff Show More