disk cache", enableDiskCache, diskCachePath)
+ }
+ signal changeLocalCacheFinished()
+
+
+ // Settings
+ property bool isAutomaticUpdateOn : true
+ function toggleAutomaticUpdate(makeItActive) {
+ console.debug("-> silent updates", makeItActive, root.isAutomaticUpdateOn)
+ root.isAutomaticUpdateOn = makeItActive
+ }
+
+ property bool isAutostartOn : true // Example of settings with loading state
+ function toggleAutostart(makeItActive) {
+ console.debug("-> autostart", makeItActive, root.isAutomaticUpdateOn)
+ }
+ 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 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 logsPath: "/home/cuto" // StandardPaths.locate(StandardPaths.DesktopLocation)
+ property string version: "v2.0.X"
+ property string licensePath: "/home/cuto" // StandardPaths.locate(StandardPaths.DesktopLocation)
+ property string releaseNotesLink: "https://protonmail.com/download/bridge/early_releases.html"
+
+ 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()
- signal cacheAnavailable()
- signal cacheCantMove()
+ property var availableKeychain: ["gnome-keyring", "pass"]
+ property string selectedKeychain
+ function selectKeychain(wantedKeychain){
+ selectedKeychain = wantedKeychain
+ }
+ signal hasNoKeychain()
+
+ signal noActiveKeyForRecipient(string email)
+ signal showMainWindow()
+
+ signal addressChanged(string address)
+ signal addressChangedLogout(string address)
+ signal userDisconnected(string username)
+ signal apiCertIssue()
+
+
+
+ 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()
+ }
- signal diskFull()
onLoginUsernamePasswordError: {
console.debug("<- loginUsernamePasswordError")
@@ -557,6 +812,9 @@ Window {
onLogin2PasswordErrorAbort: {
console.debug("<- login2PasswordErrorAbort")
}
+ onLoginFinished: {
+ console.debug("<- loginFinished")
+ }
onInternetOff: {
console.debug("<- internetOff")
@@ -571,30 +829,6 @@ Window {
Bridge {
backend: root
- onLogin: {
- root.log("-> login(" + username + ", " + password + ")")
-
- loginUser.username = username
- loginUser.isLoginRequested = true
- }
-
- onLogin2FA: {
- root.log("-> login2FA(" + username + ", " + code + ")")
-
- loginUser.isLogin2FAProvided = true
- }
-
- onLogin2Password: {
- root.log("-> login2FA(" + username + ", " + password + ")")
-
- loginUser.isLogin2PasswordProvided = true
- }
-
- onLoginAbort: {
- root.log("-> loginAbort(" + username + ")")
-
- loginUser.resetLoginRequests()
- }
}
}
diff --git a/internal/frontend/qml/BugReportView.qml b/internal/frontend/qml/BugReportView.qml
new file mode 100644
index 00000000..30868de0
--- /dev/null
+++ b/internal/frontend/qml/BugReportView.qml
@@ -0,0 +1,167 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+import QtQuick 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.12
+
+import Proton 4.0
+
+SettingsView {
+ id: root
+
+ property var selectedAddress
+
+ Label {
+ text: qsTr("Report a problem")
+ colorScheme: root.colorScheme
+ type: Label.Heading
+ }
+
+
+ TextArea {
+ id: description
+ property int _minChars: 150
+ property bool _inputOK: description.text.length>=description._minChars
+
+ label: qsTr("Description")
+ colorScheme: root.colorScheme
+ Layout.fillWidth: true
+ Layout.minimumHeight: 100
+ hint: description.text.length + "/800"
+ placeholderText: qsTr("Tell us what went wrong or isn't working (min. 150 characters).")
+ onEditingFinished: {
+ if (!description._inputOK) {
+ description.error = true
+ description.assistiveText = qsTr("Enter a problem description (min. 150 characters)")
+ } else {
+ description.error = false
+ description.assistiveText = ""
+ }
+ }
+ }
+
+
+ TextField {
+ id: address
+ property bool _inputOK: root.isValidEmail(address.text)
+
+ label: qsTr("Your contact email")
+ colorScheme: root.colorScheme
+ Layout.fillWidth: true
+ placeholderText: qsTr("e.g. jane.doe@protonmail.com")
+
+ onEditingFinished: {
+ if (!address._inputOK) {
+ address.error = true
+ address.assistiveText = qsTr("Enter valid email address")
+ } else {
+ address.assistiveText = ""
+ address.error = false
+ }
+ }
+ }
+
+ TextField {
+ id: emailClient
+ property bool _inputOK: emailClient.text.length > 0
+
+ label: qsTr("Your email client (including version)")
+ colorScheme: root.colorScheme
+ Layout.fillWidth: true
+ placeholderText: qsTr("e.g. Apple Mail 14.0")
+ onEditingFinished: {
+ if (!emailClient._inputOK) {
+ emailClient.assistiveText = qsTr("Enter an email client name and version")
+ emailClient.error = true
+ } else {
+ emailClient.assistiveText = ""
+ emailClient.error = false
+ }
+ }
+ }
+
+
+ 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("file://"+root.backend.logsPath)
+ }
+ }
+
+ Label {
+ text: {
+ var address = "bridge@protonmail.com"
+ var mailTo = `${address}`
+ return qsTr("These reports are not end-to-end encrypted. In case of sensitive information, contact us at %1.").arg(mailTo)
+ }
+ colorScheme: root.colorScheme
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ type: Label.Caption
+ color: root.colorScheme.text_weak
+ }
+
+ Button {
+ id: sendButton
+ text: qsTr("Send")
+ colorScheme: root.colorScheme
+ onClicked: root.submit()
+ enabled: description._inputOK && address._inputOK && emailClient._inputOK
+
+ 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 = /\w+@\w+\.\w+/
+ return reEmail.test(text)
+ }
+
+ function submit() {
+ sendButton.loading = true
+ root.backend.reportBug(
+ description.text,
+ address.text,
+ emailClient.text,
+ includeLogs.checked
+ )
+ }
+
+ Component.onCompleted: root.setDefaultValue()
+
+
+ onBack: {
+ root.setDefaultValue()
+ root.parent.showHelpView()
+ }
+}
diff --git a/internal/frontend/qml/Configuration.qml b/internal/frontend/qml/Configuration.qml
new file mode 100644
index 00000000..42521ed2
--- /dev/null
+++ b/internal/frontend/qml/Configuration.qml
@@ -0,0 +1,73 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+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: 9
+
+ 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 }
+ }
+}
+
diff --git a/internal/frontend/qml/ConfigurationItem.qml b/internal/frontend/qml/ConfigurationItem.qml
new file mode 100644
index 00000000..ed37b4ab
--- /dev/null
+++ b/internal/frontend/qml/ConfigurationItem.qml
@@ -0,0 +1,81 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+import QtQuick 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls.impl 2.12
+
+import Proton 4.0
+
+ColumnLayout {
+ id: root
+ Layout.fillWidth: true
+
+ property var colorScheme
+ property string label
+ property string value
+
+ 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
+
+ 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
+ }
+}
diff --git a/internal/frontend/qml/ContentWrapper.qml b/internal/frontend/qml/ContentWrapper.qml
index 98db99d0..07287042 100644
--- a/internal/frontend/qml/ContentWrapper.qml
+++ b/internal/frontend/qml/ContentWrapper.qml
@@ -26,12 +26,26 @@ Item {
property ColorScheme colorScheme
property var backend
+ property var notifications
signal login(string username, string password)
signal login2FA(string username, string code)
signal login2Password(string username, string password)
signal loginAbort(string username)
+ signal showSetupGuide(var user, string address)
+
+ property var noUser: QtObject {
+ property var avatarText: ""
+ property var username: ""
+ property var password: ""
+ property var usedBytes: 1
+ property var totalBytes: 1
+ property var loggedIn: false
+ property var splitMode: false
+ property var addresses: []
+ }
+
RowLayout {
anchors.fill: parent
spacing: 0
@@ -91,6 +105,8 @@ Item {
horizontalPadding: 0
icon.source: "./icons/ic-question-circle.svg"
+
+ onClicked: rightContent.showHelpView()
}
Button {
@@ -109,10 +125,14 @@ Item {
horizontalPadding: 0
icon.source: "./icons/ic-cog-wheel.svg"
+
+ onClicked: rightContent.showGeneralSettings()
}
}
- // Separator
+ Item {implicitHeight:10}
+
+ // Separator line
Rectangle {
Layout.fillWidth: true
Layout.minimumHeight: 1
@@ -122,14 +142,20 @@ Item {
ListView {
id: accounts
+
+ property var _topBottomMargins: 24
+ property var _leftRightMargins: 16
+
Layout.fillWidth: true
Layout.fillHeight: true
- Layout.leftMargin: 16
- Layout.rightMargin: 16
- Layout.topMargin: 24
- Layout.bottomMargin: 24
+ 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
@@ -142,11 +168,28 @@ Item {
}
}
+ highlight: Rectangle {
+ color: leftBar.colorScheme.interaction_default_active
+ radius: 4
+ }
+
model: root.backend.users
delegate: AccountDelegate{
+ width: leftBar.width - 2*accounts._leftRightMargins
+
id: accountDelegate
colorScheme: leftBar.colorScheme
- user: modelData
+ user: root.backend.users.get(index)
+ onClicked: {
+ var user = root.backend.users.get(index)
+ accounts.currentIndex = index
+ if (user.loggedIn) {
+ rightContent.showAccount()
+ } else {
+ signIn.username = user.username
+ rightContent.showSignIn()
+ }
+ }
}
}
@@ -181,15 +224,16 @@ Item {
icon.source: "./icons/ic-plus.svg"
- onClicked: root.showSignIn()
+ onClicked: {
+ signIn.username = ""
+ rightContent.showSignIn()
+ }
}
}
}
}
- Rectangle {
- id: rightPlane
-
+ Rectangle { // right content background
Layout.fillWidth: true
Layout.fillHeight: true
@@ -199,14 +243,44 @@ Item {
id: rightContent
anchors.fill: parent
- AccountView {
+ AccountView { // 0
colorScheme: root.colorScheme
+ backend: root.backend
+ notifications: root.notifications
+ user: {
+ if (accounts.currentIndex < 0) return root.noUser
+ if (root.backend.users.count == 0) return root.noUser
+ return root.backend.users.get(accounts.currentIndex)
+ }
+ onShowSignIn: {
+ signIn.username = this.user.username
+ rightContent.showSignIn()
+ }
+ onShowSetupGuide: {
+ root.showSetupGuide(user,address)
+ }
}
- GridLayout {
+ GridLayout { // 1
+ columns: 2
+
+ Button {
+ id: backButton
+ Layout.leftMargin: 18
+ Layout.topMargin: 10
+ Layout.alignment: Qt.AlignTop
+
+ colorScheme: root.colorScheme
+ onClicked: rightContent.showAccount()
+ icon.source: "icons/ic-arrow-left.svg"
+ secondary: true
+ horizontalPadding: 8
+ }
+
SignIn {
+ id: signIn
Layout.topMargin: 68
- Layout.leftMargin: 80
+ Layout.leftMargin: 80 - backButton.width - 18
Layout.rightMargin: 80
Layout.bottomMargin: 68
Layout.preferredWidth: 320
@@ -214,21 +288,70 @@ Item {
Layout.fillHeight: true
colorScheme: root.colorScheme
- user: (root.backend.users.count === 1 && root.backend.users.get(0).loggedIn === false) ? root.backend.users.get(0) : undefined
backend: root.backend
- onLogin : { root.login ( username , password ) }
- onLogin2FA : { root.login2FA ( username , code ) }
- onLogin2Password : { root.login2Password ( username , password ) }
- onLoginAbort : { root.loginAbort ( username ) }
+ onLogin : { root.backend.login ( username , password ) }
+ onLogin2FA : { root.backend.login2FA ( username , code ) }
+ onLogin2Password : { root.backend.login2Password ( username , password ) }
+ onLoginAbort : { root.backend.loginAbort ( username ) }
}
}
+
+ GeneralSettings { // 2
+ colorScheme: root.colorScheme
+ backend: root.backend
+ notifications: root.notifications
+ }
+
+ PortSettings { // 3
+ colorScheme: root.colorScheme
+ backend: root.backend
+ }
+
+ SMTPSettings { // 4
+ colorScheme: root.colorScheme
+ backend: root.backend
+ }
+
+ LocalCacheSettings { // 5
+ colorScheme: root.colorScheme
+ backend: root.backend
+ notifications: root.notifications
+ }
+
+ HelpView { // 6
+ colorScheme: root.colorScheme
+ backend: root.backend
+ }
+
+ BugReportView { // 7
+ colorScheme: root.colorScheme
+ backend: root.backend
+ selectedAddress: {
+ if (accounts.currentIndex < 0) return ""
+ if (root.backend.users.count == 0) return ""
+ return root.backend.users.get(accounts.currentIndex).addresses[0]
+ }
+ }
+
+ function showAccount () { rightContent.currentIndex = 0 }
+ function showSignIn () { rightContent.currentIndex = 1 }
+ function showGeneralSettings () { rightContent.currentIndex = 2 }
+ function showPortSettings () { rightContent.currentIndex = 3 }
+ function showSMTPSettings () { rightContent.currentIndex = 4 }
+ function showLocalCacheSettings () { rightContent.currentIndex = 5 }
+ function showHelpView () { rightContent.currentIndex = 6 }
+ function showBugReport () { rightContent.currentIndex = 7 }
}
}
}
-
- function showSignIn() {
- rightContent.currentIndex = 1
+ function showLocalCacheSettings(){rightContent.showLocalCacheSettings() }
+ function showSettings(){rightContent.showGeneralSettings() }
+ function showHelp(){rightContent.showHelpView() }
+ function showSignIn(username){
+ signIn.username = username
+ rightContent.showSignIn()
}
+
}
diff --git a/internal/frontend/qml/GeneralSettings.qml b/internal/frontend/qml/GeneralSettings.qml
new file mode 100644
index 00000000..ccedbd15
--- /dev/null
+++ b/internal/frontend/qml/GeneralSettings.qml
@@ -0,0 +1,168 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+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
+
+ 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)
+ }
+
+ SettingsItem {
+ id: autostart
+ colorScheme: root.colorScheme
+ text: qsTr("Automatically start Bridge")
+ description: qsTr("The app will autostart everytime you reset your device.")
+ type: SettingsItem.Toggle
+ checked: root.backend.isAutostartOn
+ onClicked: {
+ autostart.loading = true
+ root.backend.toggleAutostart(!autoUpdate.checked)
+ }
+ Connections{
+ target: root.backend
+ onToggleAutostartFinished: {
+ autostart.loading = false
+ }
+ }
+ }
+
+ SettingsItem {
+ id: beta
+ colorScheme: root.colorScheme
+ text: qsTr("Enable Beta access")
+ description: qsTr("Be the first one to see new features.")
+ type: SettingsItem.Toggle
+ checked: root.backend.isBetaEnabled
+ onClicked: {
+ if (!beta.checked) {
+ root.notifications.askEnableBeta()
+ } else {
+ root.notifications.askDisableBeta()
+ }
+ }
+ }
+
+ 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
+ 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: doh
+ visible: root._isAdvancedShown
+ colorScheme: root.colorScheme
+ text: qsTr("Alternative routing")
+ description: qsTr("If Proton’s 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)
+ }
+
+ 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()
+ }
+
+ 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()
+ }
+
+ SettingsItem {
+ id: cache
+ visible: root._isAdvancedShown
+ colorScheme: root.colorScheme
+ text: qsTr("Local cache")
+ actionText: qsTr("Configure")
+ description: qsTr("Configure Bridge's local cache settings.")
+ type: SettingsItem.Button
+ onClicked: root.parent.showLocalCacheSettings()
+ }
+
+ 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()
+ }
+ }
+
+ onBack: root.parent.showAccount()
+}
diff --git a/internal/frontend/qml/HelpView.qml b/internal/frontend/qml/HelpView.qml
new file mode 100644
index 00000000..a14f1cb9
--- /dev/null
+++ b/internal/frontend/qml/HelpView.qml
@@ -0,0 +1,110 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+import QtQuick 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.12
+
+import Proton 4.0
+
+SettingsView {
+ id: root
+
+ 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/bridge/install")}
+ }
+
+ 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}
+ }
+
+ 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)}
+ }
+
+ 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()
+ }
+ }
+
+ Label {
+ Layout.alignment: Qt.AlignHCenter
+ colorScheme: root.colorScheme
+ type: Label.Caption
+ color: root.colorScheme.text_weak
+ textFormat: Text.RichText
+ linkColor: root.colorScheme.interaction_norm_active
+
+ text: {
+ var version = root.backend.version
+ var license = qsTr("License")
+ var licensePath = root.backend.licensePath
+ var release= qsTr("Release notes")
+ var releaseNotesLink = root.backend.releaseNotesLink
+ return `Proton Mail Bridge v${version}
+ © 2021 Proton Technologies AG
+ ${license}
+ ${release}
+
`
+ }
+
+ onLinkActivated: Qt.openUrlExternally(link)
+ }
+
+ onBack: {
+ root.parent.showAccount()
+ }
+}
diff --git a/internal/frontend/qml/LocalCacheSettings.qml b/internal/frontend/qml/LocalCacheSettings.qml
new file mode 100644
index 00000000..8809ff47
--- /dev/null
+++ b/internal/frontend/qml/LocalCacheSettings.qml
@@ -0,0 +1,146 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+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
+
+ property var notifications
+ property bool _diskCacheEnabled: true
+ property string _diskCachePath: "/home"
+
+ Label {
+ colorScheme: root.colorScheme
+ text: qsTr("Local cache")
+ type: Label.Heading
+ Layout.fillWidth: true
+ }
+
+ Label {
+ colorScheme: root.colorScheme
+ text: qsTr("Bridge caches your encrypted messages localy to optimise the communication with the local client. Disabling this feature might have a nevative impact on performance.")
+ 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: "When enabled messages are stored on disk." // TODO: wrong text in wireframe
+ type: SettingsItem.Toggle
+ checked: root._diskCacheEnabled
+ onClicked: root._diskCacheEnabled = !root._diskCacheEnabled
+ }
+
+ SettingsItem {
+ colorScheme: root.colorScheme
+ text: qsTr("Current cache location")
+ actionText: qsTr("Change location")
+ description: root._diskCachePath
+ type: SettingsItem.Button
+ enabled: root._diskCacheEnabled
+ onClicked: {
+ pathDialog.open()
+ }
+
+ FileDialog {
+ id: pathDialog
+ title: qsTr("Select cache location")
+ folder: shortcuts.home
+ onAccepted: root.sanitizePath(pathDialog.fileUrl.toString())
+ 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.parent.showGeneralSettings()
+ root.setDefaultValues()
+ }
+
+ function submit(){
+ console.log("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
+ }
+
+ function sanitizePath(path) {
+ var pattern = "file://"
+ if (root.backend.goos=="windows") pattern+="/"
+ root._diskCachePath = path.replace(pattern, "")
+ }
+
+ Component.onCompleted: root.setDefaultValues()
+}
diff --git a/internal/frontend/qml/MainWindow.qml b/internal/frontend/qml/MainWindow.qml
index bcf7cf38..d57a5843 100644
--- a/internal/frontend/qml/MainWindow.qml
+++ b/internal/frontend/qml/MainWindow.qml
@@ -62,7 +62,7 @@ ApplicationWindow {
return
}
- root.showSetup(user)
+ root.showSetup(user,user.addresses[0])
}
onRowsAboutToBeRemoved: {
@@ -78,15 +78,6 @@ ApplicationWindow {
}
}
- function showSetup(user) {
- setupGuide.user = user
- if (setupGuide.user) {
- contentLayout._showSetup = true
- } else {
- contentLayout._showSetup = false
- }
- }
-
StackLayout {
id: contentLayout
@@ -111,12 +102,18 @@ ApplicationWindow {
}
ContentWrapper {
+ id: contentWrapper
colorScheme: root.colorScheme
backend: root.backend
+ notifications: root.notifications
Layout.fillHeight: true
Layout.fillWidth: true
+ onShowSetupGuide: {
+ root.showSetup(user,address)
+ }
+
onLogin: {
root.login(username, password)
}
@@ -161,7 +158,7 @@ ApplicationWindow {
Layout.fillWidth: true
onDismissed: {
- root.showSetup(null)
+ root.showSetup(null,"")
}
}
}
@@ -169,5 +166,25 @@ ApplicationWindow {
NotificationPopups {
colorScheme: root.colorScheme
notifications: root.notifications
+ mainWindow: root
+ }
+
+ 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
+ if (setupGuide.user) {
+ contentLayout._showSetup = true
+ } else {
+ contentLayout._showSetup = false
+ }
}
}
diff --git a/internal/frontend/qml/NotificationDialog.qml b/internal/frontend/qml/NotificationDialog.qml
index 914ba8f4..79e98962 100644
--- a/internal/frontend/qml/NotificationDialog.qml
+++ b/internal/frontend/qml/NotificationDialog.qml
@@ -55,13 +55,12 @@ Dialog {
}
switch (root.notification.type) {
- case Notification.NotificationType.Info:
- // TODO: Add info icon?
- return ""
- case Notification.NotificationType.Success:
+ 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:
+ case Notification.NotificationType.Warning:
+ case Notification.NotificationType.Danger:
return "./icons/ic-alert.svg"
}
}
@@ -110,6 +109,8 @@ Dialog {
action: modelData
secondary: index > 0
+
+ loading: notification.loading
}
}
}
diff --git a/internal/frontend/qml/NotificationPopups.qml b/internal/frontend/qml/NotificationPopups.qml
index bb9d6433..a98a4095 100644
--- a/internal/frontend/qml/NotificationPopups.qml
+++ b/internal/frontend/qml/NotificationPopups.qml
@@ -28,6 +28,7 @@ Item {
property ColorScheme colorScheme
property var notifications
+ property var mainWindow
property int notificationWhitelist: NotificationFilter.FilterConsts.All
property int notificationBlacklist: NotificationFilter.FilterConsts.None
@@ -42,6 +43,7 @@ Item {
Banner {
colorScheme: root.colorScheme
notification: bannerNotificationFilter.topmost
+ mainWindow: root.mainWindow
}
NotificationDialog {
@@ -66,17 +68,17 @@ Item {
NotificationDialog {
colorScheme: root.colorScheme
- notification: root.notifications.bugReportSendSuccess
+ notification: root.notifications.disableBeta
}
NotificationDialog {
colorScheme: root.colorScheme
- notification: root.notifications.bugReportSendError
+ notification: root.notifications.enableBeta
}
NotificationDialog {
colorScheme: root.colorScheme
- notification: root.notifications.cacheAnavailable
+ notification: root.notifications.cacheUnavailable
}
NotificationDialog {
@@ -88,4 +90,24 @@ Item {
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
+ }
}
diff --git a/internal/frontend/qml/Notifications/Notification.qml b/internal/frontend/qml/Notifications/Notification.qml
index 5c267b53..3c773021 100644
--- a/internal/frontend/qml/Notifications/Notification.qml
+++ b/internal/frontend/qml/Notifications/Notification.qml
@@ -39,6 +39,7 @@ QtObject {
property bool dismissed: false
property bool active: false
+ property bool loading: false
readonly property var occurred: active ? new Date() : undefined
property var data
diff --git a/internal/frontend/qml/Notifications/Notifications.qml b/internal/frontend/qml/Notifications/Notifications.qml
index f3f110bf..c4302dc3 100644
--- a/internal/frontend/qml/Notifications/Notifications.qml
+++ b/internal/frontend/qml/Notifications/Notifications.qml
@@ -29,6 +29,13 @@ QtObject {
property StatusWindow frontendStatus
property SystemTrayIcon frontendTray
+ signal askDisableBeta()
+ signal askEnableBeta()
+ signal askEnableSplitMode(var user)
+ signal askDisableLocalCache()
+ signal askEnableLocalCache(var path)
+ signal askResetBridge()
+
enum Group {
Connection = 1,
Update = 2,
@@ -48,12 +55,20 @@ QtObject {
root.updateForceError,
root.updateSilentRestartNeeded,
root.updateSilentError,
+ root.updateIsLatestVersion,
+ root.disableBeta,
+ root.enableBeta,
root.bugReportSendSuccess,
root.bugReportSendError,
- root.cacheAnavailable,
+ root.cacheUnavailable,
root.cacheCantMove,
root.accountChanged,
- root.diskFull
+ root.diskFull,
+ root.cacheLocationChangeSuccess,
+ root.enableSplitMode,
+ root.disableLocalCache,
+ root.enableLocalCache,
+ root.resetBridge
]
// Connection
@@ -93,10 +108,18 @@ QtObject {
action: [
Action {
- text: qsTr("Update")
+ text: qsTr("Install update")
onTriggered: {
- // TODO: call update from backend
+ root.backend.installUpdate()
+ root.updateManualReady.active = false
+ }
+ },
+ Action {
+ text: qsTr("Update manually")
+
+ onTriggered: {
+ Qt.openUrlExternally(root.backend.getLandingPage())
root.updateManualReady.active = false
}
},
@@ -104,7 +127,6 @@ QtObject {
text: qsTr("Remind me later")
onTriggered: {
- // TODO: start timer here
root.updateManualReady.active = false
}
}
@@ -128,14 +150,14 @@ QtObject {
text: qsTr("Restart Bridge")
onTriggered: {
- // TODO
+ root.backend.restart()
root.updateManualRestartNeeded.active = false
}
}
}
property Notification updateManualError: Notification {
- text: qsTr("Bridge couldn’t update")
+ text: qsTr("Bridge couldn’t update. Please update manually.")
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Warning
group: Notifications.Group.Update
@@ -147,19 +169,28 @@ QtObject {
}
}
- action: Action {
- text: qsTr("Update manually")
+ action: [
+ Action {
+ text: qsTr("Update manually")
- onTriggered: {
- // TODO
- root.updateManualError.active = false
+ onTriggered: {
+ Qt.openUrlExternally(root.backend.getLandingPage())
+ root.updateManualError.active = false
+ }
+ },
+ Action {
+ text: qsTr("Remind me later")
+
+ onTriggered: {
+ root.updateManualReady.active = false
+ }
}
- }
+ ]
}
property Notification updateForce: Notification {
text: qsTr("Update to ProtonMail Bridge") + " " + (data ? data.version : "")
- description: qsTr("This version of Bridge is no longer supported, please update. Learn why. To update manually, go to: https:/protonmail.com/bridge/download")
+ description: qsTr("This version of Bridge is no longer supported, please update.")
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
group: Notifications.Group.Update | Notifications.Group.Dialogs
@@ -175,18 +206,26 @@ QtObject {
action: [
Action {
- text: qsTr("Update")
+ text: qsTr("Install update")
onTriggered: {
- // TODO: trigger update here
+ root.backend.installUpdate()
root.updateForce.active = false
}
},
Action {
- text: qsTr("Quite Bridge")
+ text: qsTr("Update manually")
onTriggered: {
- // TODO: quit Bridge here
+ Qt.openUrlExternally(root.backend.getLandingPage())
+ root.updateForce.active = false
+ }
+ },
+ Action {
+ text: qsTr("Quit Bridge")
+
+ onTriggered: {
+ root.backend.quit()
root.updateForce.active = false
}
}
@@ -195,7 +234,7 @@ QtObject {
property Notification updateForceError: Notification {
text: qsTr("Bridge coudn’t update")
- description: qsTr("You must update manually. Go to: https:/protonmail.com/bridge/download")
+ description: qsTr("You must update manually.")
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
group: Notifications.Group.Update | Notifications.Group.Dialogs
@@ -213,15 +252,15 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
- // TODO: trigger update here
+ Qt.openUrlExternally(root.backend.getLandingPage())
root.updateForceError.active = false
}
},
Action {
- text: qsTr("Quite Bridge")
+ text: qsTr("Quit Bridge")
onTriggered: {
- // TODO: quit Bridge here
+ root.backend.quit()
root.updateForce.active = false
}
}
@@ -245,7 +284,7 @@ QtObject {
text: qsTr("Restart Bridge")
onTriggered: {
- // TODO
+ root.backend.restart()
root.updateSilentRestartNeeded.active = false
}
}
@@ -268,18 +307,105 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
- // TODO
+ Qt.openUrlExternally(root.backend.getLandingPage())
root.updateSilentError.active = false
}
}
}
+ property Notification updateIsLatestVersion: Notification {
+ text: qsTr("Bridge is up to date")
+ icon: "./icons/ic-info-circle-filled.svg"
+ type: Notification.NotificationType.Info
+ group: Notifications.Group.Update
+
+ Connections {
+ target: root.backend
+ onUpdateIsLatestVersion: {
+ root.updateIsLatestVersion.active = true
+ }
+ }
+
+ action: Action {
+ text: qsTr("Ok")
+
+ onTriggered: {
+ root.updateIsLatestVersion.active = false
+ }
+ }
+ }
+
+ property Notification disableBeta: Notification {
+ text: qsTr("Disable beta access?")
+ description: qsTr("This resets Bridge to the current release and will restart the app. Your preferences, cached data, and email client configurations will be cleared. ")
+ icon: "./icons/ic-exclamation-circle-filled.svg"
+ type: Notification.NotificationType.Warning
+ group: Notifications.Group.Update | Notifications.Group.Dialogs
+
+ Connections {
+ target: root
+ onAskDisableBeta: {
+ root.disableBeta.active = true
+ }
+ }
+
+ action: [
+ Action {
+ text: qsTr("Remind me later")
+
+ onTriggered: {
+ root.disableBeta.active = false
+ }
+ },
+ Action {
+ text: qsTr("Disable and restart")
+ onTriggered: {
+ root.backend.toggleBeta(false)
+ root.disableBeta.loading = true
+ }
+ }
+ ]
+ }
+
+ property Notification enableBeta: Notification {
+ text: qsTr("Enable beta access?")
+ description: qsTr("Bridge will update to the latest beta version according to your update preferences. Disabling beta access later on will reset Bridge and require you to reconfigure your client.")
+ icon: "./icons/ic-info-circle-filled.svg"
+ type: Notification.NotificationType.Info
+ group: Notifications.Group.Update | Notifications.Group.Dialogs
+
+ Connections {
+ target: root
+ onAskEnableBeta: {
+ root.enableBeta.active = true
+ }
+ }
+
+ action: [
+ Action {
+ text: qsTr("Enable")
+ onTriggered: {
+ root.backend.toggleBeta(true)
+ root.enableBeta.active = false
+ }
+ },
+ Action {
+ text: qsTr("Cancel")
+
+ onTriggered: {
+ root.enableBeta.active = false
+ }
+ }
+ ]
+ }
+
+
// Bug reports
property Notification bugReportSendSuccess: Notification {
- text: qsTr("Bug report sent")
- description: qsTr("We’ve received your report, thank you! Our team will get back to you as soon as we can.")
+ text: qsTr("Thank you for the report. We'll get back to you as soon as we can.")
+ icon: "./icons/ic-info-circle-filled.svg"
type: Notification.NotificationType.Success
- group: Notifications.Group.Configuration | Notifications.Group.Dialogs
+ group: Notifications.Group.Configuration
Connections {
target: root.backend
@@ -302,10 +428,10 @@ QtObject {
}
property Notification bugReportSendError: Notification {
- text: qsTr("There was a problem")
- description: qsTr("There was a problem with sending your report. Please try again later or contact us directly at security@protonmail.com")
- type: Notification.NotificationType.Warning
- group: Notifications.Group.Configuration | Notifications.Group.Dialogs
+ text: qsTr("Report could not be sent. Try again or email us directly.")
+ icon: "./icons/ic-exclamation-circle-filled.svg"
+ type: Notification.NotificationType.Danger
+ group: Notifications.Group.Configuration
Connections {
target: root.backend
@@ -323,7 +449,7 @@ QtObject {
}
// Cache
- property Notification cacheAnavailable: Notification {
+ property Notification cacheUnavailable: Notification {
text: qsTr("Cache location is unavailable")
description: qsTr("Check the directory or change it in your settings.")
type: Notification.NotificationType.Warning
@@ -331,8 +457,8 @@ QtObject {
Connections {
target: root.backend
- onCacheAnavailable: {
- root.cacheAnavailable.active = true
+ onCacheUnavailable: {
+ root.cacheUnavailable.active = true
}
}
@@ -340,13 +466,15 @@ QtObject {
Action {
text: qsTr("Quit Bridge")
onTriggered: {
- root.cacheAnavailable.active = false
+ root.backend.quit()
+ root.cacheUnavailable.active = false
}
},
Action {
text: qsTr("Change location")
onTriggered: {
- root.cacheAnavailable.active = false
+ root.cacheUnavailable.active = false
+ root.frontendMain.showLocalCacheSettings()
}
}
]
@@ -376,6 +504,31 @@ QtObject {
text: qsTr("Change location")
onTriggered: {
root.cacheCantMove.active = false
+ root.frontendMain.showLocalCacheSettings()
+ }
+ }
+ ]
+ }
+
+ property Notification cacheLocationChangeSuccess: Notification {
+ text: qsTr("Cache location successfully changed")
+ icon: "./icons/ic-info-circle-filled.svg"
+ type: Notification.NotificationType.Success
+ group: Notifications.Group.Configuration
+
+ Connections {
+ target: root.backend
+ onCacheLocationChangeSuccess: {
+ console.log("notify location changed succesfully")
+ root.cacheLocationChangeSuccess.active = true
+ }
+ }
+
+ action: [
+ Action {
+ text: qsTr("Ok")
+ onTriggered: {
+ root.cacheLocationChangeSuccess.active = false
}
}
]
@@ -414,6 +567,7 @@ QtObject {
Action {
text: qsTr("Quit Bridge")
onTriggered: {
+ root.backend.quit()
root.diskFull.active = false
}
},
@@ -421,6 +575,171 @@ QtObject {
text: qsTr("Settings")
onTriggered: {
root.diskFull.active = false
+ root.frontendMain.showLocalCacheSettings()
+ }
+ }
+ ]
+ }
+
+ property Notification enableSplitMode: Notification {
+ text: qsTr("Enable split mode?")
+ description: qsTr("Changing between split and combined address mode will require you to delete your accounts(s) from your email client and begin the setup process from scratch.")
+ type: Notification.NotificationType.Warning
+ group: Notifications.Group.Configuration | Notifications.Group.Dialogs
+
+ property var user
+
+ Connections {
+ target: root
+ onAskEnableSplitMode: {
+ root.enableSplitMode.user = user
+ root.enableSplitMode.active = true
+ }
+ }
+
+
+ Connections {
+ target: (root && root.enableSplitMode && root.enableSplitMode.user ) ? root.enableSplitMode.user : null
+ onToggleSplitModeFinished: {
+ root.enableSplitMode.active = false
+ root.enableSplitMode.loading = false
+ }
+ }
+
+ action: [
+ Action {
+ text: qsTr("Cancel")
+ onTriggered: {
+ root.enableSplitMode.active = false
+ }
+ },
+ Action {
+ text: qsTr("Enable split mode")
+ onTriggered: {
+ root.enableSplitMode.loading = true
+ root.enableSplitMode.user.toggleSplitMode(true)
+ }
+ }
+ ]
+ }
+
+ property Notification disableLocalCache: Notification {
+ text: qsTr("Disable local cache?")
+ description: qsTr("This action will clear your local cache, including locally stored messages. Bridge will restart.")
+ type: Notification.NotificationType.Warning
+ group: Notifications.Group.Configuration | Notifications.Group.Dialogs
+
+ Connections {
+ target: root
+ onAskDisableLocalCache: {
+ root.disableLocalCache.active = true
+ }
+ }
+
+
+ Connections {
+ target: root.backend
+ onChangeLocalCacheFinished: {
+ root.disableLocalCache.active = false
+ root.disableLocalCache.loading = false
+ }
+ }
+
+ action: [
+ Action {
+ text: qsTr("Cancel")
+ onTriggered: {
+ root.disableLocalCache.active = false
+ }
+ },
+ Action {
+ text: qsTr("Disable and restart")
+ onTriggered: {
+ root.disableLocalCache.loading = true
+ root.backend.changeLocalCache(false, root.backend.diskCachePath)
+ }
+ }
+ ]
+ }
+
+ property Notification enableLocalCache: Notification {
+ text: qsTr("Enable local cache?")
+ description: qsTr("Bridge will restart.")
+ type: Notification.NotificationType.Warning
+ group: Notifications.Group.Configuration | Notifications.Group.Dialogs
+
+ property var path
+
+ Connections {
+ target: root
+ onAskEnableLocalCache: {
+ root.enableLocalCache.active = true
+ root.enableLocalCache.path = path
+ }
+ }
+
+
+ Connections {
+ target: root.backend
+ onChangeLocalCacheFinished: {
+ root.enableLocalCache.active = false
+ root.enableLocalCache.loading = false
+ }
+ }
+
+ action: [
+ Action {
+ text: qsTr("Enable and restart")
+ onTriggered: {
+ root.enableLocalCache.loading = true
+ root.backend.changeLocalCache(true, root.enableLocalCache.path)
+ }
+ },
+ Action {
+ text: qsTr("Cancel")
+ onTriggered: {
+ root.enableLocalCache.active = false
+ }
+ }
+ ]
+ }
+
+ property Notification resetBridge: Notification {
+ text: qsTr("Reset Bridge?")
+ description: qsTr("This will clear your accounts, preferences, and cached data. You will need to reconfigure your email client. Bridge will automatically restart")
+ type: Notification.NotificationType.Warning
+ group: Notifications.Group.Configuration | Notifications.Group.Dialogs
+
+ property var user
+
+ Connections {
+ target: root
+ onAskResetBridge: {
+ root.resetBridge.active = true
+ }
+ }
+
+
+ Connections {
+ target: root.backend
+ onResetFinished: {
+ root.resetBridge.active = false
+ root.resetBridge.loading = false
+ }
+ }
+
+ action: [
+ Action {
+ text: qsTr("Cancel")
+ onTriggered: {
+ root.resetBridge.active = false
+ }
+ },
+ Action {
+ text: qsTr("Reset and restart")
+ onTriggered: {
+ root.resetBridge.loading = true
+ root.backend.triggerReset()
}
}
]
diff --git a/internal/frontend/qml/PortSettings.qml b/internal/frontend/qml/PortSettings.qml
new file mode 100644
index 00000000..cfde2e49
--- /dev/null
+++ b/internal/frontend/qml/PortSettings.qml
@@ -0,0 +1,154 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+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 _valuesOK: !imapField.error && !smtpField.error
+ 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
+ onEditingFinished: root.validate(imapField)
+ }
+ TextField {
+ id: smtpField
+ colorScheme: root.colorScheme
+ label: qsTr("SMTP port")
+ Layout.preferredWidth: 160
+ onEditingFinished: root.validate(smtpField)
+ }
+ }
+
+ 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._valuesOK && root._valuesChanged
+ onClicked: {
+ submitButton.loading = true
+ root.submit()
+ }
+ }
+
+ Button {
+ colorScheme: root.colorScheme
+ text: qsTr("Cancel")
+ onClicked: root.back()
+ secondary: true
+ }
+
+ Connections {
+ target: root.backend
+
+ onChangePortFinished: submitButton.loading = false
+ }
+ }
+
+ onBack: {
+ root.parent.showGeneralSettings()
+ root.setDefaultValues()
+ }
+
+ function validate(field) {
+ var num = field.text*1
+ if (! (num > 1 && num < 65536) ) {
+ field.error = true
+ field.assistiveText = qsTr("Invalid port number.")
+ return
+ }
+
+ if (imapField.text == smtpField.text) {
+ field.error = true
+ field.assistiveText = qsTr("Port numbers must be different.")
+ return
+ }
+
+ field.error = false
+ field.assistiveText = ""
+ }
+
+ function isPortFree(field) {
+ field.error = false
+ field.assistiveText = ""
+
+ 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.assistiveText = qsTr("Port occupied.")
+ submitButton.loading = false
+ return false
+ }
+ }
+
+ function submit(){
+ submitButton.loading = true
+ if (!isPortFree(imapField)) return
+ if (!isPortFree(smtpField)) return
+ root.backend.changePorts(imapField.text, smtpField.text)
+ }
+
+ function setDefaultValues(){
+ imapField.text = backend.portIMAP
+ smtpField.text = backend.portSMTP
+ }
+
+ Component.onCompleted: root.setDefaultValues()
+}
diff --git a/internal/frontend/qml/Proton/Button.qml b/internal/frontend/qml/Proton/Button.qml
index f1c77c55..4ccb9983 100644
--- a/internal/frontend/qml/Proton/Button.qml
+++ b/internal/frontend/qml/Proton/Button.qml
@@ -246,9 +246,25 @@ T.Button {
}
}
- border.color: control.colorScheme.border_norm
+ 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")
+ }
+ }
}
diff --git a/internal/frontend/qml/Proton/RoundedRectangle.qml b/internal/frontend/qml/Proton/RoundedRectangle.qml
index a2edbd4c..8c94d5bd 100644
--- a/internal/frontend/qml/Proton/RoundedRectangle.qml
+++ b/internal/frontend/qml/Proton/RoundedRectangle.qml
@@ -21,7 +21,7 @@ import QtQuick 2.8
Rectangle {
id: root
- color: Style.transparent
+ color: "transparent"
property color fillColor : Style.currentStyle.background_norm
property color strokeColor : Style.currentStyle.background_strong
diff --git a/internal/frontend/qml/Proton/TextArea.qml b/internal/frontend/qml/Proton/TextArea.qml
index fb4b0a62..4ad302a3 100644
--- a/internal/frontend/qml/Proton/TextArea.qml
+++ b/internal/frontend/qml/Proton/TextArea.qml
@@ -20,6 +20,7 @@ import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
+import "."
Item {
id: root
@@ -86,9 +87,10 @@ Item {
property alias wrapMode: control.wrapMode
implicitWidth: background.width
- implicitHeight: control.implicitHeight +
- Math.max(label.implicitHeight + label.anchors.topMargin + label.anchors.bottomMargin, hint.implicitHeight + hint.anchors.topMargin + hint.anchors.bottomMargin) +
- assistiveText.implicitHeight
+ implicitHeight: control.implicitHeight + Math.max(
+ label.implicitHeight + label.anchors.topMargin + label.anchors.bottomMargin,
+ hint.implicitHeight + hint.anchors.topMargin + hint.anchors.bottomMargin
+ ) + assistiveText.implicitHeight
property alias label: label.text
property alias hint: hint.text
@@ -96,6 +98,8 @@ Item {
property bool error: false
+ signal editingFinished()
+
// Backgroud is moved away from within control as it will be clipped with scrollview
Rectangle {
id: background
@@ -200,12 +204,16 @@ Item {
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)
+ 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
+ )
padding: 8
leftPadding: 12
@@ -216,6 +224,8 @@ Item {
selectionColor: control.palette.highlight
selectedTextColor: control.palette.highlightedText
+ onEditingFinished: root.editingFinished()
+
cursorDelegate: Rectangle {
id: cursor
width: 1
diff --git a/internal/frontend/qml/Proton/Toggle.qml b/internal/frontend/qml/Proton/Toggle.qml
new file mode 100644
index 00000000..2064b3b3
--- /dev/null
+++ b/internal/frontend/qml/Proton/Toggle.qml
@@ -0,0 +1,107 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+import QtQuick 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.13
+import QtQuick.Controls.impl 2.13
+
+RowLayout{
+ id: root
+ property var colorScheme
+ property bool checked
+ property bool disabled
+ property bool hovered
+ property bool loading
+
+ signal clicked
+
+ Rectangle {
+ id: indicator
+ implicitWidth: 40
+ implicitHeight: 24
+
+ radius: 20
+ 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: 12
+ 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
+ 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
+ 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: { root.clicked();}
+ onPressed: {root.hovered = true }
+ onReleased: { root.hovered = containsMouse }
+ }
+ }
+}
diff --git a/internal/frontend/qml/Proton/qmldir b/internal/frontend/qml/Proton/qmldir
index 4a0b011e..a2942c98 100644
--- a/internal/frontend/qml/Proton/qmldir
+++ b/internal/frontend/qml/Proton/qmldir
@@ -34,3 +34,4 @@ RoundedRectangle 4.0 RoundedRectangle.qml
Switch 4.0 Switch.qml
TextArea 4.0 TextArea.qml
TextField 4.0 TextField.qml
+Toggle 4.0 Toggle.qml
diff --git a/internal/frontend/qml/SMTPSettings.qml b/internal/frontend/qml/SMTPSettings.qml
new file mode 100644
index 00000000..d03642e9
--- /dev/null
+++ b/internal/frontend/qml/SMTPSettings.qml
@@ -0,0 +1,120 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+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
+
+ 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("STARTLS")
+ }
+ }
+
+ 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()
+ }
+ }
+
+ Button {
+ colorScheme: root.colorScheme
+ text: qsTr("Cancel")
+ onClicked: root.back()
+ secondary: true
+ }
+
+ Connections {
+ target: root.backend
+
+ onToggleUseSSLFinished: submitButton.loading = false
+ }
+ }
+
+ onBack: {
+ root.parent.showGeneralSettings()
+ root.setDefaultValues()
+ }
+
+ function submit(){
+ submitButton.loading = true
+ root.backend.toggleUseSSLforSMTP(sslButton.checked)
+ }
+
+ function setDefaultValues(){
+ sslButton.checked = root.backend.useSSLforSMTP
+ starttlsButton.checked = !root.backend.useSSLforSMTP
+ }
+
+
+ Component.onCompleted: root.setDefaultValues()
+}
diff --git a/internal/frontend/qml/SettingsItem.qml b/internal/frontend/qml/SettingsItem.qml
new file mode 100644
index 00000000..320f3387
--- /dev/null
+++ b/internal/frontend/qml/SettingsItem.qml
@@ -0,0 +1,105 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+import QtQuick 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.12
+
+import Proton 4.0
+
+ColumnLayout {
+ 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 var type: SettingsItem.ActionType.Toggle
+
+ property bool checked: true
+ property bool disabled: false
+ property bool loading: false
+
+ signal clicked
+
+ spacing: 20
+
+ Layout.fillWidth: true
+ Layout.maximumWidth: root.parent.Layout.maximumWidth
+
+ enum ActionType {
+ Toggle = 1, Button = 2, PrimaryButton = 3
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ ColumnLayout {
+ Label {
+ id:mainLabel
+ colorScheme: root.colorScheme
+ text: root.text
+ type: Label.Body_semibold
+ }
+
+ Label {
+ Layout.minimumWidth: mainLabel.width
+ Layout.maximumWidth: root.Layout.maximumWidth - root.spacing - (
+ toggle.visible ? toggle.width : button.width
+ )
+
+ wrapMode: Text.WordWrap
+ colorScheme: root.colorScheme
+ text: root.description
+ color: root.colorScheme.text_weak
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ Toggle {
+ id: toggle
+ colorScheme: root.colorScheme
+ visible: root.type == SettingsItem.ActionType.Toggle
+
+ checked: root.checked
+ loading: root.loading
+ onClicked: { if (!root.loading) root.clicked() }
+ }
+
+ Button {
+ 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 {
+ Layout.fillWidth: true
+ color: colorScheme.border_weak
+ height: 1
+ }
+}
diff --git a/internal/frontend/qml/SettingsView.qml b/internal/frontend/qml/SettingsView.qml
new file mode 100644
index 00000000..28f31e90
--- /dev/null
+++ b/internal/frontend/qml/SettingsView.qml
@@ -0,0 +1,71 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+import QtQuick 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.13
+import QtQuick.Controls.impl 2.13
+
+import Proton 4.0
+
+ScrollView {
+ id: root
+
+ property var colorScheme
+ property var backend
+ default property alias items: content.children
+
+ signal back()
+
+ property int _leftRightMargins: 64
+ property int _topBottomMargins: 68
+ property int _spacing: 22
+
+ clip: true
+ contentWidth: pane.width
+ contentHeight: pane.height
+
+ RowLayout{
+ id: pane
+ width: root.width
+
+ ColumnLayout {
+ id: content
+ spacing: root._spacing
+ Layout.maximumWidth: root.width - 2*root._leftRightMargins
+ Layout.fillWidth: true
+ Layout.topMargin: root._topBottomMargins
+ Layout.bottomMargin: root._topBottomMargins
+ Layout.leftMargin: root._leftRightMargins
+ Layout.rightMargin: root._leftRightMargins
+ }
+ }
+
+ Button {
+ anchors {
+ top: parent.top
+ left: parent.left
+ topMargin: 10
+ leftMargin: 18
+ }
+ colorScheme: root.colorScheme
+ onClicked: root.back()
+ icon.source: "icons/ic-arrow-left.svg"
+ secondary: true
+ horizontalPadding: 8
+ }
+}
diff --git a/internal/frontend/qml/SetupGuide.qml b/internal/frontend/qml/SetupGuide.qml
index ae3b902e..5799d868 100644
--- a/internal/frontend/qml/SetupGuide.qml
+++ b/internal/frontend/qml/SetupGuide.qml
@@ -30,12 +30,14 @@ Item {
property var backend
property var user
+ property string address
signal dismissed()
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
+
RowLayout {
anchors.fill: parent
spacing: 0
@@ -56,7 +58,7 @@ Item {
Label {
colorScheme: root.colorScheme
- text: user ? user.username : ""
+ text: address
color: root.colorScheme.text_weak
type: Label.LabelType.Lead
}
@@ -80,30 +82,50 @@ Item {
Repeater {
model: clients
- ColumnLayout {
- RowLayout {
- Layout.topMargin: 12
- Layout.bottomMargin: 12
- Layout.leftMargin: 16
- Layout.rightMargin: 16
+ Rectangle {
+ implicitWidth: clientRow.width
+ implicitHeight: clientRow.height
- ColorImage {
- source: model.iconSource
- height: 36
+ ColumnLayout {
+ id: clientRow
+
+ RowLayout {
+ Layout.topMargin: 12
+ Layout.bottomMargin: 12
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+
+ ColorImage {
+ source: model.iconSource
+ height: 36
+ }
+
+ Label {
+ colorScheme: root.colorScheme
+ Layout.leftMargin: 12
+ text: model.name
+ type: Label.LabelType.Body
+ }
}
- 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
}
}
- Rectangle {
- Layout.fillWidth: true
- Layout.preferredHeight: 1
- color: root.colorScheme.border_weak
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ if (model.name != "Apple Mail") {
+ console.log(" TODO configure ", model.name)
+ return
+ }
+ root.user.configureAppleMail(root.address)
+ root.dismissed()
+ }
}
}
}
diff --git a/internal/frontend/qml/SignIn.qml b/internal/frontend/qml/SignIn.qml
index 8698753b..59db1305 100644
--- a/internal/frontend/qml/SignIn.qml
+++ b/internal/frontend/qml/SignIn.qml
@@ -42,18 +42,9 @@ Item {
property var backend
property var window
- // in case of adding new account this property should be undefined
- property var user
+ property alias username: usernameTextField.text
state: "Page 1"
- onUserChanged: {
- stackLayout.currentIndex = 0
- loginNormalLayout.reset()
- passwordTextField.text = ""
- login2FALayout.reset()
- login2PasswordLayout.reset()
- }
-
onLoginAbort: {
stackLayout.currentIndex = 0
loginNormalLayout.reset()
@@ -78,15 +69,15 @@ Item {
}
Connections {
- target: user !== undefined ? user : root.backend
+ target: root.backend
onLoginUsernamePasswordError: {
console.assert(stackLayout.currentIndex == 0, "Unexpected loginUsernamePasswordError")
console.assert(signInButton.loading == true, "Unexpected loginUsernamePasswordError")
stackLayout.loginFailed()
- errorLabel.text = qsTr("Your email and/or password are incorrect")
-
+ if (errorMsg!="") errorLabel.text = errorMsg
+ else errorLabel.text = qsTr("Your email and/or password are incorrect")
}
onLoginFreeUserError: {
@@ -152,6 +143,14 @@ Item {
errorLabel.text = qsTr("Incorrect login credentials. Please try again.")
passwordTextField.text = ""
}
+
+ onLoginFinished: {
+ stackLayout.currentIndex = 0
+ loginNormalLayout.reset()
+ passwordTextField.text = ""
+ login2FALayout.reset()
+ login2PasswordLayout.reset()
+ }
}
ColumnLayout {
@@ -218,8 +217,6 @@ Item {
id: usernameTextField
label: qsTr("Username or email")
- text: user !== undefined ? user.username : ""
-
Layout.fillWidth: true
Layout.topMargin: 24
@@ -304,12 +301,7 @@ Item {
enabled = false
loading = true
- if (root.user !== undefined) {
- root.user.login(usernameTextField.text, passwordTextField.text)
- return
- }
-
- root.login(usernameTextField.text, passwordTextField.text)
+ root.login(usernameTextField.text, Qt.btoa(passwordTextField.text))
}
}
@@ -394,12 +386,7 @@ Item {
enabled = false
loading = true
- if (root.user !== undefined) {
- root.user.login2FA(usernameTextField.text, twoFactorPasswordTextField.text)
- return
- }
-
- root.login2FA(usernameTextField.text, twoFactorPasswordTextField.text)
+ root.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text))
}
}
}
@@ -471,12 +458,7 @@ Item {
enabled = false
loading = true
- if (root.user !== undefined) {
- root.user.login2Password(usernameTextField.text, secondPasswordTextField.text)
- return
- }
-
- root.login2Password(usernameTextField.text, secondPasswordTextField.text)
+ root.login2Password(usernameTextField.text, Qt.btoa(secondPasswordTextField.text))
}
}
}
diff --git a/internal/frontend/qml/StatusWindow.qml b/internal/frontend/qml/StatusWindow.qml
index d74ca461..a2897422 100644
--- a/internal/frontend/qml/StatusWindow.qml
+++ b/internal/frontend/qml/StatusWindow.qml
@@ -22,20 +22,16 @@ import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import Proton 4.0
-import ProtonBackend 1.0
import Notifications 1.0
-// Because of https://bugreports.qt.io/browse/QTBUG-69777 and other bugs alike it is impossible
-// to use Window with flags: Qt.Popup here since it won't close by it's own on click outside.
-PopupWindow {
+Window {
id: root
title: "ProtonMail Bridge"
height: contentLayout.implicitHeight
width: contentLayout.implicitWidth
- minimumHeight: 201
- minimumWidth: 448
+ flags: Qt.FramelessWindowHint
property ColorScheme colorScheme: ProtonStyle.currentStyle
@@ -47,15 +43,19 @@ PopupWindow {
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
@@ -76,13 +76,13 @@ PopupWindow {
}
switch (statusItem.activeNotification.type) {
- case Notification.NotificationType.Danger:
+ case Notification.NotificationType.Danger:
return root.colorScheme.signal_danger
- case Notification.NotificationType.Warning:
+ case Notification.NotificationType.Warning:
return root.colorScheme.signal_warning
- case Notification.NotificationType.Success:
+ case Notification.NotificationType.Success:
return root.colorScheme.signal_success
- case Notification.NotificationType.Info:
+ case Notification.NotificationType.Info:
return root.colorScheme.signal_info
}
}
@@ -149,8 +149,8 @@ PopupWindow {
Layout.fillHeight: true
Layout.maximumHeight: accountListView.count ?
- accountListView.contentHeight / accountListView.count * 3 + accountListView.anchors.topMargin + accountListView.anchors.bottomMargin :
- Number.POSITIVE_INFINITY
+ accountListView.contentHeight / accountListView.count * 3 + accountListView.anchors.topMargin + accountListView.anchors.bottomMargin :
+ Number.POSITIVE_INFINITY
color: root.colorScheme.background_norm
clip: true
@@ -171,13 +171,17 @@ PopupWindow {
interactive: contentHeight > parent.height
snapMode: ListView.SnapToItem
+ boundsBehavior: Flickable.StopAtBounds
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
@@ -187,15 +191,19 @@ PopupWindow {
Layout.margins: 12
- user: modelData
+ user: viewItem.user
colorScheme: root.colorScheme
-
}
+
Button {
Layout.margins: 12
colorScheme: root.colorScheme
- visible: true
- text: "test"
+ visible: !viewItem.user.loggedIn
+ text: qsTr("Sign in")
+ onClicked: {
+ root.showSignIn(viewItem.username)
+ root.visible = false
+ }
}
}
}
@@ -297,4 +305,8 @@ PopupWindow {
}
}
}
+
+ onActiveChanged: {
+ if (!active) root.close()
+ }
}
diff --git a/internal/frontend/qml/WelcomeGuide.qml b/internal/frontend/qml/WelcomeGuide.qml
index 26f56ff9..489092a2 100644
--- a/internal/frontend/qml/WelcomeGuide.qml
+++ b/internal/frontend/qml/WelcomeGuide.qml
@@ -239,7 +239,7 @@ Item {
root.loginAbort(username)
}
- user: (backend.users.count === 1 && backend.users.get(0).loggedIn === false) ? backend.users.get(0) : undefined
+ username: (backend.users.count === 1 && backend.users.get(0).loggedIn === false) ? backend.users.get(0).username : ""
backend: root.backend
window: root.window
}
diff --git a/internal/frontend/qml/icons/ic-chevron-down.svg b/internal/frontend/qml/icons/ic-chevron-down.svg
new file mode 100644
index 00000000..eda8be41
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-chevron-down.svg
@@ -0,0 +1,5 @@
+
diff --git a/internal/frontend/qml/icons/ic-chevron-up.svg b/internal/frontend/qml/icons/ic-chevron-up.svg
new file mode 100644
index 00000000..a190565d
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-chevron-up.svg
@@ -0,0 +1,5 @@
+
diff --git a/internal/frontend/qml/icons/ic-copy.svg b/internal/frontend/qml/icons/ic-copy.svg
new file mode 100644
index 00000000..f27164ab
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-copy.svg
@@ -0,0 +1,4 @@
+
diff --git a/internal/frontend/qml/icons/ic-external-link.svg b/internal/frontend/qml/icons/ic-external-link.svg
new file mode 100644
index 00000000..e2abee05
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-external-link.svg
@@ -0,0 +1,3 @@
+
diff --git a/internal/frontend/qml/icons/ic-info.svg b/internal/frontend/qml/icons/ic-info.svg
new file mode 100644
index 00000000..f9eacb9a
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-info.svg
@@ -0,0 +1,12 @@
+
diff --git a/internal/frontend/qml/icons/ic-trash.svg b/internal/frontend/qml/icons/ic-trash.svg
new file mode 100644
index 00000000..516f894f
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-trash.svg
@@ -0,0 +1,5 @@
+
diff --git a/internal/frontend/qt/dockicon/DockIcon.h b/internal/frontend/qt/dockicon/DockIcon.h
new file mode 100644
index 00000000..e4310044
--- /dev/null
+++ b/internal/frontend/qt/dockicon/DockIcon.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build darwin
+// +build build_qt
+
+#include
+
+void SetDockIconVisibleState(bool visible);
+bool GetDockIconVisibleState();
diff --git a/internal/frontend/qt/dockicon/DockIcon.m b/internal/frontend/qt/dockicon/DockIcon.m
new file mode 100644
index 00000000..82fdc667
--- /dev/null
+++ b/internal/frontend/qt/dockicon/DockIcon.m
@@ -0,0 +1,42 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build darwin
+// +build build_qt
+
+#include "DockIcon.h"
+#include
+
+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;
+ }
+}
diff --git a/internal/frontend/qt/dockicon/dockicon_darwin.go b/internal/frontend/qt/dockicon/dockicon_darwin.go
new file mode 100644
index 00000000..a6352e55
--- /dev/null
+++ b/internal/frontend/qt/dockicon/dockicon_darwin.go
@@ -0,0 +1,33 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build darwin
+// +build build_qt
+
+package dockicon
+
+// #cgo CFLAGS: -x objective-c
+// #cgo LDFLAGS: -framework Cocoa
+// #include "DockIcon.h"
+import "C"
+
+func SetDockIconVisibleState(visible bool) {
+ C.SetDockIconVisibleState(C.bool(visible))
+}
+func GetDockIconVisibleState() bool {
+ return bool(C.GetDockIconVisibleState())
+}
diff --git a/internal/frontend/qt/dockicon/dockicon_default.go b/internal/frontend/qt/dockicon/dockicon_default.go
new file mode 100644
index 00000000..d2927587
--- /dev/null
+++ b/internal/frontend/qt/dockicon/dockicon_default.go
@@ -0,0 +1,26 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build !darwin
+// +build build_qt
+
+package dockicon
+
+func SetDockIconVisibleState(visible bool) {}
+func GetDockIconVisibleState() bool {
+ return true
+}
diff --git a/internal/frontend/qt/frontend.go b/internal/frontend/qt/frontend.go
new file mode 100644
index 00000000..3cba590a
--- /dev/null
+++ b/internal/frontend/qt/frontend.go
@@ -0,0 +1,146 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build build_qt
+
+// Package qt provides communication between Qt/QML frontend and Go backend
+package qt
+
+import (
+ "fmt"
+ "sync"
+
+ "github.com/ProtonMail/go-autostart"
+ "github.com/ProtonMail/proton-bridge/internal/config/settings"
+ "github.com/ProtonMail/proton-bridge/internal/config/useragent"
+ "github.com/ProtonMail/proton-bridge/internal/frontend/types"
+ "github.com/ProtonMail/proton-bridge/internal/locations"
+ "github.com/ProtonMail/proton-bridge/internal/updater"
+ "github.com/ProtonMail/proton-bridge/pkg/listener"
+ "github.com/ProtonMail/proton-bridge/pkg/pmapi"
+ "github.com/sirupsen/logrus"
+ "github.com/therecipe/qt/qml"
+ "github.com/therecipe/qt/widgets"
+)
+
+type FrontendQt struct {
+ programName, programVersion string
+
+ panicHandler types.PanicHandler
+ locations *locations.Locations
+ settings *settings.Settings
+ eventListener listener.Listener
+ updater types.Updater
+ userAgent *useragent.UserAgent
+ bridge types.Bridger
+ noEncConfirmator types.NoEncConfirmator
+ autostart *autostart.App
+ restarter types.Restarter
+
+ authClient pmapi.Client
+ auth *pmapi.Auth
+ password []byte
+
+ newVersionInfo updater.VersionInfo
+
+ log *logrus.Entry
+ usersMtx sync.Mutex
+
+ app *widgets.QApplication
+ engine *qml.QQmlApplicationEngine
+ qml *QMLBackend
+}
+
+func New(
+ version,
+ buildVersion,
+ programName string,
+ showWindowOnStart bool,
+ panicHandler types.PanicHandler,
+ locations *locations.Locations,
+ settings *settings.Settings,
+ eventListener listener.Listener,
+ updater types.Updater,
+ userAgent *useragent.UserAgent,
+ bridge types.Bridger,
+ _ types.NoEncConfirmator,
+ autostart *autostart.App,
+ restarter types.Restarter,
+) *FrontendQt {
+ return &FrontendQt{
+ programName: "Proton Mail Bridge",
+ programVersion: version,
+ log: logrus.WithField("pkg", "frontend/qt"),
+
+ panicHandler: panicHandler,
+ locations: locations,
+ settings: settings,
+ eventListener: eventListener,
+ updater: updater,
+ userAgent: userAgent,
+ bridge: bridge,
+ autostart: autostart,
+ restarter: restarter,
+ }
+}
+
+func (f *FrontendQt) Loop() error {
+ err := f.initiateQtApplication()
+ if err != nil {
+ return err
+ }
+
+ go func() {
+ defer f.panicHandler.HandlePanic()
+ f.watchEvents()
+ }()
+
+ if ret := f.app.Exec(); ret != 0 {
+ err := fmt.Errorf("Event loop ended with return value: %v", ret)
+ f.log.Warn("App exec", err)
+ return err
+ }
+
+ return nil
+}
+
+func (f *FrontendQt) NotifyManualUpdate(version updater.VersionInfo, canInstall bool) {
+ if canInstall {
+ f.qml.UpdateManualReady(version.Version.String())
+ } else {
+ f.qml.UpdateManualError()
+ }
+}
+
+func (f *FrontendQt) SetVersion(version updater.VersionInfo) {
+ f.newVersionInfo = version
+ f.qml.SetReleaseNotesLink(version.ReleaseNotesPage)
+ f.qml.SetLandingPageLink(version.LandingPage)
+}
+
+func (f *FrontendQt) NotifySilentUpdateInstalled() {
+ f.qml.UpdateSilentRestartNeeded()
+}
+
+func (f *FrontendQt) NotifySilentUpdateError(err error) {
+ f.log.WithError(err).Warn("Update failed, asking for manual.")
+ f.qml.UpdateManualError()
+}
+
+func (f *FrontendQt) WaitUntilFrontendIsReady() {
+ // TODO: Implement
+}
diff --git a/internal/frontend/qt/frontend_events.go b/internal/frontend/qt/frontend_events.go
new file mode 100644
index 00000000..13631ec4
--- /dev/null
+++ b/internal/frontend/qt/frontend_events.go
@@ -0,0 +1,85 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build build_qt
+
+// Package qt provides communication between Qt/QML frontend and Go backend
+package qt
+
+import (
+ "strings"
+
+ "github.com/ProtonMail/proton-bridge/internal/events"
+)
+
+func (f *FrontendQt) watchEvents() {
+ f.WaitUntilFrontendIsReady()
+
+ errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
+ credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
+ noActiveKeyForRecipientCh := f.eventListener.ProvideChannel(events.NoActiveKeyForRecipientEvent)
+ internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
+ internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
+ secondInstanceCh := f.eventListener.ProvideChannel(events.SecondInstanceEvent)
+ restartBridgeCh := f.eventListener.ProvideChannel(events.RestartBridgeEvent)
+ addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
+ addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
+ logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
+ updateApplicationCh := f.eventListener.ProvideChannel(events.UpgradeApplicationEvent)
+ userChangedCh := f.eventListener.ProvideChannel(events.UserRefreshEvent)
+ certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
+
+ for {
+ select {
+ case errorDetails := <-errorCh:
+ if strings.Contains(errorDetails, "IMAP failed") {
+ f.qml.PortIssueIMAP()
+ }
+ if strings.Contains(errorDetails, "SMTP failed") {
+ f.qml.PortIssueSMTP()
+ }
+ case <-credentialsErrorCh:
+ f.qml.NotifyHasNoKeychain()
+ case email := <-noActiveKeyForRecipientCh:
+ f.qml.NoActiveKeyForRecipient(email)
+ case <-internetOffCh:
+ f.qml.InternetOff()
+ case <-internetOnCh:
+ f.qml.InternetOn()
+ case <-secondInstanceCh:
+ f.qml.ShowMainWindow()
+ case <-restartBridgeCh:
+ f.restart()
+ case address := <-addressChangedCh:
+ f.qml.AddressChanged(address)
+ case address := <-addressChangedLogoutCh:
+ f.qml.AddressChangedLogout(address)
+ case userID := <-logoutCh:
+ user, err := f.bridge.GetUser(userID)
+ if err != nil {
+ return
+ }
+ f.qml.UserDisconnected(user.Username())
+ case <-updateApplicationCh:
+ f.updateForce()
+ case userID := <-userChangedCh:
+ f.userChanged(userID)
+ case <-certIssue:
+ f.qml.ApiCertIssue()
+ }
+ }
+}
diff --git a/internal/frontend/qt/frontend_help.go b/internal/frontend/qt/frontend_help.go
new file mode 100644
index 00000000..e105e0f4
--- /dev/null
+++ b/internal/frontend/qt/frontend_help.go
@@ -0,0 +1,45 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build build_qt
+
+package qt
+
+func (f *FrontendQt) setVersion() {
+ f.qml.SetVersion(f.programVersion)
+}
+
+func (f *FrontendQt) setLogsPath() {
+ path, err := f.locations.ProvideLogsPath()
+ if err != nil {
+ f.log.WithError(err).Error("Cannot update path folder")
+ return
+ }
+ f.qml.SetLogsPath(path)
+}
+
+func (f *FrontendQt) setLicensePath() {
+ f.qml.SetLicensePath(f.locations.GetLicenseFilePath())
+}
+
+func (f *FrontendQt) setCurrentEmailClient() {
+ f.qml.SetCurrentEmailClient(f.userAgent.String())
+}
+
+func (f *FrontendQt) reportBug(description, address, emailClient string, includeLogs bool) {
+ //TODO
+}
diff --git a/internal/frontend/qt/frontend_init.go b/internal/frontend/qt/frontend_init.go
new file mode 100644
index 00000000..1d213fb8
--- /dev/null
+++ b/internal/frontend/qt/frontend_init.go
@@ -0,0 +1,71 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build build_qt
+
+package qt
+
+import (
+ "errors"
+ qmlLog "github.com/ProtonMail/proton-bridge/internal/frontend/qt/log"
+ "github.com/therecipe/qt/core"
+ "github.com/therecipe/qt/qml"
+ "github.com/therecipe/qt/quickcontrols2"
+ "github.com/therecipe/qt/widgets"
+ "os"
+)
+
+func (f *FrontendQt) initiateQtApplication() error {
+ qmlLog.InstallMessageHandler()
+
+ f.app = widgets.NewQApplication(len(os.Args), os.Args)
+
+ core.QCoreApplication_SetApplicationName(f.programName)
+ core.QCoreApplication_SetApplicationVersion(f.programVersion)
+
+ // High DPI scaling for windows.
+ core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, false)
+ // Software OpenGL: to avoid dedicated GPU.
+ core.QCoreApplication_SetAttribute(core.Qt__AA_UseSoftwareOpenGL, true)
+
+ // Bridge runs background, no window is needed to be opened.
+ f.app.SetQuitOnLastWindowClosed(false)
+
+ // QML Engine and path
+ f.engine = qml.NewQQmlApplicationEngine(f.app)
+
+ f.qml = NewQMLBackend(nil)
+ f.qml.setup(f)
+ f.engine.RootContext().SetContextProperty("go", f.qml)
+
+ f.engine.AddImportPath("qrc:/qml/")
+ f.engine.AddPluginPath("qrc:/qml/")
+
+ // Add style: if colorScheme / style is forgotten we should fallback to
+ // default style and should be Proton
+ quickcontrols2.QQuickStyle_AddStylePath("qrc:/qml/")
+ quickcontrols2.QQuickStyle_SetStyle("Proton")
+
+ f.engine.Load(core.NewQUrl3("qrc:/qml/Bridge.qml", 0))
+
+ // Check QML is loaded properly.
+ if len(f.engine.RootObjects()) == 0 {
+ return errors.New("QML not loaded properly")
+ }
+
+ return nil
+}
diff --git a/internal/frontend/qt/frontend_nogui.go b/internal/frontend/qt/frontend_nogui.go
new file mode 100644
index 00000000..dfe96f03
--- /dev/null
+++ b/internal/frontend/qt/frontend_nogui.go
@@ -0,0 +1,83 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build !build_qt
+
+package qt
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/ProtonMail/go-autostart"
+ "github.com/ProtonMail/proton-bridge/internal/config/settings"
+ "github.com/ProtonMail/proton-bridge/internal/config/useragent"
+ "github.com/ProtonMail/proton-bridge/internal/frontend/types"
+ "github.com/ProtonMail/proton-bridge/internal/locations"
+ "github.com/ProtonMail/proton-bridge/internal/updater"
+ "github.com/ProtonMail/proton-bridge/pkg/listener"
+ "github.com/sirupsen/logrus"
+)
+
+var log = logrus.WithField("pkg", "frontend-nogui") //nolint[gochecknoglobals]
+
+type FrontendHeadless struct{}
+
+func New(
+ version,
+ buildVersion,
+ programName string,
+ showWindowOnStart bool,
+ panicHandler types.PanicHandler,
+ locations *locations.Locations,
+ settings *settings.Settings,
+ eventListener listener.Listener,
+ updater types.Updater,
+ userAgent *useragent.UserAgent,
+ bridge types.Bridger,
+ noEncConfirmator types.NoEncConfirmator,
+ autostart *autostart.App,
+ restarter types.Restarter,
+) *FrontendHeadless {
+ return &FrontendHeadless{}
+}
+
+func (s *FrontendHeadless) Loop() error {
+ log.Info("Check status on localhost:8081")
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "Bridge is running")
+ })
+ return http.ListenAndServe(":8081", nil)
+}
+
+func (s *FrontendHeadless) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) {
+ // NOTE: Save the update somewhere so that it can be installed when user chooses "install now".
+}
+
+func (s *FrontendHeadless) WaitUntilFrontendIsReady() {
+}
+
+func (s *FrontendHeadless) SetVersion(update updater.VersionInfo) {
+}
+
+func (s *FrontendHeadless) NotifySilentUpdateInstalled() {
+}
+
+func (s *FrontendHeadless) NotifySilentUpdateError(err error) {
+}
+
+func (s *FrontendHeadless) InstanceExistAlert() {}
diff --git a/internal/frontend/qt/frontend_settings.go b/internal/frontend/qt/frontend_settings.go
new file mode 100644
index 00000000..1b335659
--- /dev/null
+++ b/internal/frontend/qt/frontend_settings.go
@@ -0,0 +1,151 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build build_qt
+
+package qt
+
+import (
+ "time"
+
+ "github.com/ProtonMail/proton-bridge/internal/config/settings"
+ "github.com/ProtonMail/proton-bridge/internal/frontend/clientconfig"
+ "github.com/ProtonMail/proton-bridge/pkg/keychain"
+ "github.com/ProtonMail/proton-bridge/pkg/ports"
+)
+
+func (f *FrontendQt) setIsDiskCacheEnabled() {
+ //TODO
+}
+
+func (f *FrontendQt) setDiskCachePath() {
+ //TODO
+}
+
+func (f *FrontendQt) changeLocalCache(enableDiskCache bool, diskCachePath string) {
+ //TODO
+}
+
+func (f *FrontendQt) setIsAutostartOn() {
+ f.qml.SetIsAutostartOn(f.autostart.IsEnabled())
+}
+
+func (f *FrontendQt) toggleAutostart(makeItEnabled bool) {
+ defer f.qml.ToggleAutostartFinished()
+ if makeItEnabled == f.autostart.IsEnabled() {
+ f.setIsAutostartOn()
+ return
+ }
+
+ var err error
+ if makeItEnabled {
+ err = f.autostart.Enable()
+ } else {
+ err = f.autostart.Disable()
+ }
+ f.setIsAutostartOn()
+
+ if err != nil {
+ f.log.
+ WithField("makeItEnabled", makeItEnabled).
+ WithField("isEnabled", f.qml.IsAutostartOn()).
+ WithError(err).
+ Error("Autostart change failed")
+ }
+}
+
+func (f *FrontendQt) toggleDoH(makeItEnabled bool) {
+ if f.settings.GetBool(settings.AllowProxyKey) == makeItEnabled {
+ f.qml.SetIsDoHEnabled(makeItEnabled)
+ return
+ }
+ f.settings.SetBool(settings.AllowProxyKey, makeItEnabled)
+ f.restart()
+}
+
+func (f *FrontendQt) toggleUseSSLforSMTP(makeItEnabled bool) {
+ if f.settings.GetBool(settings.SMTPSSLKey) == makeItEnabled {
+ f.qml.SetUseSSLforSMTP(makeItEnabled)
+ return
+ }
+ f.settings.SetBool(settings.SMTPPortKey, makeItEnabled)
+ f.restart()
+}
+
+func (f *FrontendQt) changePorts(imapPort, smtpPort int) {
+ f.settings.SetInt(settings.IMAPPortKey, imapPort)
+ f.settings.SetInt(settings.SMTPPortKey, smtpPort)
+ f.restart()
+}
+
+func (f *FrontendQt) isPortFree(port int) bool {
+ return ports.IsPortFree(port)
+}
+
+func (f *FrontendQt) configureAppleMail(userID, address string) {
+ user, err := f.bridge.GetUser(userID)
+ if err != nil {
+ f.log.WithField("userID", userID).Error("Cannot configure AppleMail for user")
+ return
+ }
+
+ needRestart, err := clientconfig.ConfigureAppleMail(user, address, f.settings)
+ if err != nil {
+ f.log.WithError(err).Error("Apple Mail config failed")
+ }
+
+ if needRestart {
+ // There is delay needed for external window to open
+ time.Sleep(2 * time.Second)
+ f.restart()
+ }
+}
+
+func (f *FrontendQt) triggerReset() {
+ defer f.qml.ResetFinished()
+ //TODO
+ f.restart()
+}
+
+func (f *FrontendQt) setKeychain() {
+ availableKeychain := []string{}
+ for chain := range keychain.Helpers {
+ availableKeychain = append(availableKeychain, chain)
+ }
+ f.qml.SetAvailableKeychain(availableKeychain)
+ f.qml.SetSelectedKeychain(f.bridge.GetKeychainApp())
+}
+
+func (f *FrontendQt) selectKeychain(wantKeychain string) {
+ if f.bridge.GetKeychainApp() == wantKeychain {
+ return
+ }
+
+ f.bridge.SetKeychainApp(wantKeychain)
+ f.restart()
+}
+
+func (f *FrontendQt) restart() {
+ f.log.Info("Restarting bridge")
+ f.restarter.SetToRestart()
+ f.app.Exit(0)
+}
+
+func (f *FrontendQt) quit() {
+ f.log.Warn("Your wish is my command.. I quit!")
+ f.app.Exit(0)
+}
diff --git a/internal/frontend/qt/frontend_updates.go b/internal/frontend/qt/frontend_updates.go
new file mode 100644
index 00000000..9f02e448
--- /dev/null
+++ b/internal/frontend/qt/frontend_updates.go
@@ -0,0 +1,130 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build build_qt
+
+package qt
+
+import (
+ "sync"
+
+ "github.com/ProtonMail/proton-bridge/internal/config/settings"
+ "github.com/ProtonMail/proton-bridge/internal/updater"
+)
+
+var checkingUpdates = sync.Mutex{}
+
+func (f *FrontendQt) checkUpdates() error {
+ version, err := f.updater.Check()
+ if err != nil {
+ return err
+ }
+
+ f.SetVersion(version)
+ return nil
+}
+
+func (f *FrontendQt) checkUpdatesAndNotify(isRequestFromUser bool) {
+ checkingUpdates.Lock()
+ defer checkingUpdates.Lock()
+ defer f.qml.CheckUpdatesFinished()
+
+ if err := f.checkUpdates(); err != nil {
+ f.log.WithError(err).Error("An error occurred while checking updates")
+ if isRequestFromUser {
+ f.qml.UpdateManualError()
+ }
+ return
+ }
+
+ if !f.updater.IsUpdateApplicable(f.newVersionInfo) {
+ f.log.Debug("No need to update")
+ if isRequestFromUser {
+ f.qml.UpdateIsLatestVersion()
+ }
+ return
+ }
+
+ if !f.updater.CanInstall(f.newVersionInfo) {
+ f.log.Debug("A manual update is required")
+ f.qml.UpdateManualReady(f.newVersionInfo.Version.String())
+ return
+ }
+}
+
+func (f *FrontendQt) updateForce() {
+ checkingUpdates.Lock()
+ defer checkingUpdates.Lock()
+
+ version := ""
+ if err := f.checkUpdates(); err == nil {
+ version = f.newVersionInfo.Version.String()
+ }
+
+ f.qml.UpdateForce(version)
+}
+
+func (f *FrontendQt) setIsAutomaticUpdateOn() {
+ f.qml.SetIsAutomaticUpdateOn(f.settings.GetBool(settings.AutoUpdateKey))
+}
+
+func (f *FrontendQt) toggleAutomaticUpdate(makeItEnabled bool) {
+ f.qml.SetIsAutomaticUpdateOn(makeItEnabled)
+ isEnabled := f.settings.GetBool(settings.AutoUpdateKey)
+ if makeItEnabled == isEnabled {
+ return
+ }
+
+ f.settings.SetBool(settings.AutoUpdateKey, makeItEnabled)
+
+ f.checkUpdatesAndNotify(false)
+}
+
+func (f *FrontendQt) setIsBetaEnabled() {
+ channel := f.bridge.GetUpdateChannel()
+ f.qml.SetIsBetaEnabled(channel == updater.EarlyChannel)
+}
+
+func (f *FrontendQt) toggleBeta(makeItEnabled bool) {
+ channel := f.bridge.GetUpdateChannel()
+
+ if makeItEnabled == (channel == updater.EarlyChannel) {
+ f.qml.SetIsBetaEnabled(makeItEnabled)
+ return
+ }
+
+ channel = updater.StableChannel
+ if makeItEnabled {
+ channel = updater.EarlyChannel
+ }
+
+ needRestart, err := f.bridge.SetUpdateChannel(channel)
+ f.setIsBetaEnabled()
+
+ if err != nil {
+ f.log.WithError(err).Warn("Switching udpate channel failed.")
+ f.qml.UpdateManualError()
+ return
+ }
+
+ if needRestart {
+ f.restart()
+ return
+ }
+
+ f.checkUpdatesAndNotify(false)
+}
diff --git a/internal/frontend/qt/frontend_users.go b/internal/frontend/qt/frontend_users.go
new file mode 100644
index 00000000..f26bd298
--- /dev/null
+++ b/internal/frontend/qt/frontend_users.go
@@ -0,0 +1,224 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build build_qt
+
+package qt
+
+import (
+ "context"
+ "encoding/base64"
+ "github.com/ProtonMail/proton-bridge/internal/frontend/types"
+ "github.com/ProtonMail/proton-bridge/pkg/pmapi"
+)
+
+func (f *FrontendQt) loadUsers() {
+ f.usersMtx.Lock()
+ defer f.usersMtx.Unlock()
+
+ f.qml.Users().clear()
+
+ for _, user := range f.bridge.GetUsers() {
+ f.qml.Users().addUser(newQMLUserFromBacked(f, user))
+ }
+
+ // If there are no active accounts.
+ if f.qml.Users().Count() == 0 {
+ f.log.Info("No active accounts")
+ }
+}
+
+func (f *FrontendQt) userChanged(userID string) {
+ f.usersMtx.Lock()
+ defer f.usersMtx.Unlock()
+
+ fUsers := f.qml.Users()
+
+ index := fUsers.indexByID(userID)
+ user, err := f.bridge.GetUser(userID)
+
+ if user == nil || err != nil {
+ if index >= 0 { // delete existing user
+ fUsers.removeUser(index)
+ }
+ return
+ }
+
+ if index < 0 { // add non-existing user
+ fUsers.addUser(newQMLUserFromBacked(f, user))
+ return
+ }
+
+ // update exiting user
+ fUsers.users[index].update(user)
+}
+
+func newQMLUserFromBacked(f *FrontendQt, user types.User) *QMLUser {
+ qu := NewQMLUser(nil)
+ qu.ID = user.ID()
+
+ qu.update(user)
+
+ qu.ConnectToggleSplitMode(func(activateSplitMode bool) {
+ go func() {
+ defer qu.ToggleSplitModeFinished()
+ if activateSplitMode == user.IsCombinedAddressMode() {
+ user.SwitchAddressMode()
+ }
+ qu.SetSplitMode(!user.IsCombinedAddressMode())
+ }()
+ })
+
+ qu.ConnectLogout(func() {
+ qu.SetLoggedIn(false)
+ go user.Logout()
+ })
+
+ qu.ConnectConfigureAppleMail(func(address string) {
+ go f.configureAppleMail(qu.ID, address)
+ })
+
+ return qu
+}
+
+func (f *FrontendQt) login(username, password string) {
+ var err error
+ f.password, err = base64.StdEncoding.DecodeString(password)
+ if err != nil {
+ f.log.WithError(err).Error("Cannot decode password")
+ f.qml.LoginUsernamePasswordError("Cannot decode password")
+ f.loginClean()
+ return
+ }
+
+ f.authClient, f.auth, err = f.bridge.Login(username, f.password)
+ if err != nil {
+ f.qml.LoginUsernamePasswordError(err.Error())
+ f.loginClean()
+ return
+ }
+
+ if f.auth.HasTwoFactor() {
+ f.qml.Login2FARequested()
+ return
+ }
+ if f.auth.HasMailboxPassword() {
+ f.qml.Login2PasswordRequested()
+ return
+ }
+
+ f.finishLogin()
+}
+
+func (f *FrontendQt) login2FA(username, code string) {
+ if f.auth == nil || f.authClient == nil {
+ f.log.Errorf("Login 2FA: authethication incomplete %p %p", f.auth, f.authClient)
+ f.qml.Login2FAErrorAbort("Missing authentication, try again.")
+ f.loginClean()
+ return
+ }
+
+ twoFA, err := base64.StdEncoding.DecodeString(code)
+ if err != nil {
+ f.log.WithError(err).Error("Cannot decode 2fa code")
+ f.qml.LoginUsernamePasswordError("Cannot decode 2fa code")
+ f.loginClean()
+ return
+ }
+
+ err = f.authClient.Auth2FA(context.Background(), string(twoFA))
+ if err == pmapi.ErrBad2FACodeTryAgain {
+ f.log.Warn("Login 2FA: retry 2fa")
+ f.qml.Login2FAError("")
+ return
+ }
+
+ if err == pmapi.ErrBad2FACode {
+ f.log.Warn("Login 2FA: abort 2fa")
+ f.qml.Login2FAErrorAbort("")
+ f.loginClean()
+ return
+ }
+
+ if err != nil {
+ f.log.WithError(err).Warn("Login 2FA: failed.")
+ f.qml.Login2FAErrorAbort(err.Error())
+ f.loginClean()
+ return
+ }
+
+ if f.auth.HasMailboxPassword() {
+ f.qml.Login2PasswordRequested()
+ return
+ }
+
+ f.finishLogin()
+}
+
+func (f *FrontendQt) login2Password(username, mboxPassword string) {
+ var err error
+ f.password, err = base64.StdEncoding.DecodeString(mboxPassword)
+ if err != nil {
+ f.log.WithError(err).Error("Cannot decode mbox password")
+ f.qml.LoginUsernamePasswordError("Cannot decode mbox password")
+ f.loginClean()
+ return
+ }
+
+ f.finishLogin()
+}
+
+func (f *FrontendQt) finishLogin() {
+ defer f.loginClean()
+
+ if f.auth == nil || f.authClient == nil {
+ f.log.Errorf("Finish login: Authethication incomplete %p %p", f.auth, f.authClient)
+ f.qml.Login2PasswordErrorAbort("Missing authentication, try again.")
+ return
+ }
+
+ user, err := f.bridge.FinishLogin(f.authClient, f.auth, f.password)
+ if err != nil {
+ f.log.Errorf("Authethication incomplete %p %p", f.auth, f.authClient)
+ f.qml.Login2PasswordErrorAbort("Missing authentication, try again.")
+ return
+ }
+
+ index := f.qml.Users().indexByID(user.ID())
+ if index < 0 {
+ qu := newQMLUserFromBacked(f, user)
+ qu.SetSetupGuideSeen(false)
+ f.qml.Users().addUser(qu)
+ return
+ }
+
+ f.qml.Users().users[index].update(user)
+ f.qml.LoginFinished()
+}
+
+func (f *FrontendQt) loginAbort(username string) {
+ f.loginClean()
+}
+
+func (f *FrontendQt) loginClean() {
+ f.auth = nil
+ f.authClient = nil
+ for i := range f.password {
+ f.password[i] = '\x00'
+ }
+ f.password = f.password[0:0]
+}
diff --git a/internal/frontend/qt/helpers.go b/internal/frontend/qt/helpers.go
new file mode 100644
index 00000000..50d83ce7
--- /dev/null
+++ b/internal/frontend/qt/helpers.go
@@ -0,0 +1,70 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build build_qt
+
+package qt
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/therecipe/qt/core"
+ "github.com/therecipe/qt/gui"
+)
+
+// getCursorPos returns current mouse position to be able to use in QML
+func getCursorPos() *core.QPoint {
+ return gui.QCursor_Pos()
+}
+
+// newQByteArrayFromString is a wrapper for new QByteArray from string.
+func newQByteArrayFromString(name string) *core.QByteArray {
+ return core.NewQByteArray2(name, len(name))
+}
+
+var (
+ reMultiSpaces = regexp.MustCompile(`\s{2,}`)
+ reStartWithSymbol = regexp.MustCompile(`^[.,/#!$@%^&*;:{}=\-_` + "`" + `~()]`)
+)
+
+// getInitials based on webapp implementation:
+// https://github.com/ProtonMail/WebClients/blob/55d96a8b4afaaa4372fc5f1ef34953f2070fd7ec/packages/shared/lib/helpers/string.ts#L145
+func getInitials(fullName string) string {
+ words := strings.Split(
+ reMultiSpaces.ReplaceAllString(fullName, " "),
+ " ",
+ )
+
+ n := 0
+ for _, word := range words {
+ if !reStartWithSymbol.MatchString(word) {
+ words[n] = word
+ n++
+ }
+ }
+
+ if n == 0 {
+ return "?"
+ }
+
+ initials := words[0][0:1]
+ if n != 1 {
+ initials += words[n-1][0:1]
+ }
+ return strings.ToUpper(initials)
+}
diff --git a/internal/frontend/qt/log/log.cpp b/internal/frontend/qt/log/log.cpp
new file mode 100644
index 00000000..1950b060
--- /dev/null
+++ b/internal/frontend/qt/log/log.cpp
@@ -0,0 +1,44 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+
+// +build build_qt
+
+#include "log.h"
+#include "_cgo_export.h"
+
+#include
+#include
+#include
+#include
+#include
+
+void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
+{
+ Q_UNUSED( type )
+ Q_UNUSED( context )
+ QByteArray localMsg = msg.toUtf8().prepend("WHITESPACE");
+ logMsgPacked(
+ const_cast( (localMsg.constData()) +10 ),
+ localMsg.size()-10
+ );
+}
+
+void InstallMessageHandler() {
+ qInstallMessageHandler(messageHandler);
+}
+
diff --git a/internal/frontend/qt/log/log.go b/internal/frontend/qt/log/log.go
new file mode 100644
index 00000000..5af48931
--- /dev/null
+++ b/internal/frontend/qt/log/log.go
@@ -0,0 +1,46 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build build_qt
+
+// Package log redirects QML logs to logrus
+package log
+
+//#include "log.h"
+import "C"
+
+import (
+ "github.com/sirupsen/logrus"
+ "github.com/therecipe/qt/core"
+)
+
+var logQML = logrus.WithField("pkg", "frontent/qml")
+
+// InstallMessageHandler is registering logQML as logger for QML calls.
+func InstallMessageHandler() {
+ C.InstallMessageHandler()
+}
+
+//export logMsgPacked
+func logMsgPacked(data *C.char, len C.int) {
+ logQML.Warn(C.GoStringN(data, len))
+}
+
+// logDummy is here to trigger qtmoc to create cgo instructions
+type logDummy struct {
+ core.QObject
+}
diff --git a/internal/frontend/qt/log/log.h b/internal/frontend/qt/log/log.h
new file mode 100644
index 00000000..777e97af
--- /dev/null
+++ b/internal/frontend/qt/log/log.h
@@ -0,0 +1,36 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+#pragma once
+
+#ifndef LOGRUS_QML_LOG_H
+#define LOGRUS_QML_LOG_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif // C++
+
+ void InstallMessageHandler();
+ ;
+
+#ifdef __cplusplus
+}
+#endif // C++
+
+#endif // LOGRUS_QML_LOG_H
diff --git a/internal/frontend/qt/qml_backend.go b/internal/frontend/qt/qml_backend.go
new file mode 100644
index 00000000..01f9ce32
--- /dev/null
+++ b/internal/frontend/qt/qml_backend.go
@@ -0,0 +1,203 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build build_qt
+
+package qt
+
+import (
+ "runtime"
+
+ "github.com/ProtonMail/proton-bridge/internal/bridge"
+ "github.com/ProtonMail/proton-bridge/internal/config/settings"
+ dockIcon "github.com/ProtonMail/proton-bridge/internal/frontend/qt/dockicon"
+ "github.com/therecipe/qt/core"
+)
+
+// QMLBackend connects QML frontend with Go backend.
+type QMLBackend struct {
+ core.QObject
+
+ _ func() *core.QPoint `slot:"getCursorPos"`
+ _ func() `slot:"quit"`
+ _ func() `slot:"restart"`
+
+ _ bool `property:dockIconVisible`
+
+ _ QMLUserModel `property:"users"`
+
+ // TODO copy stuff from Bridge_test.qml backend object
+ _ string `property:"goos"`
+
+ _ func(username, password string) `slot:"login"`
+ _ func(username, code string) `slot:"login2FA"`
+ _ func(username, password string) `slot:"login2Password"`
+ _ func(username string) `slot:"loginAbort"`
+ _ func(errorMsg string) `signal:"loginUsernamePasswordError"`
+ _ func(errorMsg string) `signal:"loginFreeUserError"`
+ _ func(errorMsg string) `signal:"loginConnectionError"`
+ _ func() `signal:"login2FARequested"`
+ _ func(errorMsg string) `signal:"login2FAError"`
+ _ func(errorMsg string) `signal:"login2FAErrorAbort"`
+ _ func() `signal:"login2PasswordRequested"`
+ _ func(errorMsg string) `signal:"login2PasswordError"`
+ _ func(errorMsg string) `signal:"login2PasswordErrorAbort"`
+ _ func() `signal:"loginFinished"`
+
+ _ func() `signal:"internetOff"`
+ _ func() `signal:"internetOn"`
+
+ _ func(version string) `signal:"updateManualReady"`
+ _ func() `signal:"updateManualRestartNeeded"`
+ _ func() `signal:"updateManualError"`
+ _ func(version string) `signal:"updateForce"`
+ _ func() `signal:"updateForceError"`
+ _ func() `signal:"updateSilentRestartNeeded"`
+ _ func() `signal:"updateSilentError"`
+ _ func() `signal:"updateIsLatestVersion"`
+ _ func() `slot:"checkUpdates"`
+ _ func() `signal:"checkUpdatesFinished"`
+
+ _ bool `property:"isDiskCacheEnabled"`
+ _ string `property:"diskCachePath"`
+ _ func() `signal:"cacheUnavailable"`
+ _ func() `signal:"cacheCantMove"`
+ _ func() `signal:"cacheLocationChangeSuccess"`
+ _ func() `signal:"diskFull"`
+ _ func(enableDiskCache bool, diskCachePath string) `slot:"changeLocalCache"`
+ _ func() `signal:"changeLocalCacheFinished"`
+
+ _ bool `property:"isAutomaticUpdateOn"`
+ _ func(makeItActive bool) `slot:"toggleAutomaticUpdate"`
+
+ _ bool `property:"isAutostartOn"`
+ _ func(makeItActive bool) `slot:"toggleAutostart"`
+ _ func() `signal:"toggleAutostartFinished"`
+
+ _ bool `property:"isBetaEnabled"`
+ _ func(makeItActive bool) `slot:"toggleBeta"`
+
+ _ bool `property:"isDoHEnabled"`
+ _ func(makeItActive bool) `slot:"toggleDoH"`
+
+ _ bool `property:"useSSLforSMTP"`
+ _ func(makeItActive bool) `slot:"toggleUseSSLforSMTP"`
+ _ func() `signal:"toggleUseSSLFinished"`
+
+ _ string `property:"hostname"`
+ _ int `property:"portIMAP"`
+ _ int `property:"portSMTP"`
+ _ func(imapPort, smtpPort int) `slot:"changePorts"`
+ _ func(port int) bool `slot:"isPortFree"`
+ _ func() `signal:"changePortFinished"`
+ _ func() `signal:"portIssueIMAP"`
+ _ func() `signal:"portIssueSMTP"`
+
+ _ func() `slot:"triggerReset"`
+ _ func() `signal:"resetFinished"`
+
+ _ string `property:"version"`
+ _ string `property:"logsPath"`
+ _ string `property:"licensePath"`
+ _ string `property:"releaseNotesLink"`
+ _ string `property:"landingPageLink"`
+
+ _ string `property:"currentEmailClient"`
+ _ func() `slot:"updateCurrentMailClient"`
+ _ func(description, address, emailClient string, includeLogs bool) `slot:"reportBug"`
+ _ func() `signal:"reportBugFinished"`
+ _ func() `signal:"bugReportSendSuccess"`
+ _ func() `signal:"bugReportSendError"`
+
+ _ []string `property:"availableKeychain"`
+ _ string `property:"selectedKeychain"`
+ _ func(keychain string) `slot:"selectKeychain"`
+ _ func() `signal:"notifyHasNoKeychain"`
+
+ _ func(email string) `signal:noActiveKeyForRecipient`
+ _ func() `signal:showMainWindow`
+
+ _ func(address string) `signal:addressChanged`
+ _ func(address string) `signal:addressChangedLogout`
+ _ func(username string) `signal:userDisconnected`
+ _ func() `signal:apiCertIssue`
+}
+
+func (q *QMLBackend) setup(f *FrontendQt) {
+ q.ConnectGetCursorPos(getCursorPos)
+ q.ConnectQuit(f.quit)
+ q.ConnectRestart(f.restart)
+
+ q.ConnectIsDockIconVisible(func() bool {
+ return dockIcon.GetDockIconVisibleState()
+ })
+ q.ConnectSetDockIconVisible(func(visible bool) {
+ dockIcon.SetDockIconVisibleState(visible)
+ })
+
+ q.SetUsers(NewQMLUserModel(nil))
+ f.loadUsers()
+
+ q.SetGoos(runtime.GOOS)
+
+ q.ConnectLogin(func(u, p string) { go f.login(u, p) })
+ q.ConnectLogin2FA(func(u, p string) { go f.login2FA(u, p) })
+ q.ConnectLogin2Password(func(u, p string) { go f.login2Password(u, p) })
+ q.ConnectLoginAbort(func(u string) { go f.loginAbort(u) })
+
+ go f.checkUpdatesAndNotify(false)
+ q.ConnectCheckUpdates(func() { go f.checkUpdatesAndNotify(true) })
+
+ f.setIsDiskCacheEnabled()
+ f.setDiskCachePath()
+ q.ConnectChangeLocalCache(f.changeLocalCache)
+
+ f.setIsAutomaticUpdateOn()
+ q.ConnectToggleAutomaticUpdate(func(m bool) { go f.toggleAutomaticUpdate(m) })
+
+ f.setIsAutostartOn()
+ q.ConnectToggleAutostart(f.toggleAutostart)
+
+ f.setIsBetaEnabled()
+ q.ConnectToggleBeta(func(m bool) { go f.toggleBeta(m) })
+
+ q.SetIsDoHEnabled(f.settings.GetBool(settings.AllowProxyKey))
+ q.ConnectToggleDoH(f.toggleDoH)
+
+ q.SetUseSSLforSMTP(f.settings.GetBool(settings.SMTPSSLKey))
+ q.ConnectToggleUseSSLforSMTP(f.toggleUseSSLforSMTP)
+
+ q.SetHostname(bridge.Host)
+ q.SetPortIMAP(f.settings.GetInt(settings.IMAPPortKey))
+ q.SetPortSMTP(f.settings.GetInt(settings.SMTPPortKey))
+ q.ConnectChangePorts(f.changePorts)
+ q.ConnectIsPortFree(f.isPortFree)
+
+ q.ConnectTriggerReset(func() { go f.triggerReset() })
+
+ f.setVersion()
+ f.setLogsPath()
+ // release notes link is set by update
+ f.setLicensePath()
+
+ f.setCurrentEmailClient()
+ q.ConnectUpdateCurrentMailClient(func() { go f.setCurrentEmailClient() })
+ q.ConnectReportBug(func(d, a, e string, i bool) { go f.reportBug(d, a, e, i) })
+
+ f.setKeychain()
+ q.ConnectSelectKeychain(func(k string) { go f.selectKeychain(k) })
+}
diff --git a/internal/frontend/qt/qml_users.go b/internal/frontend/qt/qml_users.go
new file mode 100644
index 00000000..ae15aea8
--- /dev/null
+++ b/internal/frontend/qt/qml_users.go
@@ -0,0 +1,135 @@
+// Copyright (c) 2021 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail 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.
+//
+// ProtonMail 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 ProtonMail Bridge. If not, see .
+
+// +build build_qt
+
+package qt
+
+import (
+ "github.com/ProtonMail/proton-bridge/internal/frontend/types"
+ "github.com/therecipe/qt/core"
+)
+
+// QMLUserModel stores list of of users
+type QMLUserModel struct {
+ core.QAbstractListModel
+
+ _ map[int]*core.QByteArray `property:"roles"`
+ _ int `property:"count"`
+ _ func() `constructor:"init"`
+ _ func(row int) *core.QVariant `slot:"get"`
+
+ users []*QMLUser
+}
+
+func (um *QMLUserModel) init() {
+ um.SetRoles(map[int]*core.QByteArray{
+ int(core.Qt__UserRole + 1): newQByteArrayFromString("object"),
+ })
+ um.ConnectRowCount(um.rowCount)
+ um.ConnectData(um.data)
+ um.ConnectGet(um.get)
+ um.users = []*QMLUser{}
+ um.setCount()
+}
+
+func (um *QMLUserModel) data(index *core.QModelIndex, property int) *core.QVariant {
+ if !index.IsValid() {
+ return core.NewQVariant()
+ }
+ return um.get(index.Row())
+}
+
+func (um *QMLUserModel) get(index int) *core.QVariant {
+ if index < 0 || index >= um.rowCount(nil) {
+ return core.NewQVariant()
+ }
+ return um.users[index].ToVariant()
+}
+
+func (um *QMLUserModel) rowCount(*core.QModelIndex) int {
+ return len(um.users)
+}
+
+func (um *QMLUserModel) setCount() {
+ um.SetCount(len(um.users))
+}
+
+func (um *QMLUserModel) addUser(user *QMLUser) {
+ um.BeginInsertRows(core.NewQModelIndex(), um.rowCount(nil), um.rowCount(nil))
+ um.users = append(um.users, user)
+ um.setCount()
+ um.EndInsertRows()
+}
+
+func (um *QMLUserModel) removeUser(row int) {
+ um.BeginRemoveRows(core.NewQModelIndex(), row, row)
+ um.users = append(um.users[:row], um.users[row+1:]...)
+ um.setCount()
+ um.EndRemoveRows()
+}
+
+func (um *QMLUserModel) clear() {
+ um.BeginRemoveRows(core.NewQModelIndex(), 0, um.rowCount(nil))
+ um.users = []*QMLUser{}
+ um.setCount()
+ um.EndRemoveRows()
+}
+
+func (um *QMLUserModel) indexByID(id string) int {
+ for i, qu := range um.users {
+ if id == qu.ID {
+ return i
+ }
+ }
+ return -1
+}
+
+// QMLUser holds data, slots and signals and for user.
+type QMLUser struct {
+ core.QObject
+
+ _ string `property:"username"`
+ _ string `property:"avatarText"`
+ _ bool `property:"loggedIn"`
+ _ bool `property:"splitMode"`
+ _ bool `property:"setupGuideSeen"`
+ _ float32 `property:"usedBytes"`
+ _ float32 `property:"totalBytes"`
+ _ string `property:"password"`
+ _ []string `property:"addresses"`
+
+ _ func(makeItActive bool) `slot:"toggleSplitMode"`
+ _ func() `signal:"toggleSplitModeFinished"`
+ _ func() `slot:"logout"`
+ _ func(address string) `slot:"configureAppleMail"`
+
+ ID string
+}
+
+func (qu *QMLUser) update(user types.User) {
+ username := user.Username()
+ qu.SetAvatarText(getInitials(username))
+ qu.SetUsername(username)
+ qu.SetLoggedIn(user.IsConnected())
+ qu.SetSplitMode(!user.IsCombinedAddressMode())
+ qu.SetSetupGuideSeen(true)
+ qu.SetUsedBytes(1.0) // TODO
+ qu.SetTotalBytes(10000.0) // TODO
+ qu.SetPassword(user.GetBridgePassword())
+ qu.SetAddresses(user.GetAddresses())
+}
diff --git a/internal/frontend/share/Bridge.icns b/internal/frontend/share/Bridge.icns
new file mode 100644
index 00000000..3520a0eb
Binary files /dev/null and b/internal/frontend/share/Bridge.icns differ
diff --git a/internal/frontend/share/logo.ico b/internal/frontend/share/logo.ico
new file mode 100644
index 00000000..2b0e2f0f
Binary files /dev/null and b/internal/frontend/share/logo.ico differ
diff --git a/internal/frontend/share/logo.svg b/internal/frontend/share/logo.svg
new file mode 100644
index 00000000..dc807142
--- /dev/null
+++ b/internal/frontend/share/logo.svg
@@ -0,0 +1,31 @@
+
+
+
diff --git a/internal/users/user.go b/internal/users/user.go
index b3f60932..5f9ef246 100644
--- a/internal/users/user.go
+++ b/internal/users/user.go
@@ -349,6 +349,7 @@ func (u *User) CheckBridgeLogin(password string) error {
func (u *User) UpdateUser(ctx context.Context) error {
u.lock.Lock()
defer u.lock.Unlock()
+ defer u.listener.Emit(events.UserRefreshEvent, u.userID)
_, err := u.client.UpdateUser(ctx)
if err != nil {
@@ -376,6 +377,7 @@ func (u *User) SwitchAddressMode() error {
u.lock.Lock()
defer u.lock.Unlock()
+ defer u.listener.Emit(events.UserRefreshEvent, u.userID)
u.CloseAllConnections()
@@ -414,7 +416,6 @@ func (u *User) logout() error {
if wasConnected {
u.listener.Emit(events.LogoutEvent, u.userID)
- u.listener.Emit(events.UserRefreshEvent, u.userID)
}
return err
@@ -425,6 +426,7 @@ func (u *User) logout() error {
func (u *User) Logout() error {
u.lock.Lock()
defer u.lock.Unlock()
+ defer u.listener.Emit(events.UserRefreshEvent, u.userID)
u.log.Debug("Logging out user")
diff --git a/internal/users/user_credentials_test.go b/internal/users/user_credentials_test.go
index df994fed..6c4cb030 100644
--- a/internal/users/user_credentials_test.go
+++ b/internal/users/user_credentials_test.go
@@ -41,6 +41,7 @@ func TestUpdateUser(t *testing.T) {
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email}).Return(testCredentials, nil),
+ m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
)
r.NoError(t, user.UpdateUser(context.Background()))
@@ -68,6 +69,7 @@ func TestUserSwitchAddressMode(t *testing.T) {
m.pmapiClient.EXPECT().CountMessages(gomock.Any(), "").Return([]*pmapi.MessagesCount{}, nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(testCredentialsSplit, nil),
+ m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
)
// Check switch to split mode.
@@ -85,6 +87,7 @@ func TestUserSwitchAddressMode(t *testing.T) {
m.pmapiClient.EXPECT().CountMessages(gomock.Any(), "").Return([]*pmapi.MessagesCount{}, nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(testCredentials, nil),
+ m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
)
// Check switch to combined mode.
@@ -105,6 +108,7 @@ func TestLogoutUser(t *testing.T) {
m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil),
m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil),
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"),
+ m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
)
err := user.Logout()
@@ -123,6 +127,7 @@ func TestLogoutUserFailsLogout(t *testing.T) {
m.credentialsStore.EXPECT().Logout("user").Return(nil, errors.New("logout failed")),
m.credentialsStore.EXPECT().Delete("user").Return(nil),
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"),
+ m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
)
err := user.Logout()
diff --git a/internal/users/user_new_test.go b/internal/users/user_new_test.go
index b3b4045b..215856f8 100644
--- a/internal/users/user_new_test.go
+++ b/internal/users/user_new_test.go
@@ -52,8 +52,8 @@ func TestNewUserUnlockFails(t *testing.T) {
m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil),
m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil),
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"),
- m.eventListener.EXPECT().Emit(events.LogoutEvent, "user"),
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
+ m.eventListener.EXPECT().Emit(events.LogoutEvent, "user"),
)
checkNewUserHasCredentials(m, "failed to unlock user: bad password", testCredentialsDisconnected)
diff --git a/internal/users/users.go b/internal/users/users.go
index 01056515..b6b21c63 100644
--- a/internal/users/users.go
+++ b/internal/users/users.go
@@ -351,6 +351,7 @@ func (u *Users) ClearData() error {
func (u *Users) DeleteUser(userID string, clearStore bool) error {
u.lock.Lock()
defer u.lock.Unlock()
+ defer u.events.Emit(events.UserRefreshEvent, userID)
log := log.WithField("user", userID)
diff --git a/internal/users/users_clear_test.go b/internal/users/users_clear_test.go
index 91800e85..af54fb7d 100644
--- a/internal/users/users_clear_test.go
+++ b/internal/users/users_clear_test.go
@@ -32,6 +32,9 @@ func TestClearData(t *testing.T) {
users := testNewUsersWithUsers(t, m)
defer cleanUpUsersData(users)
+ m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
+ m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "users")
+
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "anotheruser@pm.me")
diff --git a/internal/users/users_delete_test.go b/internal/users/users_delete_test.go
index 0aee1052..de9b32cd 100644
--- a/internal/users/users_delete_test.go
+++ b/internal/users/users_delete_test.go
@@ -38,7 +38,9 @@ func TestDeleteUser(t *testing.T) {
m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil),
m.credentialsStore.EXPECT().Delete("user").Return(nil),
)
+ m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
+ m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
err := users.DeleteUser("user", true)
r.NoError(t, err)
@@ -61,7 +63,9 @@ func TestDeleteUserWithFailingLogout(t *testing.T) {
m.credentialsStore.EXPECT().Delete("user").Return(nil),
)
+ m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
+ m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
err := users.DeleteUser("user", true)
r.NoError(t, err)
diff --git a/internal/users/users_new_test.go b/internal/users/users_new_test.go
index c00c42b1..1d286bef 100644
--- a/internal/users/users_new_test.go
+++ b/internal/users/users_new_test.go
@@ -91,6 +91,7 @@ func TestNewUsersWithConnectedUserWithBadToken(t *testing.T) {
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil)
+ m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")