diff --git a/internal/frontend/qml/AccountDelegate.qml b/internal/frontend/qml/AccountDelegate.qml
new file mode 100644
index 00000000..58ca0443
--- /dev/null
+++ b/internal/frontend/qml/AccountDelegate.qml
@@ -0,0 +1,69 @@
+// 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
+
+RowLayout {
+ id: root
+ property var colorScheme: parent.colorScheme
+
+ property var text: "janedoe@protonmail.com"
+ property var avatarText: "jd"
+ property var captionText: "50.5 MB / 20 GB"
+
+ spacing: 16
+
+ Rectangle {
+ id: avatar
+ Layout.preferredHeight: account.height
+ Layout.preferredWidth: account.height
+ radius: 4
+
+ color: root.colorScheme.background_avatar
+
+ ProtonLabel {
+ anchors.centerIn: avatar
+ color: root.colorScheme.text_norm
+ text: root.avatarText.toUpperCase()
+ state: "body"
+ horizontalAlignment: Qt.AlignHCenter
+ verticalAlignment: Qt.AlignVCenter
+ }
+ }
+
+ ColumnLayout {
+ id: account
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ ProtonLabel {
+ text: root.text
+ color: root.colorScheme.text_norm
+ state: "body"
+ }
+
+ ProtonLabel {
+ text: root.captionText
+ color: root.colorScheme.text_weak
+ state: "caption"
+ }
+ }
+}
diff --git a/internal/frontend/qml/AccountView.qml b/internal/frontend/qml/AccountView.qml
new file mode 100644
index 00000000..4e5ed937
--- /dev/null
+++ b/internal/frontend/qml/AccountView.qml
@@ -0,0 +1,48 @@
+// 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: parent.colorScheme
+
+ spacing: 0
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.minimumHeight: 277
+ Layout.maximumHeight: 277
+
+ color: root.colorScheme.background_norm
+
+ ColumnLayout {
+
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ color: root.colorScheme.background_weak
+ }
+}
diff --git a/internal/frontend/qml/Banners.qml b/internal/frontend/qml/Banners.qml
new file mode 100644
index 00000000..19365014
--- /dev/null
+++ b/internal/frontend/qml/Banners.qml
@@ -0,0 +1,121 @@
+// 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 QtQml 2.12
+import QtQuick 2.13
+import Proton 4.0
+import QtQuick.Controls 2.13
+
+Rectangle {
+ id: root
+
+ property var window
+
+ property bool onTop: false
+ property bool blocking: root.nDangers != 0
+ property int nDangers: 0
+
+ color: root.getTransparentVersion(window.colorScheme.text_norm,root.blocking ? 0.5 : 0)
+
+ MouseArea {
+ anchors.fill: root
+ acceptedButtons: root.blocking ? Qt.AllButtons : Qt.NoButton
+ enabled: root.blocking
+ }
+
+ ListModel {
+ id: notifications
+ }
+
+ ListView {
+ id: view
+ anchors.top : root.top
+ anchors.bottom : root.bottom
+ anchors.horizontalCenter : root.horizontalCenter
+ anchors.topMargin : root.height/20
+ anchors.bottomMargin : root.height/20
+
+ layoutDirection: ListView.Vertical
+ verticalLayoutDirection: root.onTop ? ListView.TopToBottom : ListView.BottomToTop
+
+ spacing: 5
+
+ model: notifications
+ delegate: Banner {
+ id: bannerDelegate
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: model.text
+ actionText: model.buttonText
+ state: model.state
+
+ onAccepted: {
+ switch (model.submitAction) {
+ case "update":
+ console.log("I am updating now")
+ break;
+ default:
+ console.log("NOOP")
+ }
+ if (model.state == "danger") root.nDangers-=1
+ anchors.horizontalCenter = undefined
+ notifications.remove(index)
+ }
+ }
+ }
+
+ function notify(descriptionText, buttonText, type = "info", submitAction = "noop") {
+ if (type === "danger") root.nDangers+=1
+ notifications.append({
+ "text": descriptionText,
+ "buttonText": buttonText,
+ "state": type,
+ "submitAction": submitAction
+ })
+ }
+
+ function notifyOnlyPaidUsers(){
+ root.notify(
+ qsTr("Bridge is exclusive to our paid plans. Upgrade your account to use Bridge."),
+ qsTr("ok"), "danger"
+ )
+ }
+
+ function notifyConnectionLostWhileLogin(){
+ root.notify(
+ qsTr("Can't connect to the server. Check your internet connection and try again."),
+ qsTr("ok"), "danger"
+ )
+ }
+
+ function notifyUpdateManually(){
+ root.notify(
+ qsTr("Bridge could not update automatically."),
+ qsTr("update"), "warning", "update"
+ )
+ }
+
+ function notifyUserAdded(){
+ root.notify(
+ qsTr("Your account has been added to Bridge and you are now signed in."),
+ qsTr("ok"), "success"
+ )
+ }
+
+ function getTransparentVersion(original, transparency){
+ return Qt.rgba(original.r, original.g, original.b, transparency)
+ }
+}
diff --git a/internal/frontend/qml/Bridge.qml b/internal/frontend/qml/Bridge.qml
index 382774aa..8f729a56 100644
--- a/internal/frontend/qml/Bridge.qml
+++ b/internal/frontend/qml/Bridge.qml
@@ -15,17 +15,41 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
+import QtQml 2.12
import QtQuick 2.13
import QtQuick.Window 2.13
import Qt.labs.platform 1.0
QtObject {
- // default property list children: []
+ id: root
- property var _mainWindow: MainWindow {
+ property var backend
+ property var users
+
+ signal login(string username, string password)
+ signal login2FA(string username, string code)
+ signal login2Password(string username, string password)
+ signal loginAbort(string username)
+
+ property var mainWindow: MainWindow {
id: mainWindow
- title: "ProtonMail Bridge"
- visible: false
+ visible: true
+
+ backend: root.backend
+ users: root.users
+
+ onLogin: {
+ root.login(username, password)
+ }
+ onLogin2FA: {
+ root.login2FA(username, code)
+ }
+ onLogin2Password: {
+ root.login2Password(username, password)
+ }
+ onLoginAbort: {
+ root.loginAbort(username)
+ }
}
property var _trayMenu: Window {
@@ -33,22 +57,23 @@ QtObject {
title: "window 2"
visible: false
flags: Qt.Dialog
-
- width: 448
}
property var _trayIcon: SystemTrayIcon {
id: trayIcon
visible: true
- iconSource: "./icons/rectangle-systray.png"
+ iconSource: "./icons/ic-systray.svg"
onActivated: {
switch (reason) {
case SystemTrayIcon.Unknown:
break;
case SystemTrayIcon.Context:
+ trayMenu.x = (Screen.desktopAvailableWidth - trayMenu.width) / 2
+ trayMenu.visible = !trayMenu.visible
break
case SystemTrayIcon.DoubleClick:
- break
+ mainWindow.visible = !mainWindow.visible
+ break;
case SystemTrayIcon.Trigger:
trayMenu.x = (Screen.desktopAvailableWidth - trayMenu.width) / 2
trayMenu.visible = !trayMenu.visible
diff --git a/internal/frontend/qml/BridgeTest/UserControl.qml b/internal/frontend/qml/BridgeTest/UserControl.qml
new file mode 100644
index 00000000..826053f9
--- /dev/null
+++ b/internal/frontend/qml/BridgeTest/UserControl.qml
@@ -0,0 +1,208 @@
+// 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 QtQml 2.12
+import QtQuick 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.13
+
+ColumnLayout {
+ property var user
+ property var backend
+
+ spacing : 5
+
+ Layout.fillHeight: true
+ //Layout.fillWidth: true
+
+ property var colorScheme
+
+ TextField {
+ Layout.fillWidth: true
+
+ text: user !== undefined ? user.username : ""
+
+ onEditingFinished: {
+ user.username = text
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Button {
+ //Layout.fillWidth: true
+
+ text: "Login"
+ enabled: user !== undefined && !user.loggedIn && user.username.length > 0
+
+ onClicked: {
+ if (user === backend.loginUser) {
+ var newUserObject = backend.userComponent.createObject(backend, {username: user.username, loggedIn: true})
+ backend.users.append( { object: newUserObject } )
+
+ user.username = ""
+ user.resetLoginRequests()
+ return
+ }
+
+ user.loggedIn = true
+ user.resetLoginRequests()
+ }
+ }
+
+ Button {
+ //Layout.fillWidth: true
+
+ text: "Logout"
+ enabled: user !== undefined && user.loggedIn && user.username.length > 0
+
+ onClicked: {
+ user.loggedIn = false
+ user.resetLoginRequests()
+ }
+ }
+ }
+
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Label {
+ id: loginLabel
+ text: "Login:"
+
+ Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
+ }
+
+ Button {
+ text: "name/pass error"
+ enabled: user !== undefined && user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordProvided
+
+ onClicked: {
+ user.loginUsernamePasswordError()
+ user.resetLoginRequests()
+ }
+ }
+
+ Button {
+ text: "free user error"
+ enabled: user !== undefined && user.isLoginRequested
+ onClicked: {
+ user.loginFreeUserError()
+ user.resetLoginRequests()
+ }
+ }
+
+ Button {
+ text: "connection error"
+ enabled: user !== undefined && user.isLoginRequested
+ onClicked: {
+ user.loginConnectionError()
+ user.resetLoginRequests()
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Label {
+ id: faLabel
+ text: "2FA:"
+
+ Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
+ }
+
+ Button {
+ text: "request"
+
+ enabled: user !== undefined && user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordRequested
+ onClicked: {
+ user.login2FARequested()
+ user.isLogin2FARequested = true
+ }
+ }
+
+ Button {
+ text: "error"
+
+ enabled: user !== undefined && user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
+ onClicked: {
+ user.login2FAError()
+ user.isLogin2FAProvided = false
+ }
+ }
+
+ Button {
+ text: "Abort"
+
+ enabled: user !== undefined && user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
+ onClicked: {
+ user.login2FAErrorAbort()
+ user.resetLoginRequests()
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Label {
+ id: passLabel
+ text: "2 Password:"
+
+ Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
+ }
+
+ Button {
+ text: "request"
+
+ enabled: user !== undefined && user.isLoginRequested && !user.isLogin2PasswordRequested && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
+ onClicked: {
+ user.login2PasswordRequested()
+ user.isLogin2PasswordRequested = true
+ }
+ }
+
+ Button {
+ text: "error"
+
+ enabled: user !== undefined && user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
+ onClicked: {
+ user.login2PasswordError()
+
+ user.isLogin2PasswordProvided = false
+ }
+ }
+
+ Button {
+ text: "Abort"
+
+ enabled: user !== undefined && user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
+ onClicked: {
+ user.login2PasswordErrorAbort()
+ user.resetLoginRequests()
+ }
+ }
+ }
+
+
+ Item {
+ Layout.fillHeight: true
+ }
+}
diff --git a/internal/frontend/qml/BridgeTest/UserList.qml b/internal/frontend/qml/BridgeTest/UserList.qml
new file mode 100644
index 00000000..356f363e
--- /dev/null
+++ b/internal/frontend/qml/BridgeTest/UserList.qml
@@ -0,0 +1,91 @@
+// 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 Proton 4.0
+
+ColumnLayout {
+ id: root
+
+ property var colorScheme
+ property var backend
+
+ property alias currentIndex: usersListView.currentIndex
+ ListView {
+ id: usersListView
+ Layout.fillHeight: true
+ Layout.preferredWidth: 200
+
+ model: backend.usersTest
+ highlightFollowsCurrentItem: true
+
+ delegate: Item {
+
+ implicitHeight: children[0].implicitHeight + anchors.topMargin + anchors.bottomMargin
+ implicitWidth: children[0].implicitWidth + anchors.leftMargin + anchors.rightMargin
+
+ width: usersListView.width
+
+ anchors.margins: 10
+
+ Label {
+ text: modelData.username
+ anchors.margins: 10
+ anchors.fill: parent
+ color: root.colorScheme.text_norm
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ usersListView.currentIndex = index
+ }
+ }
+ }
+ }
+
+ highlight: Rectangle {
+ color: root.colorScheme.interaction_default_active
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Button {
+ text: "+"
+
+ onClicked: {
+ var newUserObject = backend.userComponent.createObject(backend, { username: "test@protonmail.com", loggedIn: false } )
+ backend.users.append( { object: newUserObject } )
+ }
+ }
+ Button {
+ text: "-"
+
+ enabled: usersListView.currentIndex != 0
+
+ onClicked: {
+ // var userObject = backend.users.get(usersListView.currentIndex - 1)
+ backend.users.remove(usersListView.currentIndex - 1)
+ // userObject.deleteLater()
+ }
+ }
+ }
+}
diff --git a/internal/frontend/qml/BridgeTest/UserModel.qml b/internal/frontend/qml/BridgeTest/UserModel.qml
new file mode 100644
index 00000000..8b59819a
--- /dev/null
+++ b/internal/frontend/qml/BridgeTest/UserModel.qml
@@ -0,0 +1,28 @@
+// 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 QtQml.Models 2.12
+
+ListModel {
+ // overriding get method to ignore any role and return directly object itself
+ function get(row) {
+ if (row < 0 || row >= count) {
+ return undefined
+ }
+ return data(index(row, 0), Qt.DisplayRole)
+ }
+}
diff --git a/internal/frontend/qml/Bridge_test.qml b/internal/frontend/qml/Bridge_test.qml
index b772401a..ba998bce 100644
--- a/internal/frontend/qml/Bridge_test.qml
+++ b/internal/frontend/qml/Bridge_test.qml
@@ -15,42 +15,431 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
+import QtQml 2.12
import QtQuick 2.13
import QtQuick.Window 2.13
+import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
+import QtQml.Models 2.12
+
+import Proton 4.0
+
+import "./BridgeTest"
+
Window {
- id: testroot
- width : 250
- height : 600
- flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
+ id: root
+
+ width: 640
+ height: 480
+ x: 100
+ y: 100
+
+ property var colorScheme: ProtonStyle.darkStyle
+
+ flags : Qt.Window | Qt.Dialog
visible : true
- title : "GUI test Window"
- color : "#10101010"
+ title : "Bridge Test GUI"
+ color : colorScheme.background_norm
- Column {
- anchors.horizontalCenter: parent.horizontalCenter
- spacing : 5
- Button {
- text: "Show window"
- onClicked: {
- bridge._mainWindow.visible = true
+ function _log(msg, color) {
+ logTextArea.text += "
" + msg + "
"
+ logTextArea.text += "\n"
+ }
+
+ function log(msg) {
+ console.log(msg)
+ _log(msg, root.colorScheme.signal_info)
+ }
+
+ function error(msg) {
+ console.error(msg)
+ _log(msg, root.colorScheme.signal_danger)
+ }
+
+ // No user object should be put in this list until a successful login
+ property var users: UserModel {
+ id: _users
+
+ onRowsInserted: {
+ for (var i = first; i <= last; i++) {
+ _usersTest.insert(i + 1, { object: get(i) } )
}
}
- Button {
- text: "Hide window"
- onClicked: {
- bridge._mainWindow.visible = false
+
+ onRowsRemoved: {
+ _usersTest.remove(first + 1, first - last + 1)
+ }
+
+ onRowsMoved: {
+ _usersTest.move(start + 1, row + 1, end - start + 1)
+ }
+
+ onDataChanged: {
+ for (var i = topLeft.row; i <= bottomRight.row; i++) {
+ _usersTest.set(i + 1, { object: get(i) } )
}
}
}
- Component.onCompleted : {
- testroot.x= 10
- testroot.y= 100
- bridge._mainWindow.visible = true
+ // this list is used on test gui: it contains same users list as users above + fake user to represent login request of new user on pos 0
+ property var usersTest: UserModel {
+ id: _usersTest
+ }
+
+ property var userComponent: Component {
+ id: _userComponent
+
+ QtObject {
+ property string username: ""
+ property bool loggedIn: false
+
+ signal loginUsernamePasswordError()
+ signal loginFreeUserError()
+ signal loginConnectionError()
+ signal login2FARequested()
+ signal login2FAError()
+ signal login2FAErrorAbort()
+ signal login2PasswordRequested()
+ signal login2PasswordError()
+ signal login2PasswordErrorAbort()
+
+ // Test purpose only:
+ property bool isFakeUser: this === root.loginUser
+
+ function userSignal(msg) {
+ if (isFakeUser) {
+ return
+ }
+
+ root.log("<- User (" + username + "): " + msg)
+ }
+
+ onLoginUsernamePasswordError: {
+ userSignal("loginUsernamePasswordError")
+ }
+ onLoginFreeUserError: {
+ userSignal("loginFreeUserError")
+ }
+ onLoginConnectionError: {
+ userSignal("loginConnectionError")
+ }
+ onLogin2FARequested: {
+ userSignal("login2FARequested")
+ }
+ onLogin2FAError: {
+ userSignal("login2FAError")
+ }
+ onLogin2FAErrorAbort: {
+ userSignal("login2FAErrorAbort")
+ }
+ onLogin2PasswordRequested: {
+ userSignal("login2PasswordRequested")
+ }
+ onLogin2PasswordError: {
+ userSignal("login2PasswordError")
+ }
+ onLogin2PasswordErrorAbort: {
+ userSignal("login2PasswordErrorAbort")
+ }
+
+ function resetLoginRequests() {
+ isLoginRequested = false
+ isLogin2FARequested = false
+ isLogin2FAProvided = false
+ isLogin2PasswordRequested = false
+ isLogin2PasswordProvided = false
+ }
+
+ property bool isLoginRequested: false
+
+ property bool isLogin2FARequested: false
+ property bool isLogin2FAProvided: false
+
+ property bool isLogin2PasswordRequested: false
+ property bool isLogin2PasswordProvided: false
+ }
+ }
+
+ // this it fake user used only for representing first login request
+ property var loginUser
+ Component.onCompleted: {
+ var newLoginUser = _userComponent.createObject()
+ root.loginUser = newLoginUser
+ _usersTest.append({object: newLoginUser})
+
+ newLoginUser.loginUsernamePasswordError.connect(root.loginUsernamePasswordError)
+ newLoginUser.loginFreeUserError.connect(root.loginFreeUserError)
+ newLoginUser.loginConnectionError.connect(root.loginConnectionError)
+ newLoginUser.login2FARequested.connect(root.login2FARequested)
+ newLoginUser.login2FAError.connect(root.login2FAError)
+ newLoginUser.login2FAErrorAbort.connect(root.login2FAErrorAbort)
+ newLoginUser.login2PasswordRequested.connect(root.login2PasswordRequested)
+ newLoginUser.login2PasswordError.connect(root.login2PasswordError)
+ newLoginUser.login2PasswordErrorAbort.connect(root.login2PasswordErrorAbort)
}
- Bridge {id:bridge}
+ TabBar {
+ id: tabBar
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ TabButton {
+ text: "Global settings"
+ }
+
+ TabButton {
+ text: "User control"
+ }
+
+ TabButton {
+ text: "Playground"
+ }
+
+ TabButton {
+ text: "Log"
+ }
+ }
+
+ StackLayout {
+ anchors.top: tabBar.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+
+ currentIndex: tabBar.currentIndex
+
+ anchors.margins: 10
+
+ RowLayout {
+ id: globalTab
+ spacing : 5
+ property alias colorScheme: root.colorScheme
+
+ ColumnLayout {
+ spacing : 5
+
+ property alias colorScheme: globalTab.colorScheme
+
+ ProtonLabel {
+ text: "Global settings"
+ color: globalTab.colorScheme.text_norm
+ }
+
+ ButtonGroup {
+ id: styleRadioGroup
+ }
+
+ RadioButton {
+ Layout.fillWidth: true
+
+ text: "Light UI"
+ checked: ProtonStyle.currentStyle === ProtonStyle.lightStyle
+ ButtonGroup.group: styleRadioGroup
+
+ onCheckedChanged: {
+ if (checked && ProtonStyle.currentStyle !== ProtonStyle.lightStyle) {
+ ProtonStyle.currentStyle = ProtonStyle.lightStyle
+ }
+ }
+ }
+
+ RadioButton {
+ Layout.fillWidth: true
+
+ text: "Dark UI"
+ checked: ProtonStyle.currentStyle === ProtonStyle.darkStyle
+ ButtonGroup.group: styleRadioGroup
+
+ onCheckedChanged: {
+ if (checked && ProtonStyle.currentStyle !== ProtonStyle.darkStyle) {
+ ProtonStyle.currentStyle = ProtonStyle.darkStyle
+ }
+ }
+
+
+ }
+
+ Button {
+ //Layout.fillWidth: true
+
+ text: "Open Bridge"
+ enabled: bridge === undefined || bridge === null
+ onClicked: {
+ bridge = bridgeComponent.createObject()
+ }
+ }
+
+ Button {
+ //Layout.fillWidth: true
+
+ text: "Close Bridge"
+ enabled: bridge !== undefined && bridge !== null
+ onClicked: {
+ bridge.destroy()
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+ }
+
+ ColumnLayout {
+ spacing : 5
+
+ property alias colorScheme: globalTab.colorScheme
+
+ ProtonLabel {
+ text: "Notifications"
+ color: globalTab.colorScheme.text_norm
+ }
+
+ Button {
+ text: "Notify: danger"
+ enabled: bridge !== undefined && bridge !== null
+ onClicked: {
+ bridge.mainWindow.notifyOnlyPaidUsers()
+ }
+ }
+
+ Button {
+ text: "Notify: warning"
+ enabled: bridge !== undefined && bridge !== null
+ onClicked: {
+ bridge.mainWindow.notifyUpdateManually()
+ }
+ }
+
+ Button {
+ text: "Notify: success"
+ enabled: bridge !== undefined && bridge !== null
+ onClicked: {
+ bridge.mainWindow.notifyUserAdded()
+ }
+ }
+
+
+
+ Item {
+ Layout.fillHeight: true
+ }
+ }
+ }
+
+ RowLayout {
+ id: usersTab
+ UserList {
+ id: usersListView
+ Layout.fillHeight: true
+ colorScheme: root.colorScheme
+ backend: root
+ }
+
+ UserControl {
+ colorScheme: root.colorScheme
+ backend: root
+ user: ((root.usersTest.count > usersListView.currentIndex) && usersListView.currentIndex != -1) ? root.usersTest.get(usersListView.currentIndex) : undefined
+ }
+ }
+
+ RowLayout {
+ id: playgroundTab
+
+ property var colorScheme: root.colorScheme
+
+ AccountDelegate{}
+ }
+
+ TextArea {
+ id: logTextArea
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ Layout.preferredWidth: 400
+ Layout.preferredHeight: 200
+
+ textFormat: TextEdit.RichText
+ //readOnly: true
+ }
+ }
+
+ property var bridge
+
+ // this signals are used only when trying to login with new user (i.e. not in users model)
+ signal loginUsernamePasswordError()
+ signal loginFreeUserError()
+ signal loginConnectionError()
+ signal login2FARequested()
+ signal login2FAError()
+ signal login2FAErrorAbort()
+ signal login2PasswordRequested()
+ signal login2PasswordError()
+ signal login2PasswordErrorAbort()
+
+ onLoginUsernamePasswordError: {
+ console.debug("<- loginUsernamePasswordError")
+ }
+ onLoginFreeUserError: {
+ console.debug("<- loginFreeUserError")
+ }
+ onLoginConnectionError: {
+ console.debug("<- loginConnectionError")
+ }
+ onLogin2FARequested: {
+ console.debug("<- login2FARequested")
+ }
+ onLogin2FAError: {
+ console.debug("<- login2FAError")
+ }
+ onLogin2FAErrorAbort: {
+ console.debug("<- login2FAErrorAbort")
+ }
+ onLogin2PasswordRequested: {
+ console.debug("<- login2PasswordRequested")
+ }
+ onLogin2PasswordError: {
+ console.debug("<- login2PasswordError")
+ }
+ onLogin2PasswordErrorAbort: {
+ console.debug("<- login2PasswordErrorAbort")
+ }
+
+ Component {
+ id: bridgeComponent
+
+ 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()
+ }
+ }
+ }
+
+ onClosing: {
+ Qt.quit()
+ }
}
diff --git a/internal/frontend/qml/ContentWrapper.qml b/internal/frontend/qml/ContentWrapper.qml
new file mode 100644
index 00000000..f78815ac
--- /dev/null
+++ b/internal/frontend/qml/ContentWrapper.qml
@@ -0,0 +1,230 @@
+// 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
+
+Item {
+ id: root
+ property var colorScheme: parent.colorScheme
+
+ property var window
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ Rectangle {
+ id: leftBar
+ property var colorScheme: ProtonStyle.prominentStyle
+
+ Layout.minimumWidth: 264
+ Layout.maximumWidth: 320
+ Layout.preferredWidth: 320
+ Layout.fillHeight: true
+
+ color: colorScheme.background_norm
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ RowLayout {
+ id:topLeftBar
+
+ Layout.fillWidth: true
+ Layout.minimumHeight: 60
+ Layout.maximumHeight: 60
+ Layout.preferredHeight: 60
+ spacing: 0
+
+ property var colorScheme: leftBar.colorScheme
+
+ Status {
+ Layout.leftMargin: 16
+ Layout.topMargin: 24
+ Layout.bottomMargin: 17
+
+ Layout.alignment: Qt.AlignHCenter
+ }
+
+ // just a placeholder
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ }
+
+ Button {
+ Layout.minimumHeight: 36
+ Layout.maximumHeight: 36
+ Layout.preferredHeight: 36
+ Layout.minimumWidth: 36
+ Layout.maximumWidth: 36
+ Layout.preferredWidth: 36
+
+ Layout.topMargin: 16
+ Layout.bottomMargin: 9
+ Layout.rightMargin: 4
+
+ horizontalPadding: 0
+
+ icon.source: "./icons/ic-question-circle.svg"
+ }
+
+ Button {
+ Layout.minimumHeight: 36
+ Layout.maximumHeight: 36
+ Layout.preferredHeight: 36
+ Layout.minimumWidth: 36
+ Layout.maximumWidth: 36
+ Layout.preferredWidth: 36
+
+ Layout.topMargin: 16
+ Layout.bottomMargin: 9
+ Layout.rightMargin: 16
+
+ horizontalPadding: 0
+
+ icon.source: "./icons/ic-cog-wheel.svg"
+ }
+ }
+
+ // Separator
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.minimumHeight: 1
+ Layout.maximumHeight: 1
+ color: leftBar.colorScheme.border_weak
+ }
+
+ ListView {
+ id: accounts
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 24
+ Layout.bottomMargin: 24
+
+ spacing: 12
+
+ header: Rectangle {
+ height: headerLabel.height+16
+ color: ProtonStyle.transparent
+ ProtonLabel{
+ id:headerLabel
+ text: qsTr("Accounts")
+ color: leftBar.colorScheme.text_norm
+ state: "body"
+ }
+ }
+
+ model: window.backend.users
+ delegate: AccountDelegate{
+ id: accountDelegate
+ colorScheme: leftBar.colorScheme
+ text: modelData.username
+ }
+ }
+
+ // Separator
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.minimumHeight: 1
+ Layout.maximumHeight: 1
+ color: leftBar.colorScheme.border_weak
+ }
+
+ Item {
+ id: bottomLeftBar
+
+ Layout.fillWidth: true
+ Layout.minimumHeight: 52
+ Layout.maximumHeight: 52
+ Layout.preferredHeight: 52
+
+ property var colorScheme: leftBar.colorScheme
+
+ Button {
+ width: 36
+ height: 36
+
+ anchors.left: parent.left
+ anchors.top: parent.top
+
+ anchors.leftMargin: 16
+ anchors.topMargin: 7
+
+ horizontalPadding: 0
+
+ icon.source: "./icons/ic-plus.svg"
+
+ onClicked: root.showSignIn()
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: rightPlane
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ color: colorScheme.background_norm
+
+ StackLayout {
+ id: rightContent
+ anchors.fill: parent
+
+ AccountView {
+ colorScheme: root.colorScheme
+ }
+
+ GridLayout {
+ SignIn {
+ Layout.topMargin: 68
+ Layout.leftMargin: 80
+ Layout.rightMargin: 80
+ Layout.bottomMargin: 68
+ Layout.preferredWidth: 320
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ colorScheme: root.colorScheme
+ user: (root.window.backend.users.count === 1 && root.window.backend.users.get(0).loggedIn === false) ? root.window.backend.users.get(0) : undefined
+ backend: root.window.backend
+ window: root.window
+
+ onLogin : { root.window.login ( username , password ) }
+ onLogin2FA : { root.window.login2FA ( username , code ) }
+ onLogin2Password : { root.window.login2Password ( username , password ) }
+ onLoginAbort : { root.window.loginAbort ( username ) }
+ }
+ }
+ }
+ }
+ }
+
+
+ function showSignIn() {
+ rightContent.currentIndex = 1
+ }
+}
diff --git a/internal/frontend/qml/DebugWrapper.qml b/internal/frontend/qml/DebugWrapper.qml
new file mode 100644
index 00000000..c8b883f4
--- /dev/null
+++ b/internal/frontend/qml/DebugWrapper.qml
@@ -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 .
+
+import QtQuick 2.13
+import QtQuick.Controls 2.12
+
+Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+ border.color: "red"
+ border.width: 1
+ z: parent.z - 1
+
+ Label {
+ text: parent.width + "x" + parent.height
+ anchors.centerIn: parent
+ color: "black"
+ }
+}
diff --git a/internal/frontend/qml/MainWindow.qml b/internal/frontend/qml/MainWindow.qml
index c72ca68e..51fcb1e5 100644
--- a/internal/frontend/qml/MainWindow.qml
+++ b/internal/frontend/qml/MainWindow.qml
@@ -15,6 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
+import QtQml 2.12
import QtQuick 2.13
import QtQuick.Window 2.13
import QtQuick.Layouts 1.12
@@ -22,29 +23,96 @@ import QtQuick.Controls 2.12
import Proton 4.0
+import "tests"
+
Window {
- //currentStyle: Proton.Style.prominentStyle
+ id: root
+ title: "ProtonMail Bridge"
- //Button {
- //
- //}
+ width: 960
+ height: 576
- visible: true
- color: ProtonStyle.currentStyle.background_norm
- //StackLayout {
- // SignIn {
- //
- // }
- //}
+ minimumHeight: contentLayout.implicitHeight
+ minimumWidth: contentLayout.implicitWidth
- Button {
- id: testButton1
- text: "Test button"
+ property var colorScheme: ProtonStyle.currentStyle
+
+ property var backend
+ property var users
+
+
+ property bool isNoUser: backend.users.count === 0
+ property bool isNoLoggedUser: backend.users.count === 1 && backend.users.get(0).loggedIn === false
+ property bool showSetup: true
+
+ signal login(string username, string password)
+ signal login2FA(string username, string code)
+ signal login2Password(string username, string password)
+ signal loginAbort(string username)
+
+ StackLayout {
+ id: contentLayout
+
+ anchors.fill: parent
+
+ currentIndex: (root.isNoUser || root.isNoLoggedUser) ? 0 : ( root.showSetup ? 1 : 2)
+
+ WelcomeWindow {
+ colorScheme: root.colorScheme
+ backend: root.backend
+ window: root
+ enabled: !banners.blocking
+
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ onLogin: {
+ root.login(username, password)
}
-
- Button {
- anchors.top: testButton1.bottom
- secondary: true
- text: "Test button"
+ onLogin2FA: {
+ root.login2FA(username, code)
+ }
+ onLogin2Password: {
+ root.login2Password(username, password)
+ }
+ onLoginAbort: {
+ root.loginAbort(username)
}
}
+
+ SetupGuide {
+ colorScheme: root.colorScheme
+ window: root
+ enabled: !banners.blocking
+
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ }
+
+ ContentWrapper {
+ colorScheme: root.colorScheme
+ window: root
+ enabled: !banners.blocking
+
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ }
+ }
+
+ Banners {
+ id: banners
+ anchors.fill: parent
+ window: root
+ onTop: contentLayout.currentIndex == 0
+ }
+
+ function notifyOnlyPaidUsers() { banners.notifyOnlyPaidUsers() }
+ function notifyConnectionLostWhileLogin() { banners.notifyConnectionLostWhileLogin() }
+ function notifyUpdateManually() { banners.notifyUpdateManually() }
+ function notifyUserAdded() { banners.notifyUserAdded() }
+
+ function showSetupGuide(user) {
+ setupGuide.user = user
+ root.showSetup = true
+ }
+}
diff --git a/internal/frontend/qml/Proton/Banner.qml b/internal/frontend/qml/Proton/Banner.qml
new file mode 100644
index 00000000..ba1d8cc4
--- /dev/null
+++ b/internal/frontend/qml/Proton/Banner.qml
@@ -0,0 +1,117 @@
+// 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 QtQml 2.12
+import QtQuick 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls.impl 2.12
+
+Rectangle {
+ id: root
+
+ width: layout.width
+ height: layout.height
+
+ radius: 10
+
+ signal accepted()
+
+
+ property alias text: description.text
+ property var actionText: ""
+
+ property var colorText: Style.currentStyle.text_invert
+ property var colorMain: "#000"
+ property var colorHover: "#000"
+ property var colorActive: "#000"
+ property var iconSource: "../icons/ic-exclamation-circle-filled.svg"
+
+ color: root.colorMain
+ border.color: root.colorActive
+ border.width: 1
+
+ property var maxWidth: 600
+ property var minWidth: 400
+ property var usedWidth: button.width + icon.width
+
+ RowLayout {
+ id: layout
+
+ IconLabel {
+ id:icon
+ Layout.alignment: Qt.AlignCenter
+ Layout.leftMargin: 17.5
+ Layout.topMargin: 15.5
+ Layout.bottomMargin: 15.5
+ color: root.colorText
+ icon.source: root.iconSource
+ icon.color: root.colorText
+ icon.height: Style.title_line_height
+ }
+
+ ProtonLabel {
+ id: description
+ Layout.alignment: Qt.AlignCenter
+ Layout.leftMargin: 9.5
+ Layout.minimumWidth: root.minWidth - root.usedWidth
+ Layout.maximumWidth: root.maxWidth - root.usedWidth
+
+ color: root.colorText
+ state: "body"
+
+ wrapMode: Text.WordWrap
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ Button {
+ id:button
+ Layout.fillHeight: true
+
+ hoverEnabled: true
+
+ text: root.actionText.toUpperCase()
+
+ onClicked: root.accepted()
+
+ background: RoundedRectangle {
+ width:parent.width
+ height:parent.height
+ strokeColor: root.colorActive
+ strokeWidth: root.border.width
+
+ radiusTopRight : root.radius
+ radiusBottomRight : root.radius
+ radiusTopLeft : 0
+ radiusBottomLeft : 0
+
+ fillColor: button.down ? root.colorActive : (
+ button.hovered ? root.colorHover :
+ root.colorMain
+ )
+ }
+ }
+ }
+
+ state: "info"
+ states: [
+ State{ name : "danger" ; PropertyChanges{ target : root ; colorMain : Style.currentStyle.signal_danger ; colorHover : Style.currentStyle.signal_danger_hover ; colorActive : Style.currentStyle.signal_danger_active ; iconSource: "../icons/ic-exclamation-circle-filled.svg"}} ,
+ State{ name : "warning" ; PropertyChanges{ target : root ; colorMain : Style.currentStyle.signal_warning ; colorHover : Style.currentStyle.signal_warning_hover ; colorActive : Style.currentStyle.signal_warning_active ; iconSource: "../icons/ic-exclamation-circle-filled.svg"}} ,
+ State{ name : "success" ; PropertyChanges{ target : root ; colorMain : Style.currentStyle.signal_success ; colorHover : Style.currentStyle.signal_success_hover ; colorActive : Style.currentStyle.signal_success_active ; iconSource: "../icons/ic-info-circle-filled.svg"}} ,
+ State{ name : "info" ; PropertyChanges{ target : root ; colorMain : Style.currentStyle.signal_info ; colorHover : Style.currentStyle.signal_info_hover ; colorActive : Style.currentStyle.signal_info_active ; iconSource: "../icons/ic-info-circle-filled.svg"}}
+ ]
+}
diff --git a/internal/frontend/qml/Proton/Button.qml b/internal/frontend/qml/Proton/Button.qml
index 43edbab0..19fc2409 100644
--- a/internal/frontend/qml/Proton/Button.qml
+++ b/internal/frontend/qml/Proton/Button.qml
@@ -28,49 +28,151 @@ T.Button {
readonly property bool primary: !secondary
readonly property bool isIcon: control.text === ""
+ property bool loading: false
+
+ // TODO: store previous enabled state and restore it?
+ // For now assuming that only enabled buttons could have loading state
+ onLoadingChanged: {
+ if (loading) {
+ enabled = false
+ } else {
+ enabled = true
+ }
+ }
+
id: control
- implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
- implicitContentWidth + leftPadding + rightPadding)
- implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
- implicitContentHeight + topPadding + bottomPadding)
+ implicitWidth: Math.max(
+ implicitBackgroundWidth + leftInset + rightInset,
+ implicitContentWidth + leftPadding + rightPadding
+ )
+ implicitHeight: Math.max(
+ implicitBackgroundHeight + topInset + bottomInset,
+ implicitContentHeight + topPadding + bottomPadding
+ )
padding: 8
horizontalPadding: 16
spacing: 10
- icon.width: 12
- icon.height: 12
- icon.color: control.checked || control.highlighted ? control.palette.brightText :
- control.flat && !control.down ? (control.visualFocus ? control.palette.highlight : control.palette.windowText) : control.palette.buttonText
+ font.family: Style.font_family
+ font.pixelSize: Style.body_font_size
+ font.letterSpacing: Style.body_letter_spacing
- contentItem: IconLabel {
- spacing: control.spacing
- mirrored: control.mirrored
- display: control.display
+ icon.width: 16
+ icon.height: 16
+ icon.color: {
+ if (primary && !isIcon) {
+ return "#FFFFFF"
+ } else {
+ return colorScheme.text_norm
+ }
+ }
- icon: control.icon
- text: control.text
- font: control.font
- color: {
- if (!secondary) {
- // Primary colors
- return "#FFFFFF"
- } else {
- // Secondary colors
- return colorScheme.text_norm
+ contentItem: Item {
+ id: _contentItem
+
+ // Since contentItem is allways resized to maximum available size - we need to "incapsulate" label
+ // and icon within one single item with calculated fixed implicit size
+
+ implicitHeight: labelIcon.implicitHeight
+ implicitWidth: labelIcon.implicitWidth
+
+ Item {
+ id: labelIcon
+
+ anchors.horizontalCenter: _contentItem.horizontalCenter
+ anchors.verticalCenter: _contentItem.verticalCenter
+
+ width: Math.min(implicitWidth, control.availableWidth)
+ height: Math.min(implicitHeight, control.availableHeight)
+
+ implicitWidth: {
+ var textImplicitWidth = control.text !== "" ? label.implicitWidth : 0
+ var iconImplicitWidth = iconImage.source ? iconImage.implicitWidth : 0
+ var spacing = (control.text !== "" && iconImage.source && control.display === AbstractButton.TextBesideIcon) ? control.spacing : 0
+
+ return control.display === AbstractButton.TextBesideIcon ? textImplicitWidth + iconImplicitWidth + spacing : Math.max(textImplicitWidth, iconImplicitWidth)
+ }
+ implicitHeight: {
+ var textImplicitHeight = control.text !== "" ? label.implicitHeight : 0
+ var iconImplicitHeight = iconImage.source ? iconImage.implicitHeight : 0
+ var spacing = (control.text !== "" && iconImage.source && control.display === AbstractButton.TextUnderIcon) ? control.spacing : 0
+
+ return control.display === AbstractButton.TextUnderIcon ? textImplicitHeight + iconImplicitHeight + spacing : Math.max(textImplicitHeight, iconImplicitHeight)
+ }
+
+ Label {
+ id: label
+ anchors.left: labelIcon.left
+ anchors.top: labelIcon.top
+ anchors.bottom: labelIcon.bottom
+ anchors.right: control.loading ? iconImage.left : labelIcon.right
+ anchors.rightMargin: control.loading ? control.spacing : 0
+
+ elide: Text.ElideRight
+ horizontalAlignment: Qt.AlignHCenter
+ verticalAlignment: Qt.AlignVCenter
+
+ text: control.text
+ font: control.font
+ color: {
+ if (primary && !isIcon) {
+ return "#FFFFFF"
+ } else {
+ return colorScheme.text_norm
+ }
+ }
+ opacity: control.enabled || control.loading ? 1.0 : 0.5
+ }
+
+ ColorImage {
+ id: iconImage
+
+ anchors.verticalCenter: labelIcon.verticalCenter
+ anchors.right: labelIcon.right
+
+ width: {
+ // special case for loading since we want icon to be square for rotation animation
+ if (control.loading) {
+ return Math.min(control.icon.width, availableWidth, control.icon.height, availableHeight)
+ }
+
+ return Math.min(control.icon.width, availableWidth)
+ }
+ height: {
+ if (control.loading) {
+ return width
+ }
+
+ Math.min(control.icon.height, availableHeight)
+ }
+
+ color: control.icon.color
+ source: control.loading ? "../icons/Loader_16.svg" : control.icon.source
+ visible: control.loading || control.icon.source
+
+ RotationAnimation {
+ target: iconImage
+ loops: Animation.Infinite
+ duration: 1000
+ from: 0
+ to: 360
+ direction: RotationAnimation.Clockwise
+ running: control.loading
+ }
}
}
}
background: Rectangle {
- implicitWidth: 72
+ implicitWidth: 36
implicitHeight: 36
radius: 4
- visible: !control.flat || control.down || control.checked || control.highlighted
+ visible: true
color: {
if (!isIcon) {
- if (!secondary) {
+ if (primary) {
// Primary colors
if (control.down) {
@@ -81,6 +183,10 @@ T.Button {
return colorScheme.interaction_norm_hover
}
+ if (control.loading) {
+ return colorScheme.interaction_norm_hover
+ }
+
return colorScheme.interaction_norm
} else {
// Secondary colors
@@ -93,10 +199,14 @@ T.Button {
return colorScheme.interaction_default_hover
}
+ if (control.loading) {
+ return colorScheme.interaction_default_hover
+ }
+
return colorScheme.interaction_default
}
} else {
- if (!secondary) {
+ if (primary) {
// Primary icon colors
if (control.down) {
@@ -107,6 +217,10 @@ T.Button {
return colorScheme.interaction_default_hover
}
+ if (control.loading) {
+ return colorScheme.interaction_default_hover
+ }
+
return colorScheme.interaction_default
} else {
// Secondary icon colors
@@ -119,10 +233,18 @@ T.Button {
return colorScheme.interaction_default_hover
}
+ if (control.loading) {
+ return colorScheme.interaction_default_hover
+ }
+
return colorScheme.interaction_default
}
}
}
- opacity: control.enabled ? 1.0 : 0.5
+
+ border.color: colorScheme.border_norm
+ border.width: secondary ? 1 : 0
+
+ opacity: control.enabled || control.loading ? 1.0 : 0.5
}
}
diff --git a/internal/frontend/qml/Proton/CheckBox.qml b/internal/frontend/qml/Proton/CheckBox.qml
new file mode 100644
index 00000000..c5144f46
--- /dev/null
+++ b/internal/frontend/qml/Proton/CheckBox.qml
@@ -0,0 +1,132 @@
+// 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.12
+import QtQuick.Controls 2.12
+import QtQuick.Controls.impl 2.12
+import QtQuick.Templates 2.12 as T
+
+T.CheckBox {
+ property var colorScheme: parent.colorScheme ? parent.colorScheme : Style.currentStyle
+
+ property bool error: false
+
+ id: control
+
+ implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
+ implicitContentWidth + leftPadding + rightPadding)
+ implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
+ implicitContentHeight + topPadding + bottomPadding,
+ implicitIndicatorHeight + topPadding + bottomPadding)
+
+ padding: 0
+ spacing: 8
+
+ indicator: Rectangle {
+ implicitWidth: 20
+ implicitHeight: 20
+ radius: 4
+
+ x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
+ y: control.topPadding + (control.availableHeight - height) / 2
+
+ color: {
+ if (!checked) {
+ return colorScheme.background_norm
+ }
+
+ if (!control.enabled) {
+ return colorScheme.field_disabled
+ }
+
+ if (control.error) {
+ return colorScheme.signal_danger
+ }
+
+ if (control.hovered) {
+ return colorScheme.interaction_norm_hover
+ }
+
+ return colorScheme.interaction_norm
+ }
+
+ border.width: control.checked ? 0 : 1
+ border.color: {
+ if (!control.enabled) {
+ return colorScheme.field_disabled
+ }
+
+ if (control.error) {
+ return colorScheme.signal_danger
+ }
+
+ if (control.hovered) {
+ return colorScheme.interaction_norm_hover
+ }
+
+ return colorScheme.field_norm
+ }
+
+ ColorImage {
+ x: (parent.width - width) / 2
+ y: (parent.height - height) / 2
+
+ width: parent.width - 4
+ height: parent.height - 4
+ color: "#FFFFFF"
+ source: "../icons/ic-check.svg"
+ visible: control.checkState === Qt.Checked
+ }
+
+ // TODO: do we need PartiallyChecked state?
+
+ //Rectangle {
+ // x: (parent.width - width) / 2
+ // y: (parent.height - height) / 2
+ // width: 16
+ // height: 3
+ // color: control.palette.text
+ // visible: control.checkState === Qt.PartiallyChecked
+ //}
+ }
+
+ contentItem: CheckLabel {
+ leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
+ rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
+
+ text: control.text
+
+ color: {
+ if (!enabled) {
+ return colorScheme.text_disabled
+ }
+
+ if (error) {
+ return colorScheme.signal_danger
+ }
+
+ return colorScheme.text_norm
+ }
+
+ font.family: Style.font_family
+ font.weight: Style.fontWidth_400
+ font.pixelSize: 14
+ lineHeight: 20
+ lineHeightMode: Text.FixedHeight
+ font.letterSpacing: 0.2
+ }
+}
diff --git a/internal/frontend/qml/Proton/ColorScheme.qml b/internal/frontend/qml/Proton/ColorScheme.qml
index 3cf677fb..b3ea9845 100644
--- a/internal/frontend/qml/Proton/ColorScheme.qml
+++ b/internal/frontend/qml/Proton/ColorScheme.qml
@@ -17,10 +17,10 @@
import QtQml 2.13
-// https://wiki.qt.io/Qml_Styling
-// http://imaginativethinking.ca/make-qml-component-singleton/
-
QtObject {
+ // should be a pointer to ColorScheme object
+ property var prominent
+
// Primary
property color primay_norm
diff --git a/internal/frontend/qml/Proton/ProtonLabel.qml b/internal/frontend/qml/Proton/ProtonLabel.qml
new file mode 100644
index 00000000..82959a7b
--- /dev/null
+++ b/internal/frontend/qml/Proton/ProtonLabel.qml
@@ -0,0 +1,43 @@
+// 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.Controls 2.12
+
+Label {
+ id: root
+
+ color: Style.currentStyle.text_norm
+ palette.link: Style.currentStyle.interaction_norm
+
+ font.family: ProtonStyle.font_family
+ font.weight: ProtonStyle.fontWidth_400
+ lineHeightMode: Text.FixedHeight
+
+ function putLink(linkURL,linkText) {
+ return `${linkText}`
+ }
+
+ state: "title"
+ states: [
+ State { name : "heading" ; PropertyChanges { target : root ; font.pixelSize : Style.heading_font_size ; lineHeight : Style.heading_line_height } },
+ State { name : "title" ; PropertyChanges { target : root ; font.pixelSize : Style.title_font_size ; lineHeight : Style.title_line_height } },
+ State { name : "lead" ; PropertyChanges { target : root ; font.pixelSize : Style.lead_font_size ; lineHeight : Style.lead_line_height } },
+ State { name : "body" ; PropertyChanges { target : root ; font.pixelSize : Style.body_font_size ; lineHeight : Style.body_line_height ; font.letterSpacing : Style.body_letter_spacing } },
+ State { name : "caption" ; PropertyChanges { target : root ; font.pixelSize : Style.caption_font_size ; lineHeight : Style.caption_line_height ; font.letterSpacing : Style.caption_letter_spacing } }
+ ]
+}
diff --git a/internal/frontend/qml/Proton/RadioButton.qml b/internal/frontend/qml/Proton/RadioButton.qml
new file mode 100644
index 00000000..2d5d187c
--- /dev/null
+++ b/internal/frontend/qml/Proton/RadioButton.qml
@@ -0,0 +1,115 @@
+// 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.12
+import QtQuick.Controls 2.12
+import QtQuick.Controls.impl 2.12
+import QtQuick.Templates 2.12 as T
+
+T.RadioButton {
+ property var colorScheme: parent.colorScheme ? parent.colorScheme : Style.currentStyle
+
+ property bool error: false
+
+ id: control
+
+ implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
+ implicitContentWidth + leftPadding + rightPadding)
+ implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
+ implicitContentHeight + topPadding + bottomPadding,
+ implicitIndicatorHeight + topPadding + bottomPadding)
+
+ padding: 0
+ spacing: 8
+
+ indicator: Rectangle {
+ implicitWidth: 20
+ implicitHeight: 20
+ radius: width / 2
+
+ x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
+ y: control.topPadding + (control.availableHeight - height) / 2
+
+ color: colorScheme.background_norm
+ border.width: 1
+ border.color: {
+ if (!control.enabled) {
+ return colorScheme.field_disabled
+ }
+
+ if (control.error) {
+ return colorScheme.signal_danger
+ }
+
+ if (control.hovered) {
+ return colorScheme.interaction_norm_hover
+ }
+
+ return colorScheme.field_norm
+ }
+
+ Rectangle {
+ x: (parent.width - width) / 2
+ y: (parent.height - height) / 2
+ width: 8
+ height: 8
+ radius: width / 2
+ color: {
+ if (!control.enabled) {
+ return colorScheme.field_disabled
+ }
+
+ if (control.error) {
+ return colorScheme.signal_danger
+ }
+
+ if (control.hovered) {
+ return colorScheme.interaction_norm_hover
+ }
+
+ return colorScheme.interaction_norm
+ }
+ visible: control.checked
+ }
+ }
+
+ contentItem: CheckLabel {
+ leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
+ rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
+
+ text: control.text
+
+ color: {
+ if (!enabled) {
+ return colorScheme.text_disabled
+ }
+
+ if (error) {
+ return colorScheme.signal_danger
+ }
+
+ return colorScheme.text_norm
+ }
+
+ font.family: Style.font_family
+ font.weight: Style.fontWidth_400
+ font.pixelSize: 14
+ lineHeight: 20
+ lineHeightMode: Text.FixedHeight
+ font.letterSpacing: 0.2
+ }
+}
diff --git a/internal/frontend/qml/Proton/RoundedRectangle.qml b/internal/frontend/qml/Proton/RoundedRectangle.qml
new file mode 100644
index 00000000..a2edbd4c
--- /dev/null
+++ b/internal/frontend/qml/Proton/RoundedRectangle.qml
@@ -0,0 +1,116 @@
+// 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.8
+
+
+Rectangle {
+ id: root
+
+ color: Style.transparent
+
+ property color fillColor : Style.currentStyle.background_norm
+ property color strokeColor : Style.currentStyle.background_strong
+ property real strokeWidth : 1
+
+ property real radiusTopLeft : 10
+ property real radiusBottomLeft : 10
+ property real radiusTopRight : 10
+ property real radiusBottomRight : 10
+
+ function paint() {
+ canvas.requestPaint()
+ }
+
+ onFillColorChanged : root.paint()
+ onStrokeColorChanged : root.paint()
+ onStrokeWidthChanged : root.paint()
+ onRadiusTopLeftChanged : root.paint()
+ onRadiusBottomLeftChanged : root.paint()
+ onRadiusTopRightChanged : root.paint()
+ onRadiusBottomRightChanged : root.paint()
+
+
+ Canvas {
+ id: canvas
+ anchors.fill: root
+
+ onPaint: {
+ var ctx = getContext("2d")
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = root.fillColor
+ ctx.strokeStyle = root.strokeColor
+ ctx.lineWidth = root.strokeWidth
+ var dimensions = {
+ x: ctx.lineWidth,
+ y: ctx.lineWidth,
+ w: canvas.width-2*ctx.lineWidth,
+ h: canvas.height-2*ctx.lineWidth,
+ }
+ var radius = {
+ tl: root.radiusTopLeft,
+ tr: root.radiusTopRight,
+ bl: root.radiusBottomLeft,
+ br: root.radiusBottomRight,
+ }
+
+ root.roundRect(
+ ctx,
+ dimensions,
+ radius, true, true
+ )
+ }
+ }
+
+ // adapted from: https://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas/3368118#3368118
+ function roundRect(ctx, dim, radius, fill, stroke) {
+ if (typeof stroke == 'undefined') {
+ stroke = true;
+ }
+ if (typeof radius === 'undefined') {
+ radius = 5;
+ }
+ if (typeof radius === 'number') {
+ radius = {tl: radius, tr: radius, br: radius, bl: radius};
+ } else {
+ var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0};
+ for (var side in defaultRadius) {
+ radius[side] = radius[side] || defaultRadius[side];
+ }
+ }
+ ctx.beginPath();
+ ctx.moveTo(dim.x + radius.tl, dim.y);
+ ctx.lineTo(dim.x + dim.w - radius.tr, dim.y);
+ ctx.quadraticCurveTo(dim.x + dim.w, dim.y, dim.x + dim.w, dim.y + radius.tr);
+ ctx.lineTo(dim.x + dim.w, dim.y + dim.h - radius.br);
+ ctx.quadraticCurveTo(dim.x + dim.w, dim.y + dim.h, dim.x + dim.w - radius.br, dim.y + dim.h);
+ ctx.lineTo(dim.x + radius.bl, dim.y + dim.h);
+ ctx.quadraticCurveTo(dim.x, dim.y + dim.h, dim.x, dim.y + dim.h - radius.bl);
+ ctx.lineTo(dim.x, dim.y + radius.tl);
+ ctx.quadraticCurveTo(dim.x, dim.y, dim.x + radius.tl, dim.y);
+ ctx.closePath();
+ if (fill) {
+ ctx.fill();
+ }
+ if (stroke) {
+ ctx.stroke();
+ }
+ }
+
+ Component.onCompleted: root.paint()
+}
+
diff --git a/internal/frontend/qml/Proton/Style.qml b/internal/frontend/qml/Proton/Style.qml
index 22840556..ef9ec9bf 100644
--- a/internal/frontend/qml/Proton/Style.qml
+++ b/internal/frontend/qml/Proton/Style.qml
@@ -17,6 +17,7 @@
pragma Singleton
import QtQml 2.13
+import QtQuick 2.12
import "./"
@@ -24,279 +25,271 @@ import "./"
// http://imaginativethinking.ca/make-qml-component-singleton/
QtObject {
-
// TODO: Once we will use Qt >=5.15 this should be refactored with inline components as follows:
// https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components
- //component ColorScheme: QtObject {
- // property color primay_norm
- // ...
- //}
+ // component ColorScheme: QtObject {
+ // property color primay_norm
+ // ...
+ // }
+ // and instead of "var" later on "ColorScheme" should be used (also in each component)
- // and instead of "var" later on "ColorScheme" should be used
+ property var lightStyle: ColorScheme {
+ id: _lightStyle
- property var _lightStyle: ColorScheme {
- id: lightStyle
+ prominent: prominentStyle
- // Primary
- primay_norm: "#657EE4"
+ // Primary
+ primay_norm: "#657EE4"
- // Interaction-norm
- interaction_norm: "#657EE4"
- interaction_norm_hover: "#5064B6"
- interaction_norm_active: "#3C4B88"
+ // Interaction-norm
+ interaction_norm: "#657EE4"
+ interaction_norm_hover: "#5064B6"
+ interaction_norm_active: "#3C4B88"
- // Text
- text_norm: "#262A33"
- text_weak: "#696F7D"
- text_hint: "#A4A9B5"
- text_disabled: "#BABEC7"
- text_invert: "#FFFFFF"
+ // Text
+ text_norm: "#262A33"
+ text_weak: "#696F7D"
+ text_hint: "#A4A9B5"
+ text_disabled: "#BABEC7"
+ text_invert: "#FFFFFF"
- // Field
- field_norm: "#BABEC7"
- field_hover: "#A4A9B5"
- field_disabled: "#D0D3DA"
+ // Field
+ field_norm: "#BABEC7"
+ field_hover: "#A4A9B5"
+ field_disabled: "#D0D3DA"
- // Border
- border_norm: "#D0D3DA"
- border_weak: "#E7E9EC"
+ // Border
+ border_norm: "#D0D3DA"
+ border_weak: "#E7E9EC"
- // Background
- background_norm: "#FFFFFF"
- background_weak: "#F3F4F6"
- background_strong: "#E7E9EC"
- background_avatar: "#A4A9B5"
+ // Background
+ background_norm: "#FFFFFF"
+ background_weak: "#F3F4F6"
+ background_strong: "#E7E9EC"
+ background_avatar: "#A4A9B5"
- // Interaction-weak
- interaction_weak: "#D0D3DA"
- interaction_weak_hover: "#BABEC7"
- interaction_weak_active: "#A4A9B5"
+ // Interaction-weak
+ interaction_weak: "#D0D3DA"
+ interaction_weak_hover: "#BABEC7"
+ interaction_weak_active: "#A4A9B5"
- // Interaction-default
- interaction_default: "#00000000"
- interaction_default_hover: "#33BABEC7"
- interaction_default_active: "#4DBABEC7"
+ // Interaction-default
+ interaction_default: "#00000000"
+ interaction_default_hover: "#33BABEC7"
+ interaction_default_active: "#4DBABEC7"
- // Scrollbar
- scrollbar_norm: "#D0D3DA"
- scrollbar_hover: "#BABEC7"
+ // Scrollbar
+ scrollbar_norm: "#D0D3DA"
+ scrollbar_hover: "#BABEC7"
- // Signal
- signal_danger: "#D42F34"
- signal_danger_hover: "#C7262B"
- signal_danger_active: "#BA1E23"
- signal_warning: "#F5830A"
- signal_warning_hover: "#F5740A"
- signal_warning_active: "#F5640A"
- signal_success: "#1B8561"
- signal_success_hover: "#147857"
- signal_success_active: "#0F6B4C"
- signal_info: "#1578CF"
- signal_info_hover: "#0E6DC2"
- signal_info_active: "#0764B5"
+ // Signal
+ signal_danger: "#D42F34"
+ signal_danger_hover: "#C7262B"
+ signal_danger_active: "#BA1E23"
+ signal_warning: "#F5830A"
+ signal_warning_hover: "#F5740A"
+ signal_warning_active: "#F5640A"
+ signal_success: "#1B8561"
+ signal_success_hover: "#147857"
+ signal_success_active: "#0F6B4C"
+ signal_info: "#1578CF"
+ signal_info_hover: "#0E6DC2"
+ signal_info_active: "#0764B5"
- // Shadows
- shadow_norm: "#FFFFFF"
- shadow_lifted: "#FFFFFF"
+ // Shadows
+ shadow_norm: "#FFFFFF"
+ shadow_lifted: "#FFFFFF"
- // Backdrop
- backdrop_norm: "#7A262A33"
- }
+ // Backdrop
+ backdrop_norm: "#7A262A33"
+ }
- property var _prominentStyle: ColorScheme {
- id: prominentStyle
+ property var prominentStyle: ColorScheme {
+ id: _prominentStyle
- // Primary
- primay_norm: "#657EE4"
+ prominent: this
- // Interaction-norm
- interaction_norm: "#657EE4"
- interaction_norm_hover: "#7D92E8"
- interaction_norm_active: "#98A9EE"
+ // Primary
+ primay_norm: "#657EE4"
- // Text
- text_norm: "#FFFFFF"
- text_weak: "#949BB9"
- text_hint: "#565F84"
- text_disabled: "#444E72"
- text_invert: "#1C223D"
+ // Interaction-norm
+ interaction_norm: "#657EE4"
+ interaction_norm_hover: "#7D92E8"
+ interaction_norm_active: "#98A9EE"
- // Field
- field_norm: "#565F84"
- field_hover: "#949BB9"
- field_disabled: "#353E60"
+ // Text
+ text_norm: "#FFFFFF"
+ text_weak: "#949BB9"
+ text_hint: "#565F84"
+ text_disabled: "#444E72"
+ text_invert: "#1C223D"
- // Border
- border_norm: "#353E60"
- border_weak: "#2D3657"
+ // Field
+ field_norm: "#565F84"
+ field_hover: "#949BB9"
+ field_disabled: "#353E60"
- // Background
- background_norm: "#1C223D"
- background_weak: "#272F4F"
- background_strong: "#2D3657"
- background_avatar: "#444E72"
+ // Border
+ border_norm: "#353E60"
+ border_weak: "#2D3657"
- // Interaction-weak
- interaction_weak: "#353E60"
- interaction_weak_hover: "#444E72"
- interaction_weak_active: "#565F84"
+ // Background
+ background_norm: "#1C223D"
+ background_weak: "#272F4F"
+ background_strong: "#2D3657"
+ background_avatar: "#444E72"
- // Interaction-default
- interaction_default: "#00000000"
- interaction_default_hover: "#4D444E72"
- interaction_default_active: "#66444E72"
+ // Interaction-weak
+ interaction_weak: "#353E60"
+ interaction_weak_hover: "#444E72"
+ interaction_weak_active: "#565F84"
- // Scrollbar
- scrollbar_norm: "#353E60"
- scrollbar_hover: "#444E72"
+ // Interaction-default
+ interaction_default: "#00000000"
+ interaction_default_hover: "#4D444E72"
+ interaction_default_active: "#66444E72"
- // Signal
- signal_danger: "#ED4C51"
- signal_danger_hover: "#F7595E"
- signal_danger_active: "#FF666B"
- signal_warning: "#F5930A"
- signal_warning_hover: "#F5A716"
- signal_warning_active: "#F5B922"
- signal_success: "#349172"
- signal_success_hover: "#339C79"
- signal_success_active: "#31A67F"
- signal_info: "#2C89DB"
- signal_info_hover: "#3491E3"
- signal_info_active: "#3D99EB"
+ // Scrollbar
+ scrollbar_norm: "#353E60"
+ scrollbar_hover: "#444E72"
- // Shadows
- shadow_norm: "#1C223D"
- shadow_lifted: "#1C223D"
+ // Signal
+ signal_danger: "#ED4C51"
+ signal_danger_hover: "#F7595E"
+ signal_danger_active: "#FF666B"
+ signal_warning: "#F5930A"
+ signal_warning_hover: "#F5A716"
+ signal_warning_active: "#F5B922"
+ signal_success: "#349172"
+ signal_success_hover: "#339C79"
+ signal_success_active: "#31A67F"
+ signal_info: "#2C89DB"
+ signal_info_hover: "#3491E3"
+ signal_info_active: "#3D99EB"
- // Backdrop
- backdrop_norm: "#52000000"
- }
+ // Shadows
+ shadow_norm: "#1C223D"
+ shadow_lifted: "#1C223D"
- property var _darkStyle: ColorScheme {
- id: darkStyle
+ // Backdrop
+ backdrop_norm: "#52000000"
+ }
- // Primary
- primay_norm: "#657EE4"
+ property var darkStyle: ColorScheme {
+ id: _darkStyle
- // Interaction-norm
- interaction_norm: "#657EE4"
- interaction_norm_hover: "#7D92E8"
- interaction_norm_active: "#98A9EE"
+ prominent: prominentStyle
- // Text
- text_norm: "#FFFFFF"
- text_weak: "#A4A9B5"
- text_hint: "#696F7D"
- text_disabled: "#575D6B"
- text_invert: "#262A33"
+ // Primary
+ primay_norm: "#657EE4"
- // Field
- field_norm: "#575D6B"
- field_hover: "#696F7D"
- field_disabled: "#464B58"
+ // Interaction-norm
+ interaction_norm: "#657EE4"
+ interaction_norm_hover: "#7D92E8"
+ interaction_norm_active: "#98A9EE"
- // Border
- border_norm: "#464B58"
- border_weak: "#363A46"
+ // Text
+ text_norm: "#FFFFFF"
+ text_weak: "#A4A9B5"
+ text_hint: "#696F7D"
+ text_disabled: "#575D6B"
+ text_invert: "#262A33"
- // Background
- background_norm: "#262A33"
- background_weak: "#2E323C"
- background_strong: "#363A46"
- background_avatar: "#575D6B"
+ // Field
+ field_norm: "#575D6B"
+ field_hover: "#696F7D"
+ field_disabled: "#464B58"
- // Interaction-weak
- interaction_weak: "#464B58"
- interaction_weak_hover: "#575D6B"
- interaction_weak_active: "#696F7D"
+ // Border
+ border_norm: "#464B58"
+ border_weak: "#363A46"
- // Interaction-default
- interaction_default: "#00000000"
- interaction_default_hover: "#33575D6B"
- interaction_default_active: "#4D575D6B"
+ // Background
+ background_norm: "#262A33"
+ background_weak: "#2E323C"
+ background_strong: "#363A46"
+ background_avatar: "#575D6B"
- // Scrollbar
- scrollbar_norm: "#464B58"
- scrollbar_hover: "#575D6B"
+ // Interaction-weak
+ interaction_weak: "#464B58"
+ interaction_weak_hover: "#575D6B"
+ interaction_weak_active: "#696F7D"
- // Signal
- signal_danger: "#ED4C51"
- signal_danger_hover: "#F7595E"
- signal_danger_active: "#FF666B"
- signal_warning: "#F5930A"
- signal_warning_hover: "#F5A716"
- signal_warning_active: "#F5B922"
- signal_success: "#349172"
- signal_success_hover: "#339C79"
- signal_success_active: "#31A67F"
- signal_info: "#2C89DB"
- signal_info_hover: "#3491E3"
- signal_info_active: "#3D99EB"
+ // Interaction-default
+ interaction_default: "#00000000"
+ interaction_default_hover: "#33575D6B"
+ interaction_default_active: "#4D575D6B"
- // Shadows
- shadow_norm: "#262A33"
- shadow_lifted: "#262A33"
+ // Scrollbar
+ scrollbar_norm: "#464B58"
+ scrollbar_hover: "#575D6B"
- // Backdrop
- backdrop_norm: "#52000000"
- }
+ // Signal
+ signal_danger: "#ED4C51"
+ signal_danger_hover: "#F7595E"
+ signal_danger_active: "#FF666B"
+ signal_warning: "#F5930A"
+ signal_warning_hover: "#F5A716"
+ signal_warning_active: "#F5B922"
+ signal_success: "#349172"
+ signal_success_hover: "#339C79"
+ signal_success_active: "#31A67F"
+ signal_info: "#2C89DB"
+ signal_info_hover: "#3491E3"
+ signal_info_active: "#3D99EB"
- // TODO: if default style should be loaded from somewhere - it should be loaded here
- property var currentStyle: lightStyle
+ // Shadows
+ shadow_norm: "#262A33"
+ shadow_lifted: "#262A33"
- property var _timer: Timer {
- interval: 1000
- repeat: true
- running: true
- onTriggered: {
- switch (currentStyle) {
- case lightStyle:
- console.debug("Dark Style")
- currentStyle = darkStyle
- return
- case darkStyle:
- console.debug("Prominent Style")
- currentStyle = prominentStyle
- return
- case prominentStyle:
- console.debug("Light Style")
- currentStyle = lightStyle
- return
- }
- }
- }
+ // Backdrop
+ backdrop_norm: "#52000000"
+ }
+ // TODO: if default style should be loaded from somewhere
+ // (i.e. from preferencies file) - it should be loaded here
+ property var currentStyle: lightStyle
-
- property string font: {
- // TODO: add OS to backend
+ property string font_family: {
+ switch (Qt.platform.os) {
+ case "windows":
+ return "Segoe UI"
+ case "osx":
+ return "SF Pro Display"
+ case "linux":
return "Ubuntu"
-
- //switch (backend.OS) {
- // case "Windows":
- // return "Segoe UI"
- // case "OSX":
- // return "SF Pro Display"
- // case "Linux":
- // return "Ubuntu"
- //}
- }
-
- property int heading_font_size: 28
- property int heading_line_height: 36
-
- property int title_font_size: 20
- property int title_line_height: 24
-
- property int lead_font_size: 18
- property int lead_line_height: 26
-
- property int body_font_size: 14
- property int body_line_height: 20
- property real body_letter_spacing: 0.2
-
- property int caption_font_size: 12
- property int caption_line_height: 16
- property real caption_letter_spacing: 0.4
+ default:
+ console.error("Unknown platform")
}
+ }
+
+ property int heading_font_size: 28
+ property int heading_line_height: 36
+
+ property int title_font_size: 20
+ property int title_line_height: 24
+
+ property int lead_font_size: 18
+ property int lead_line_height: 26
+
+ property int body_font_size: 14
+ property int body_line_height: 20
+ property real body_letter_spacing: 0.2
+
+ property int caption_font_size: 12
+ property int caption_line_height: 16
+ property real caption_letter_spacing: 0.4
+
+ property int fontWidth_100: Font.Thin
+ property int fontWidth_200: Font.Light
+ property int fontWidth_300: Font.ExtraLight
+ property int fontWidth_400: Font.Normal
+ property int fontWidth_500: Font.Medium
+ property int fontWidth_600: Font.DemiBold
+ property int fontWidth_700: Font.Bold
+ property int fontWidth_800: Font.ExtraBold
+ property int fontWidth_900: Font.Black
+
+ property var transparent: "#00000000"
+}
diff --git a/internal/frontend/qml/Proton/Switch.qml b/internal/frontend/qml/Proton/Switch.qml
new file mode 100644
index 00000000..703fd0ef
--- /dev/null
+++ b/internal/frontend/qml/Proton/Switch.qml
@@ -0,0 +1,149 @@
+// 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.12
+import QtQuick.Templates 2.12 as T
+import QtQuick.Controls 2.12
+import QtQuick.Controls.impl 2.12
+
+T.Switch {
+ property var colorScheme: parent.colorScheme ? parent.colorScheme : Style.currentStyle
+
+ property bool loading: false
+
+ // TODO: store previous enabled state and restore it?
+ // For now assuming that only enabled buttons could have loading state
+ onLoadingChanged: {
+ if (loading) {
+ enabled = false
+ } else {
+ enabled = true
+ }
+ }
+
+ id: control
+
+ implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
+ implicitContentWidth + leftPadding + rightPadding)
+ implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
+ implicitContentHeight + topPadding + bottomPadding,
+ implicitIndicatorHeight + topPadding + bottomPadding)
+
+ padding: 0
+ spacing: 7
+
+ indicator: PaddedRectangle {
+ implicitWidth: 40
+ implicitHeight: 24
+
+ x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
+ y: control.topPadding + (control.availableHeight - height) / 2
+
+ radius: 12
+ leftPadding: 0
+ rightPadding: 0
+ padding: 0
+ color: control.enabled || control.loading ? colorScheme.background_norm : colorScheme.background_strong
+ border.width: control.enabled && !loading ? 1 : 0
+ border.color: control.hovered ? colorScheme.field_hover : colorScheme.field_norm
+
+ Rectangle {
+ x: Math.max(0, Math.min(parent.width - width, control.visualPosition * parent.width - (width / 2)))
+ y: (parent.height - height) / 2
+ width: 24
+ height: 24
+ radius: 12
+
+ visible: !loading
+
+ color: {
+ if (!control.enabled) {
+ return colorScheme.field_disabled
+ }
+
+ if (control.checked) {
+ if (control.hovered) {
+ return colorScheme.interaction_norm_hover
+ }
+
+ return colorScheme.interaction_norm
+ }
+
+ if (control.hovered) {
+ return colorScheme.field_hover
+ }
+
+ return colorScheme.field_norm
+ }
+
+ ColorImage {
+ x: (parent.width - width) / 2
+ y: (parent.height - height) / 2
+
+ width: 16
+ height: 16
+ color: "#FFFFFF"
+ source: "../icons/ic-check.svg"
+ visible: control.checked
+ }
+
+ Behavior on x {
+ enabled: !control.down
+ SmoothedAnimation { velocity: 200 }
+ }
+ }
+
+ ColorImage {
+ id: loadingImage
+ x: parent.width - width
+ y: (parent.height - height) / 2
+
+ width: 18
+ height: 18
+ color: colorScheme.interaction_norm_hover
+ source: "../icons/Loader_16.svg"
+ visible: control.loading
+
+ RotationAnimation {
+ target: loadingImage
+ loops: Animation.Infinite
+ duration: 1000
+ from: 0
+ to: 360
+ direction: RotationAnimation.Clockwise
+ running: control.loading
+ }
+ }
+ }
+
+ contentItem: CheckLabel {
+ id: label
+ leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
+ rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
+
+ text: control.text
+
+ color: control.enabled || control.loading ? colorScheme.text_norm : colorScheme.text_disabled
+
+ font.family: Style.font_family
+ font.weight: Style.fontWidth_400
+ font.pixelSize: 14
+ lineHeight: 20
+ lineHeightMode: Text.FixedHeight
+ font.letterSpacing: 0.2
+ }
+}
diff --git a/internal/frontend/qml/Proton/TextArea.qml b/internal/frontend/qml/Proton/TextArea.qml
new file mode 100644
index 00000000..e170009f
--- /dev/null
+++ b/internal/frontend/qml/Proton/TextArea.qml
@@ -0,0 +1,274 @@
+// 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 QtQml 2.12
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Controls.impl 2.12
+import QtQuick.Templates 2.12 as T
+
+Item {
+ id: root
+ property var colorScheme: parent.colorScheme ? parent.colorScheme : Style.currentStyle
+
+ property alias background: control.background
+ property alias bottomInset: control.bottomInset
+ //property alias flickable: control.flickable
+ property alias focusReason: control.focusReason
+ property alias hoverEnabled: control.hoverEnabled
+ property alias hovered: control.hovered
+ property alias implicitBackgroundHeight: control.implicitBackgroundHeight
+ property alias implicitBackgroundWidth: control.implicitBackgroundWidth
+ property alias leftInset: control.leftInset
+ property alias palette: control.palette
+ property alias placeholderText: control.placeholderText
+ property alias placeholderTextColor: control.placeholderTextColor
+ property alias rightInset: control.rightInset
+ property alias topInset: control.topInset
+ property alias activeFocusOnPress: control.activeFocusOnPress
+ property alias baseUrl: control.baseUrl
+ property alias bottomPadding: control.bottomPadding
+ property alias canPaste: control.canPaste
+ property alias canRedo: control.canRedo
+ property alias canUndo: control.canUndo
+ property alias color: control.color
+ property alias contentHeight: control.contentHeight
+ property alias contentWidth: control.contentWidth
+ property alias cursorDelegate: control.cursorDelegate
+ property alias cursorPosition: control.cursorPosition
+ property alias cursorRectangle: control.cursorRectangle
+ property alias cursorVisible: control.cursorVisible
+ property alias effectiveHorizontalAlignment: control.effectiveHorizontalAlignment
+ property alias font: control.font
+ property alias horizontalAlignment: control.horizontalAlignment
+ property alias hoveredLink: control.hoveredLink
+ property alias inputMethodComposing: control.inputMethodComposing
+ property alias inputMethodHints: control.inputMethodHints
+ property alias leftPadding: control.leftPadding
+ property alias length: control.length
+ property alias lineCount: control.lineCount
+ property alias mouseSelectionMode: control.mouseSelectionMode
+ property alias overwriteMode: control.overwriteMode
+ property alias padding: control.padding
+ property alias persistentSelection: control.persistentSelection
+ property alias preeditText: control.preeditText
+ property alias readOnly: control.readOnly
+ property alias renderType: control.renderType
+ property alias rightPadding: control.rightPadding
+ property alias selectByKeyboard: control.selectByKeyboard
+ property alias selectByMouse: control.selectByMouse
+ property alias selectedText: control.selectedText
+ property alias selectedTextColor: control.selectedTextColor
+ property alias selectionColor: control.selectionColor
+ property alias selectionEnd: control.selectionEnd
+ property alias selectionStart: control.selectionStart
+ property alias tabStopDistance: control.tabStopDistance
+ property alias text: control.text
+ property alias textDocument: control.textDocument
+ property alias textFormat: control.textFormat
+ property alias textMargin: control.textMargin
+ property alias topPadding: control.topPadding
+ property alias verticalAlignment: control.verticalAlignment
+ 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
+
+ property alias label: label.text
+ property alias hint: hint.text
+ property alias assistiveText: assistiveText.text
+
+ property bool error: false
+
+ // Backgroud is moved away from within control as it will be clipped with scrollview
+ Rectangle {
+ id: background
+
+ anchors.fill: controlView
+
+ radius: 4
+ visible: true
+ color: colorScheme.background_norm
+ border.color: {
+ if (!control.enabled) {
+ return colorScheme.field_disabled
+ }
+
+ if (control.activeFocus) {
+ return colorScheme.interaction_norm
+ }
+
+ if (root.error) {
+ return colorScheme.signal_danger
+ }
+
+ if (control.hovered) {
+ return colorScheme.field_hover
+ }
+
+ return colorScheme.field_norm
+ }
+ border.width: 1
+ }
+
+ Label {
+ id: label
+
+ anchors.top: root.top
+ anchors.left: root.left
+ anchors.bottomMargin: 4
+
+ color: root.enabled ? colorScheme.text_norm : colorScheme.text_disabled
+
+ font.family: Style.font_family
+ font.weight: Style.fontWidth_600
+ font.pixelSize: 14
+ lineHeight: 20
+ lineHeightMode: Text.FixedHeight
+ font.letterSpacing: 0.2
+ }
+
+ Label {
+ id: hint
+
+ anchors.right: root.right
+ anchors.bottom: controlView.top
+ anchors.bottomMargin: 5
+
+ color: root.enabled ? colorScheme.text_weak : colorScheme.text_disabled
+
+ font.family: Style.font_family
+ font.weight: Style.fontWidth_400
+ font.pixelSize: 12
+ lineHeight: 16
+ lineHeightMode: Text.FixedHeight
+ font.letterSpacing: 0.4
+ }
+
+ ColorImage {
+ id: errorIcon
+ visible: root.error
+ anchors.left: parent.left
+ anchors.top: assistiveText.top
+ anchors.bottom: assistiveText.bottom
+ source: "../icons/ic-exclamation-circle-filled.svg"
+ color: colorScheme.signal_danger
+ }
+
+ Label {
+ id: assistiveText
+
+ anchors.left: root.error ? errorIcon.right : parent.left
+ anchors.leftMargin: root.error ? 5 : 0
+ anchors.bottom: root.bottom
+ anchors.topMargin: 4
+
+ color: {
+ if (!root.enabled) {
+ return colorScheme.text_disabled
+ }
+
+ if (root.error) {
+ return colorScheme.signal_danger
+ }
+
+ return colorScheme.text_weak
+ }
+
+ font.family: Style.font_family
+ font.weight: root.error ? Style.fontWidth_600 : Style.fontWidth_400
+ font.pixelSize: 12
+ lineHeight: 16
+ lineHeightMode: Text.FixedHeight
+ font.letterSpacing: 0.4
+ }
+
+ ScrollView {
+ id: controlView
+
+ anchors.top: label.bottom
+ anchors.left: root.left
+ anchors.right: root.right
+ anchors.bottom: assistiveText.top
+
+ clip: true
+
+ T.TextArea {
+ id: control
+
+ implicitWidth: Math.max(contentWidth + leftPadding + rightPadding,
+ implicitBackgroundWidth + leftInset + rightInset,
+ placeholder.implicitWidth + leftPadding + rightPadding)
+ implicitHeight: Math.max(contentHeight + topPadding + bottomPadding,
+ implicitBackgroundHeight + topInset + bottomInset,
+ placeholder.implicitHeight + topPadding + bottomPadding)
+
+ padding: 8
+ leftPadding: 12
+
+ color: control.enabled ? colorScheme.text_norm : colorScheme.text_disabled
+ placeholderTextColor: control.enabled ? colorScheme.text_hint : colorScheme.text_disabled
+
+ selectionColor: control.palette.highlight
+ selectedTextColor: control.palette.highlightedText
+
+ cursorDelegate: Rectangle {
+ id: cursor
+ width: 1
+ color: colorScheme.interaction_norm
+ visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd
+
+ Connections {
+ target: control
+ onCursorPositionChanged: {
+ // keep a moving cursor visible
+ cursor.opacity = 1
+ timer.restart()
+ }
+ }
+
+ Timer {
+ id: timer
+ running: control.activeFocus && !control.readOnly
+ repeat: true
+ interval: Qt.styleHints.cursorFlashTime / 2
+ onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0
+ // force the cursor visible when gaining focus
+ onRunningChanged: cursor.opacity = 1
+ }
+ }
+
+ PlaceholderText {
+ id: placeholder
+ x: control.leftPadding
+ y: control.topPadding
+ width: control.width - (control.leftPadding + control.rightPadding)
+ height: control.height - (control.topPadding + control.bottomPadding)
+
+ text: control.placeholderText
+ font: control.font
+ color: control.placeholderTextColor
+ verticalAlignment: control.verticalAlignment
+ visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter)
+ elide: Text.ElideRight
+ renderType: control.renderType
+ }
+ }
+ }
+}
diff --git a/internal/frontend/qml/Proton/TextField.qml b/internal/frontend/qml/Proton/TextField.qml
new file mode 100644
index 00000000..728e8488
--- /dev/null
+++ b/internal/frontend/qml/Proton/TextField.qml
@@ -0,0 +1,329 @@
+// 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 QtQml 2.12
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Controls.impl 2.12
+import QtQuick.Templates 2.12 as T
+import QtQuick.Layouts 1.12
+
+Item {
+ id: root
+ property var colorScheme: parent.colorScheme ? parent.colorScheme : Style.currentStyle
+
+ property alias background: control.background
+ property alias bottomInset: control.bottomInset
+ property alias focusReason: control.focusReason
+ property alias hoverEnabled: control.hoverEnabled
+ property alias hovered: control.hovered
+ property alias implicitBackgroundHeight: control.implicitBackgroundHeight
+ property alias implicitBackgroundWidth: control.implicitBackgroundWidth
+ property alias leftInset: control.leftInset
+ property alias palette: control.palette
+ property alias placeholderText: control.placeholderText
+ property alias placeholderTextColor: control.placeholderTextColor
+ property alias rightInset: control.rightInset
+ property alias topInset: control.topInset
+ property alias acceptableInput: control.acceptableInput
+ property alias activeFocusOnPress: control.activeFocusOnPress
+ property alias autoScroll: control.autoScroll
+ property alias bottomPadding: control.bottomPadding
+ property alias canPaste: control.canPaste
+ property alias canRedo: control.canRedo
+ property alias canUndo: control.canUndo
+ property alias color: control.color
+ //property alias contentHeight: control.contentHeight
+ //property alias contentWidth: control.contentWidth
+ property alias cursorDelegate: control.cursorDelegate
+ property alias cursorPosition: control.cursorPosition
+ property alias cursorRectangle: control.cursorRectangle
+ property alias cursorVisible: control.cursorVisible
+ property alias displayText: control.displayText
+ property alias effectiveHorizontalAlignment: control.effectiveHorizontalAlignment
+ property alias font: control.font
+ property alias horizontalAlignment: control.horizontalAlignment
+ property alias inputMask: control.inputMask
+ property alias inputMethodComposing: control.inputMethodComposing
+ property alias inputMethodHints: control.inputMethodHints
+ property alias leftPadding: control.leftPadding
+ property alias length: control.length
+ property alias maximumLength: control.maximumLength
+ property alias mouseSelectionMode: control.mouseSelectionMode
+ property alias overwriteMode: control.overwriteMode
+ property alias padding: control.padding
+ property alias passwordCharacter: control.passwordCharacter
+ property alias passwordMaskDelay: control.passwordMaskDelay
+ property alias persistentSelection: control.persistentSelection
+ property alias preeditText: control.preeditText
+ property alias readOnly: control.readOnly
+ property alias renderType: control.renderType
+ property alias rightPadding: control.rightPadding
+ property alias selectByMouse: control.selectByMouse
+ property alias selectedText: control.selectedText
+ property alias selectedTextColor: control.selectedTextColor
+ property alias selectionColor: control.selectionColor
+ property alias selectionEnd: control.selectionEnd
+ property alias selectionStart: control.selectionStart
+ property alias text: control.text
+ property alias validator: control.validator
+ property alias verticalAlignment: control.verticalAlignment
+ property alias wrapMode: control.wrapMode
+
+ implicitWidth: children[0].implicitWidth
+ implicitHeight: children[0].implicitHeight
+
+ property alias label: label.text
+ property alias hint: hint.text
+ property alias assistiveText: assistiveText.text
+
+ property var echoMode: TextInput.Normal
+
+ property bool error: false
+
+ signal accepted()
+ signal editingFinished()
+ signal textEdited()
+
+ function clear() { control.clear() }
+ function copy() { control.copy() }
+ function cut() { control.cut() }
+ function deselect() { control.deselect() }
+ function ensureVisible(position) { control.ensureVisible(position) }
+ function getText(start, end) { control.getText(start, end) }
+ function insert(position, text) { control.insert(position, text) }
+ function isRightToLeft(start, end) { control.isRightToLeft(start, end) }
+ function moveCursorSelection(position, mode) { control.moveCursorSelection(position, mode) }
+ function paste() { control.paste() }
+ function positionAt(x, y, position) { control.positionAt(x, y, position) }
+ function positionToRectangle(pos) { control.positionToRectangle(pos) }
+ function redo() { control.redo() }
+ function remove(start, end) { control.remove(start, end) }
+ function select(start, end) { control.select(start, end) }
+ function selectAll() { control.selectAll() }
+ function selectWord() { control.selectWord() }
+ function undo() { control.undo() }
+ function forceActiveFocus() {control.forceActiveFocus()}
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 0
+
+ ProtonLabel {
+ id: label
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ color: root.enabled ? colorScheme.text_norm : colorScheme.text_disabled
+ font.weight: Style.fontWidth_600
+ state: "body"
+ }
+
+ ProtonLabel {
+ id: hint
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ color: root.enabled ? colorScheme.text_weak : colorScheme.text_disabled
+ horizontalAlignment: Text.AlignRight
+ state: "caption"
+ }
+ }
+
+ // Background is moved away from within control to cover eye button as well.
+ // In case it will remain as control background property - control's width
+ // will be adjusted to background's width making text field and eye button overlap
+ Rectangle {
+ id: background
+
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ radius: 4
+ visible: true
+ color: colorScheme.background_norm
+ border.color: {
+ if (!control.enabled) {
+ return colorScheme.field_disabled
+ }
+
+ if (control.activeFocus) {
+ return colorScheme.interaction_norm
+ }
+
+ if (root.error) {
+ return colorScheme.signal_danger
+ }
+
+ if (control.hovered) {
+ return colorScheme.field_hover
+ }
+
+ return colorScheme.field_norm
+ }
+ border.width: 1
+
+ implicitWidth: children[0].implicitWidth
+ implicitHeight: children[0].implicitHeight
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ T.TextField {
+ id: control
+
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ implicitWidth: implicitBackgroundWidth + leftInset + rightInset
+ || Math.max(contentWidth, placeholder.implicitWidth) + leftPadding + rightPadding
+ implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
+ contentHeight + topPadding + bottomPadding,
+ placeholder.implicitHeight + topPadding + bottomPadding)
+
+ padding: 8
+ leftPadding: 12
+
+ color: control.enabled ? colorScheme.text_norm : colorScheme.text_disabled
+
+ selectionColor: control.palette.highlight
+ selectedTextColor: control.palette.highlightedText
+ placeholderTextColor: control.enabled ? colorScheme.text_hint : colorScheme.text_disabled
+ verticalAlignment: TextInput.AlignVCenter
+
+ cursorDelegate: Rectangle {
+ id: cursor
+ width: 1
+ color: colorScheme.interaction_norm
+ visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd
+
+ Connections {
+ target: control
+ onCursorPositionChanged: {
+ // keep a moving cursor visible
+ cursor.opacity = 1
+ timer.restart()
+ }
+ }
+
+ Timer {
+ id: timer
+ running: control.activeFocus && !control.readOnly
+ repeat: true
+ interval: Qt.styleHints.cursorFlashTime / 2
+ onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0
+ // force the cursor visible when gaining focus
+ onRunningChanged: cursor.opacity = 1
+ }
+ }
+
+ PlaceholderText {
+ id: placeholder
+ x: control.leftPadding
+ y: control.topPadding
+ width: control.width - (control.leftPadding + control.rightPadding)
+ height: control.height - (control.topPadding + control.bottomPadding)
+
+ text: control.placeholderText
+ font: control.font
+ color: control.placeholderTextColor
+ verticalAlignment: control.verticalAlignment
+ visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter)
+ elide: Text.ElideRight
+ renderType: control.renderType
+ }
+
+ background: Item {
+ implicitWidth: 80
+ implicitHeight: 36
+ visible: false
+ }
+
+ onAccepted: {
+ root.accepted()
+ }
+ onEditingFinished: {
+ root.editingFinished()
+ }
+ onTextEdited: {
+ root.textEdited()
+ }
+ }
+
+ Button {
+ id: eyeButton
+
+ Layout.fillHeight: true
+
+ visible: root.echoMode === TextInput.Password
+ icon.source: control.echoMode == TextInput.Password ? "../icons/ic-eye.svg" : "../icons/ic-eye-slash.svg"
+ icon.color: control.color
+ background: Rectangle{color: "#00000000"}
+ onClicked: {
+ if (control.echoMode == TextInput.Password) {
+ control.echoMode = TextInput.Normal
+ } else {
+ control.echoMode = TextInput.Password
+ }
+ }
+ Component.onCompleted: control.echoMode = root.echoMode
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 0
+
+ // FIXME: maybe somewhere in the future there will be an Icon component capable of setting color to the icon
+ // but before that moment we need to use IconLabel
+ IconLabel {
+ id: errorIcon
+
+ visible: root.error && (assistiveText.text.length > 0)
+ icon.source: "../icons/ic-exclamation-circle-filled.svg"
+ icon.color: colorScheme.signal_danger
+ }
+
+ ProtonLabel {
+ id: assistiveText
+
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ Layout.leftMargin: 4
+
+ color: {
+ if (!root.enabled) {
+ return colorScheme.text_disabled
+ }
+
+ if (root.error) {
+ return colorScheme.signal_danger
+ }
+
+ return colorScheme.text_weak
+ }
+
+ font.weight: root.error ? Style.fontWidth_600 : Style.fontWidth_400
+ state: "caption"
+ }
+ }
+ }
+}
diff --git a/internal/frontend/qml/Proton/qmldir b/internal/frontend/qml/Proton/qmldir
index d518f67c..38d28389 100644
--- a/internal/frontend/qml/Proton/qmldir
+++ b/internal/frontend/qml/Proton/qmldir
@@ -2,5 +2,12 @@ module QQtQuick.Controls.Proton
depends QtQuick.Controls 2.12
singleton ProtonStyle 4.0 Style.qml
+Banner 4.0 Banner.qml
Button 4.0 Button.qml
-
+CheckBox 4.0 CheckBox.qml
+ProtonLabel 4.0 ProtonLabel.qml
+RoundedRectangle 4.0 RoundedRectangle.qml
+RadioButton 4.0 RadioButton.qml
+Switch 4.0 Switch.qml
+TextArea 4.0 TextArea.qml
+TextField 4.0 TextField.qml
diff --git a/internal/frontend/qml/SetupGuide.qml b/internal/frontend/qml/SetupGuide.qml
new file mode 100644
index 00000000..36386d4c
--- /dev/null
+++ b/internal/frontend/qml/SetupGuide.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.impl 2.12
+
+import Proton 4.0
+
+RowLayout {
+ id:root
+
+ property var colorScheme
+ property var window
+
+ property var user: { "username": "janedoe@protonmail.com" }
+
+ ColumnLayout {
+ Layout.fillHeight: true
+ Layout.leftMargin: 80
+ Layout.rightMargin: 80
+ Layout.topMargin: 30
+ Layout.bottomMargin: 70
+
+ ProtonLabel {
+ text: qsTr("Set up email client")
+ font.weight: ProtonStyle.fontWidth_700
+ state: "heading"
+ }
+
+ ProtonLabel {
+ text: user.username
+ color: root.colorScheme.text_weak
+ state: "lead"
+ }
+
+ ProtonLabel {
+ Layout.topMargin: 32
+ text: qsTr("Choose an email client")
+ font.weight: ProtonStyle.fontWidth_600
+ state: "body"
+ }
+
+ ListModel {
+ id: clients
+ ListElement{name : "Apple Mail" ; iconSource : "./icons/ic-apple-mail.svg" }
+ ListElement{name : "Microsoft Outlook" ; iconSource : "./icons/ic-microsoft-outlook.svg" }
+ ListElement{name : "Mozilla Thunderbird" ; iconSource : "./icons/ic-mozilla-thunderbird.svg" }
+ ListElement{name : "Other" ; iconSource : "./icons/ic-other-mail-clients.svg" }
+ }
+
+
+ Repeater {
+ model: clients
+
+ ColumnLayout {
+ RowLayout {
+ Layout.topMargin: 12
+ Layout.bottomMargin: 12
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+
+ IconLabel {
+ icon.source: model.iconSource
+ icon.height: 36
+ }
+
+ ProtonLabel {
+ Layout.leftMargin: 12
+ text: model.name
+ state: "body"
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: root.colorScheme.border_weak
+ }
+ }
+ }
+
+ Item { Layout.fillHeight: true }
+
+ Button {
+ text: qsTr("Set up later")
+ flat: true
+
+ onClicked: {
+ root.window.showSetup = false
+ root.reset()
+ }
+ }
+ }
+}
diff --git a/internal/frontend/qml/SignIn.qml b/internal/frontend/qml/SignIn.qml
new file mode 100644
index 00000000..f98b3276
--- /dev/null
+++ b/internal/frontend/qml/SignIn.qml
@@ -0,0 +1,503 @@
+// 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 QtQml 2.12
+import QtQuick 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.12
+import QtQuick.Controls.impl 2.12
+
+import Proton 4.0
+
+Item {
+ id: root
+ property var colorScheme: parent.colorScheme
+
+ function abort() {
+ root.loginAbort(usernameTextField.text)
+ }
+
+ signal login(string username, string password)
+ signal login2FA(string username, string code)
+ signal login2Password(string username, string password)
+ signal loginAbort(string username)
+
+ implicitHeight: children[0].implicitHeight
+ implicitWidth: children[0].implicitWidth
+
+ property var backend
+ property var window
+
+ // in case of adding new account this property should be undefined
+ property var user
+ state: "Page 1"
+
+ onUserChanged: {
+ stackLayout.currentIndex = 0
+ loginNormalLayout.reset()
+ passwordTextField.text = ""
+ login2FALayout.reset()
+ login2PasswordLayout.reset()
+ }
+
+ onLoginAbort: {
+ stackLayout.currentIndex = 0
+ loginNormalLayout.reset()
+ login2FALayout.reset()
+ login2PasswordLayout.reset()
+ }
+
+ property alias currentIndex: stackLayout.currentIndex
+
+ StackLayout {
+ id: stackLayout
+ anchors.fill: parent
+
+ function loginFailed() {
+ signInButton.loading = false
+
+ usernameTextField.enabled = true
+ usernameTextField.error = true
+
+ passwordTextField.enabled = true
+ passwordTextField.error = true
+ }
+
+ Connections {
+ target: user !== undefined ? user : 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")
+
+ }
+
+ onLoginFreeUserError: {
+ console.assert(stackLayout.currentIndex == 0, "Unexpected loginFreeUserError")
+ stackLayout.loginFailed()
+ window.notifyOnlyPaidUsers()
+ }
+ onLoginConnectionError: {
+ if (stackLayout.currentIndex == 0 ) {
+ stackLayout.loginFailed()
+ }
+ window.notifyConnectionLostWhileLogin()
+ }
+
+ onLogin2FARequested: {
+ console.assert(stackLayout.currentIndex == 0, "Unexpected login2FARequested")
+
+ stackLayout.currentIndex = 1
+ }
+ onLogin2FAError: {
+ console.assert(stackLayout.currentIndex == 1, "Unexpected login2FAError")
+
+ twoFAButton.loading = false
+
+ twoFactorPasswordTextField.enabled = true
+ twoFactorPasswordTextField.error = true
+ twoFactorPasswordTextField.assistiveText = qsTr("Your code is incorrect")
+ }
+ onLogin2FAErrorAbort: {
+ console.assert(stackLayout.currentIndex == 1, "Unexpected login2FAErrorAbort")
+
+ stackLayout.currentIndex = 0
+ loginNormalLayout.reset()
+ login2FALayout.reset()
+ login2PasswordLayout.reset()
+
+ errorLabel.text = qsTr("Incorrect login credentials. Please try again.")
+ passwordTextField.text = ""
+ }
+
+ onLogin2PasswordRequested: {
+ console.assert(stackLayout.currentIndex == 0 || stackLayout.currentIndex == 1, "Unexpected login2PasswordRequested")
+
+ stackLayout.currentIndex = 2
+ }
+ onLogin2PasswordError: {
+ console.assert(stackLayout.currentIndex == 2, "Unexpected login2PasswordError")
+
+ secondPasswordButton.loading = false
+
+ secondPasswordTextField.enabled = true
+ secondPasswordTextField.error = true
+ secondPasswordTextField.assistiveText = qsTr("Your mailbox password is incorrect")
+ }
+ onLogin2PasswordErrorAbort: {
+ console.assert(stackLayout.currentIndex == 2, "Unexpected login2PasswordErrorAbort")
+
+ stackLayout.currentIndex = 0
+ loginNormalLayout.reset()
+ login2FALayout.reset()
+ login2PasswordLayout.reset()
+
+ errorLabel.text = qsTr("Incorrect login credentials. Please try again.")
+ passwordTextField.text = ""
+ }
+ }
+
+ ColumnLayout {
+ id: loginNormalLayout
+
+ function reset() {
+ signInButton.loading = false
+
+ errorLabel.text = ""
+
+ usernameTextField.enabled = true
+ usernameTextField.error = false
+ usernameTextField.assistiveText = ""
+
+ passwordTextField.enabled = true
+ passwordTextField.error = false
+ passwordTextField.assistiveText = ""
+ }
+
+ spacing: 0
+
+ ProtonLabel {
+ text: qsTr("Sign in")
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: 16
+ font.weight: ProtonStyle.fontWidth_700
+ }
+
+ ProtonLabel {
+ id: subTitle
+ text: qsTr("Enter your Proton Account details.")
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: 8
+ color: root.colorScheme.text_weak
+ state: "body"
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.topMargin: 36
+
+ spacing: 0
+ visible: errorLabel.text.length > 0
+
+ ColorImage {
+ color: root.colorScheme.signal_danger
+ source: "./icons/ic-exclamation-circle-filled.svg"
+ }
+
+ ProtonLabel {
+ id: errorLabel
+ Layout.leftMargin: 4
+ color: root.colorScheme.signal_danger
+
+ font.weight: root.error ? ProtonStyle.fontWidth_600 : ProtonStyle.fontWidth_400
+ state: "caption"
+ }
+ }
+
+ TextField {
+ id: usernameTextField
+ label: qsTr("Username or email")
+
+ text: user !== undefined ? user.username : ""
+
+ Layout.fillWidth: true
+ Layout.topMargin: 24
+
+ onTextEdited: { // TODO: repeating?
+ if (error || errorLabel.text.length > 0) {
+ errorLabel.text = ""
+
+ usernameTextField.error = false
+ usernameTextField.assistiveText = ""
+
+ passwordTextField.error = false
+ passwordTextField.assistiveText = ""
+ }
+ }
+
+ onAccepted: passwordTextField.forceActiveFocus()
+ }
+
+ TextField {
+ id: passwordTextField
+ label: qsTr("Password")
+
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+ echoMode: TextInput.Password
+
+ onTextEdited: {
+ if (error || errorLabel.text.length > 0) {
+ errorLabel.text = ""
+
+ usernameTextField.error = false
+ usernameTextField.assistiveText = ""
+
+ passwordTextField.error = false
+ passwordTextField.assistiveText = ""
+ }
+ }
+
+ onAccepted: signInButton.checkAndSignIn()
+ }
+
+ Button {
+ id: signInButton
+ text: qsTr("Sign in")
+
+ Layout.fillWidth: true
+ Layout.topMargin: 24
+
+
+ onClicked: checkAndSignIn()
+
+ function checkAndSignIn() {
+ var err = false
+
+ if (usernameTextField.text.length == 0) {
+ usernameTextField.error = true
+ usernameTextField.assistiveText = qsTr("Enter username or email")
+ err = true
+ } else {
+ usernameTextField.error = false
+ usernameTextField.assistiveText = qsTr("")
+ }
+
+ if (passwordTextField.text.length == 0) {
+ passwordTextField.error = true
+ passwordTextField.assistiveText = qsTr("Enter password")
+ err = true
+ } else {
+ passwordTextField.error = false
+ passwordTextField.assistiveText = qsTr("")
+ }
+
+ if (err) {
+ return
+ }
+
+ usernameTextField.enabled = false
+ passwordTextField.enabled = false
+
+ enabled = false
+ loading = true
+
+ if (root.user !== undefined) {
+ root.user.login(usernameTextField.text, passwordTextField.text)
+ return
+ }
+
+ root.login(usernameTextField.text, passwordTextField.text)
+ }
+ }
+
+ ProtonLabel {
+ textFormat: Text.StyledText
+ text: putLink("https://protonmail.com/upgrade", qsTr("Create or upgrade your account"))
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: 24
+ state: "body"
+
+ onLinkActivated: {
+ Qt.openUrlExternally(link)
+ }
+
+ }
+ }
+
+ ColumnLayout {
+ id: login2FALayout
+
+ function reset() {
+ twoFAButton.loading = false
+
+ twoFactorPasswordTextField.enabled = true
+ twoFactorPasswordTextField.error = false
+ twoFactorPasswordTextField.assistiveText = ""
+ }
+
+ spacing: 0
+
+ ProtonLabel {
+ text: qsTr("Two-factor authentication")
+ Layout.topMargin: 16
+ Layout.alignment: Qt.AlignCenter
+ font.weight: ProtonStyle.fontWidth_700
+
+
+ }
+
+ TextField {
+ id: twoFactorPasswordTextField
+ label: qsTr("Two-factor authentication code")
+
+ Layout.fillWidth: true
+ Layout.topMargin: 8 + implicitHeight + 24 + subTitle.implicitHeight
+
+ onTextEdited: {
+ if (error) {
+ twoFactorPasswordTextField.error = false
+ twoFactorPasswordTextField.assistiveText = ""
+ }
+ }
+ }
+
+ Button {
+ id: twoFAButton
+ text: loading ? qsTr("Authenticating") : qsTr("Authenticate")
+
+ Layout.fillWidth: true
+ Layout.topMargin: 24
+
+ onClicked: {
+ var err = false
+
+ if (twoFactorPasswordTextField.text.length == 0) {
+ twoFactorPasswordTextField.error = true
+ twoFactorPasswordTextField.assistiveText = qsTr("Enter username or email")
+ err = true
+ } else {
+ twoFactorPasswordTextField.error = false
+ twoFactorPasswordTextField.assistiveText = qsTr("")
+ }
+
+ if (err) {
+ return
+ }
+
+ twoFactorPasswordTextField.enabled = false
+
+ enabled = false
+ loading = true
+
+ if (root.user !== undefined) {
+ root.user.login2FA(usernameTextField.text, twoFactorPasswordTextField.text)
+ return
+ }
+
+ root.login2FA(usernameTextField.text, twoFactorPasswordTextField.text)
+ }
+ }
+ }
+
+ ColumnLayout {
+ id: login2PasswordLayout
+
+ function reset() {
+ secondPasswordButton.loading = false
+
+ secondPasswordTextField.enabled = true
+ secondPasswordTextField.error = false
+ secondPasswordTextField.assistiveText = ""
+ }
+
+ spacing: 0
+
+ ProtonLabel {
+ text: qsTr("Unlock your mailbox")
+ Layout.topMargin: 16
+ Layout.alignment: Qt.AlignCenter
+ font.weight: ProtonStyle.fontWidth_700
+ }
+
+
+
+
+
+
+ TextField {
+ id: secondPasswordTextField
+ label: qsTr("Mailbox password")
+
+ Layout.fillWidth: true
+ Layout.topMargin: 8 + implicitHeight + 24 + subTitle.implicitHeight
+ echoMode: TextInput.Password
+
+ onTextEdited: {
+ if (error) {
+ secondPasswordTextField.error = false
+ secondPasswordTextField.assistiveText = ""
+ }
+ }
+ }
+
+ Button {
+ id: secondPasswordButton
+ text: loading ? qsTr("Unlocking") : qsTr("Unlock")
+
+ Layout.fillWidth: true
+ Layout.topMargin: 24
+
+ onClicked: {
+ var err = false
+
+ if (secondPasswordTextField.text.length == 0) {
+ secondPasswordTextField.error = true
+ secondPasswordTextField.assistiveText = qsTr("Enter username or email")
+ err = true
+ } else {
+ secondPasswordTextField.error = false
+ secondPasswordTextField.assistiveText = qsTr("")
+ }
+
+ if (err) {
+ return
+ }
+
+ secondPasswordTextField.enabled = false
+
+ enabled = false
+ loading = true
+
+ if (root.user !== undefined) {
+ root.user.login2Password(usernameTextField.text, secondPasswordTextField.text)
+ return
+ }
+
+ root.login2Password(usernameTextField.text, secondPasswordTextField.text)
+ }
+ }
+ }
+ }
+
+ states: [
+ State {
+ name: "Page 1"
+ PropertyChanges {
+ target: stackLayout
+ currentIndex: 0
+ }
+ },
+ State {
+ name: "Page 2"
+ PropertyChanges {
+ target: stackLayout
+ currentIndex: 1
+ }
+ },
+ State {
+ name: "Page 3"
+ PropertyChanges {
+ target: stackLayout
+ currentIndex: 2
+ }
+ }
+ ]
+}
diff --git a/internal/frontend/qml/Status.qml b/internal/frontend/qml/Status.qml
new file mode 100644
index 00000000..9c0bc41a
--- /dev/null
+++ b/internal/frontend/qml/Status.qml
@@ -0,0 +1,41 @@
+
+// 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
+
+RowLayout {
+ id: layout
+ spacing: 8
+
+ ColorImage {
+ id: image
+ source: "./icons/ic-connected.svg"
+ color: ProtonStyle.currentStyle.signal_success
+ }
+
+ Label {
+ id: label
+ text: "Connected"
+ color: ProtonStyle.currentStyle.signal_success
+ }
+}
diff --git a/internal/frontend/qml/WelcomeWindow.qml b/internal/frontend/qml/WelcomeWindow.qml
new file mode 100644
index 00000000..e9d7f1d4
--- /dev/null
+++ b/internal/frontend/qml/WelcomeWindow.qml
@@ -0,0 +1,297 @@
+// 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 QtQml 2.12
+import QtQuick 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.12
+
+import Proton 4.0
+
+RowLayout {
+ id: root
+
+ property var colorScheme: parent.colorScheme
+
+ property var backend
+ property var window
+
+ signal login(string username, string password)
+ signal login2FA(string username, string code)
+ signal login2Password(string username, string password)
+ signal loginAbort(string username)
+
+ spacing: 0
+
+ Rectangle {
+ color: root.colorScheme.background_norm
+
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ implicitHeight: children[0].implicitHeight
+ implicitWidth: children[0].implicitWidth
+
+ visible: signInItem.currentIndex == 0
+
+ GridLayout {
+ anchors.fill: parent
+
+ columnSpacing: 0
+ rowSpacing: 0
+
+ columns: 3
+
+ // top margin
+ Item {
+ Layout.columnSpan: 3
+ Layout.fillWidth: true
+
+ // Using binding component here instead of direct binding to avoid binding loop during construction of element
+ Binding on Layout.preferredHeight {
+ value: (parent.height - welcomeContentItem.height) / 4
+ }
+ }
+
+ // left margin
+ Item {
+ Layout.minimumWidth: 48
+ Layout.maximumWidth: 80
+ Layout.fillWidth: true
+ Layout.preferredHeight: welcomeContentItem.height
+ }
+
+ ColumnLayout {
+ id: welcomeContentItem
+ Layout.fillWidth: true
+ spacing: 0
+
+ Image {
+ source: "icons/img-welcome.svg"
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: 16
+ }
+
+ Label {
+ text: qsTr("Welcome to\nProtonMail Bridge")
+ Layout.alignment: Qt.AlignHCenter
+ Layout.fillWidth: true
+ Layout.topMargin: 16
+
+ color: root.colorScheme.text_norm
+
+ horizontalAlignment: Text.AlignHCenter
+
+ font.family: ProtonStyle.font_family
+ font.weight: ProtonStyle.fontWidth_700
+ font.pixelSize: 28
+ lineHeight: 36
+ lineHeightMode: Text.FixedHeight
+ }
+
+ Label {
+ id: longTextLabel
+ text: qsTr("Now you can securely access and manage ProtonMail messages in your favorite email client. Bridge runs in the background and encrypts and decrypts your messages seamlessly.")
+ Layout.alignment: Qt.AlignHCenter
+ Layout.fillWidth: true
+ Layout.topMargin: 16
+ Layout.preferredWidth: 320
+
+ color: root.colorScheme.text_norm
+ wrapMode: Text.WordWrap
+
+ horizontalAlignment: Text.AlignHCenter
+
+ font.family: ProtonStyle.font_family
+ font.weight: ProtonStyle.fontWidth_400
+ font.pixelSize: 14
+ lineHeight: 20
+ lineHeightMode: Text.FixedHeight
+ font.letterSpacing: 0.2
+ }
+ }
+
+ // Right margin
+ Item {
+ Layout.minimumWidth: 48
+ Layout.maximumWidth: 80
+ Layout.fillWidth: true
+ Layout.preferredHeight: welcomeContentItem.height
+ }
+
+ // bottom margin
+ Item {
+ Layout.columnSpan: 3
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ implicitHeight: children[0].implicitHeight + children[0].anchors.bottomMargin + children[0].anchors.topMargin
+ implicitWidth: children[0].implicitWidth
+
+ Image {
+ id: logoImage
+ source: "icons/product_logos.svg"
+
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ anchors.topMargin: 48
+ anchors.bottomMargin: 48
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ color: (signInItem.currentIndex == 0) ? root.colorScheme.background_weak : root.colorScheme.background_norm
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ implicitHeight: children[0].implicitHeight
+ implicitWidth: children[0].implicitWidth
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: 0
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ Layout.preferredWidth: signInItem.currentIndex == 0 ? 0 : parent.width / 4
+
+ implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
+ implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
+
+ Button {
+ anchors.left: parent.left
+ anchors.bottom: parent.bottom
+
+ anchors.leftMargin: 80
+ anchors.rightMargin: 80
+ anchors.topMargin: 80
+ anchors.bottomMargin: 80
+
+ visible: signInItem.currentIndex != 0
+
+ secondary: true
+ text: qsTr("Back")
+
+ onClicked: {
+ signInItem.abort()
+ }
+ }
+ }
+
+ GridLayout {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ columnSpacing: 0
+ rowSpacing: 0
+
+ columns: 3
+
+ // top margin
+ Item {
+ Layout.columnSpan: 3
+ Layout.fillWidth: true
+
+ // Using binding component here instead of direct binding to avoid binding loop during construction of element
+ Binding on Layout.preferredHeight {
+ value: (parent.height - signInItem.height) / 4
+ }
+ }
+
+ // left margin
+ Item {
+ Layout.minimumWidth: 48
+ Layout.maximumWidth: 80
+ Layout.fillWidth: true
+ Layout.preferredHeight: signInItem.height
+ }
+
+
+ SignIn {
+ id: signInItem
+ colorScheme: root.colorScheme
+
+ Layout.preferredWidth: 320
+ Layout.fillWidth: true
+
+ onLogin: {
+ root.login(username, password)
+ }
+ onLogin2FA: {
+ root.login2FA(username, code)
+ }
+ onLogin2Password: {
+ root.login2Password(username, password)
+ }
+ onLoginAbort: {
+ root.loginAbort(username)
+ }
+
+ user: (backend.users.count === 1 && backend.users.get(0).loggedIn === false) ? backend.users.get(0) : undefined
+ backend: root.backend
+ window: root.window
+ }
+
+ // Right margin
+ Item {
+ Layout.minimumWidth: 48
+ Layout.maximumWidth: 80
+ Layout.fillWidth: true
+ Layout.preferredHeight: signInItem.height
+ }
+
+ // bottom margin
+ Item {
+ Layout.columnSpan: 3
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ Layout.preferredWidth: signInItem.currentIndex == 0 ? 0 : parent.width / 4
+ }
+ }
+ }
+
+ states: [
+ State {
+ name: "Page 1"
+ PropertyChanges {
+ target: signInItem
+ currentIndex: 0
+ }
+ },
+ State {
+ name: "Page 2"
+ PropertyChanges {
+ target: signInItem
+ currentIndex: 1
+ }
+ },
+ State {
+ name: "Page 3"
+ PropertyChanges {
+ target: signInItem
+ currentIndex: 2
+ }
+ }
+ ]
+}
diff --git a/internal/frontend/qml/bridgeqml.qmlproject b/internal/frontend/qml/bridgeqml.qmlproject
index 32007b8c..d46df654 100644
--- a/internal/frontend/qml/bridgeqml.qmlproject
+++ b/internal/frontend/qml/bridgeqml.qmlproject
@@ -1,4 +1,19 @@
-/* File generated by Qt Creator */
+// 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 QmlProject 1.1
diff --git a/internal/frontend/qml/icons/Bridge.icns b/internal/frontend/qml/icons/Bridge.icns
deleted file mode 100644
index 3520a0eb..00000000
Binary files a/internal/frontend/qml/icons/Bridge.icns and /dev/null differ
diff --git a/internal/frontend/qml/icons/Loader_16.svg b/internal/frontend/qml/icons/Loader_16.svg
new file mode 100644
index 00000000..34209ebc
--- /dev/null
+++ b/internal/frontend/qml/icons/Loader_16.svg
@@ -0,0 +1,4 @@
+
diff --git a/internal/frontend/qml/icons/Loader_48.svg b/internal/frontend/qml/icons/Loader_48.svg
new file mode 100644
index 00000000..227ea504
--- /dev/null
+++ b/internal/frontend/qml/icons/Loader_48.svg
@@ -0,0 +1,4 @@
+
diff --git a/internal/frontend/qml/icons/all_icons.svg b/internal/frontend/qml/icons/all_icons.svg
deleted file mode 100644
index 55298343..00000000
--- a/internal/frontend/qml/icons/all_icons.svg
+++ /dev/null
@@ -1,541 +0,0 @@
-
-
diff --git a/internal/frontend/qml/icons/black-syserror.png b/internal/frontend/qml/icons/black-syserror.png
deleted file mode 100644
index d1366d0e..00000000
Binary files a/internal/frontend/qml/icons/black-syserror.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/black-systray.png b/internal/frontend/qml/icons/black-systray.png
deleted file mode 100644
index af418a2d..00000000
Binary files a/internal/frontend/qml/icons/black-systray.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/black-syswarn.png b/internal/frontend/qml/icons/black-syswarn.png
deleted file mode 100644
index 86f1aa27..00000000
Binary files a/internal/frontend/qml/icons/black-syswarn.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/envelope_open.png b/internal/frontend/qml/icons/envelope_open.png
deleted file mode 100644
index be8d2455..00000000
Binary files a/internal/frontend/qml/icons/envelope_open.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/export.sh b/internal/frontend/qml/icons/export.sh
deleted file mode 100644
index 2f71e7af..00000000
--- a/internal/frontend/qml/icons/export.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/bash
-
-# 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 .
-
-# create bitmaps
-for shape in rounded rectangle
-do
- for usage in systray syswarn app
- do
- group=$shape-$usage
- inkscape --without-gui --export-id=$group --export-png=$group.png all_icons.svg
- done
-done
-
-# mac icon
-png2icns Bridge.icns rounded-app.png
-
-# windows icon
-convert rectangle-app.png -define icon:auto-resize=256,128,64,48,32,16 logo.ico
diff --git a/internal/frontend/qml/icons/folder_open.png b/internal/frontend/qml/icons/folder_open.png
deleted file mode 100644
index 4cc1694a..00000000
Binary files a/internal/frontend/qml/icons/folder_open.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/ic-apple-mail.svg b/internal/frontend/qml/icons/ic-apple-mail.svg
new file mode 100644
index 00000000..95191cae
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-apple-mail.svg
@@ -0,0 +1,10 @@
+
diff --git a/internal/frontend/qml/icons/ic-arrow-left.svg b/internal/frontend/qml/icons/ic-arrow-left.svg
new file mode 100644
index 00000000..b7c8f881
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-arrow-left.svg
@@ -0,0 +1,5 @@
+
diff --git a/internal/frontend/qml/icons/ic-check.svg b/internal/frontend/qml/icons/ic-check.svg
new file mode 100644
index 00000000..906501a0
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-check.svg
@@ -0,0 +1,3 @@
+
diff --git a/internal/frontend/qml/icons/ic-cog-wheel.svg b/internal/frontend/qml/icons/ic-cog-wheel.svg
new file mode 100644
index 00000000..1866a48f
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-cog-wheel.svg
@@ -0,0 +1,3 @@
+
diff --git a/internal/frontend/qml/icons/ic-connected.svg b/internal/frontend/qml/icons/ic-connected.svg
new file mode 100644
index 00000000..83da482d
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-connected.svg
@@ -0,0 +1,5 @@
+
diff --git a/internal/frontend/qml/icons/ic-cross-close.svg b/internal/frontend/qml/icons/ic-cross-close.svg
new file mode 100644
index 00000000..0702537a
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-cross-close.svg
@@ -0,0 +1,3 @@
+
diff --git a/internal/frontend/qml/icons/ic-exclamation-circle-filled.svg b/internal/frontend/qml/icons/ic-exclamation-circle-filled.svg
new file mode 100644
index 00000000..29e6955a
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-exclamation-circle-filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/internal/frontend/qml/icons/ic-eye-slash.svg b/internal/frontend/qml/icons/ic-eye-slash.svg
new file mode 100644
index 00000000..d4f1a890
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-eye-slash.svg
@@ -0,0 +1,9 @@
+
diff --git a/internal/frontend/qml/icons/ic-eye.svg b/internal/frontend/qml/icons/ic-eye.svg
new file mode 100644
index 00000000..eedcf8be
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-eye.svg
@@ -0,0 +1,8 @@
+
diff --git a/internal/frontend/qml/icons/ic-info-circle-filled.svg b/internal/frontend/qml/icons/ic-info-circle-filled.svg
new file mode 100644
index 00000000..529e4152
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-info-circle-filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/internal/frontend/qml/icons/ic-microsoft-outlook.svg b/internal/frontend/qml/icons/ic-microsoft-outlook.svg
new file mode 100644
index 00000000..8dcf7241
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-microsoft-outlook.svg
@@ -0,0 +1,20 @@
+
diff --git a/internal/frontend/qml/icons/ic-mozilla-thunderbird.svg b/internal/frontend/qml/icons/ic-mozilla-thunderbird.svg
new file mode 100644
index 00000000..83759ef0
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-mozilla-thunderbird.svg
@@ -0,0 +1,112 @@
+
diff --git a/internal/frontend/qml/icons/ic-other-mail-clients.svg b/internal/frontend/qml/icons/ic-other-mail-clients.svg
new file mode 100644
index 00000000..a4df3a98
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-other-mail-clients.svg
@@ -0,0 +1,29 @@
+
diff --git a/internal/frontend/qml/icons/ic-plus.svg b/internal/frontend/qml/icons/ic-plus.svg
new file mode 100644
index 00000000..aba5f8e7
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-plus.svg
@@ -0,0 +1,3 @@
+
diff --git a/internal/frontend/qml/icons/ic-question-circle.svg b/internal/frontend/qml/icons/ic-question-circle.svg
new file mode 100644
index 00000000..62857444
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-question-circle.svg
@@ -0,0 +1,9 @@
+
diff --git a/internal/frontend/qml/icons/ic-systray.svg b/internal/frontend/qml/icons/ic-systray.svg
new file mode 100644
index 00000000..eca43ff8
--- /dev/null
+++ b/internal/frontend/qml/icons/ic-systray.svg
@@ -0,0 +1,17 @@
+
diff --git a/internal/frontend/qml/icons/ie.icns b/internal/frontend/qml/icons/ie.icns
deleted file mode 100644
index 6b67c558..00000000
Binary files a/internal/frontend/qml/icons/ie.icns and /dev/null differ
diff --git a/internal/frontend/qml/icons/ie.ico b/internal/frontend/qml/icons/ie.ico
deleted file mode 100644
index 375b1b49..00000000
Binary files a/internal/frontend/qml/icons/ie.ico and /dev/null differ
diff --git a/internal/frontend/qml/icons/ie.svg b/internal/frontend/qml/icons/ie.svg
deleted file mode 100644
index 58446cea..00000000
--- a/internal/frontend/qml/icons/ie.svg
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
diff --git a/internal/frontend/qml/icons/img-welcome.svg b/internal/frontend/qml/icons/img-welcome.svg
new file mode 100644
index 00000000..514a50f8
--- /dev/null
+++ b/internal/frontend/qml/icons/img-welcome.svg
@@ -0,0 +1,62 @@
+
diff --git a/internal/frontend/qml/icons/logo.ico b/internal/frontend/qml/icons/logo.ico
deleted file mode 100644
index 2b0e2f0f..00000000
Binary files a/internal/frontend/qml/icons/logo.ico and /dev/null differ
diff --git a/internal/frontend/qml/icons/logo.svg b/internal/frontend/qml/icons/logo.svg
deleted file mode 100644
index dc807142..00000000
--- a/internal/frontend/qml/icons/logo.svg
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
diff --git a/internal/frontend/qml/icons/macos_gray.png b/internal/frontend/qml/icons/macos_gray.png
deleted file mode 100644
index d941f37e..00000000
Binary files a/internal/frontend/qml/icons/macos_gray.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/macos_green.png b/internal/frontend/qml/icons/macos_green.png
deleted file mode 100644
index 63b9584b..00000000
Binary files a/internal/frontend/qml/icons/macos_green.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/macos_green_dark.png b/internal/frontend/qml/icons/macos_green_dark.png
deleted file mode 100644
index da9982f3..00000000
Binary files a/internal/frontend/qml/icons/macos_green_dark.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/macos_green_hl.png b/internal/frontend/qml/icons/macos_green_hl.png
deleted file mode 100644
index 5522de40..00000000
Binary files a/internal/frontend/qml/icons/macos_green_hl.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/macos_red.png b/internal/frontend/qml/icons/macos_red.png
deleted file mode 100644
index 28249a40..00000000
Binary files a/internal/frontend/qml/icons/macos_red.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/macos_red_dark.png b/internal/frontend/qml/icons/macos_red_dark.png
deleted file mode 100644
index 2af74b4e..00000000
Binary files a/internal/frontend/qml/icons/macos_red_dark.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/macos_red_hl.png b/internal/frontend/qml/icons/macos_red_hl.png
deleted file mode 100644
index fbaf845b..00000000
Binary files a/internal/frontend/qml/icons/macos_red_hl.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/macos_yellow.png b/internal/frontend/qml/icons/macos_yellow.png
deleted file mode 100644
index 073b3f13..00000000
Binary files a/internal/frontend/qml/icons/macos_yellow.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/macos_yellow_dark.png b/internal/frontend/qml/icons/macos_yellow_dark.png
deleted file mode 100644
index aa3252d0..00000000
Binary files a/internal/frontend/qml/icons/macos_yellow_dark.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/macos_yellow_hl.png b/internal/frontend/qml/icons/macos_yellow_hl.png
deleted file mode 100644
index 4b8a4406..00000000
Binary files a/internal/frontend/qml/icons/macos_yellow_hl.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/pm_logo.png b/internal/frontend/qml/icons/pm_logo.png
deleted file mode 100644
index d1e51430..00000000
Binary files a/internal/frontend/qml/icons/pm_logo.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/product_logos.svg b/internal/frontend/qml/icons/product_logos.svg
new file mode 100644
index 00000000..b6878137
--- /dev/null
+++ b/internal/frontend/qml/icons/product_logos.svg
@@ -0,0 +1,15 @@
+
diff --git a/internal/frontend/qml/icons/rectangle-app.png b/internal/frontend/qml/icons/rectangle-app.png
deleted file mode 100644
index c2da3151..00000000
Binary files a/internal/frontend/qml/icons/rectangle-app.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/rectangle-systray.png b/internal/frontend/qml/icons/rectangle-systray.png
deleted file mode 100644
index 39cd4f9b..00000000
Binary files a/internal/frontend/qml/icons/rectangle-systray.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/rectangle-syswarn.png b/internal/frontend/qml/icons/rectangle-syswarn.png
deleted file mode 100644
index 77b15385..00000000
Binary files a/internal/frontend/qml/icons/rectangle-syswarn.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/rounded-app.png b/internal/frontend/qml/icons/rounded-app.png
deleted file mode 100644
index 99222ac1..00000000
Binary files a/internal/frontend/qml/icons/rounded-app.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/rounded-app.svg b/internal/frontend/qml/icons/rounded-app.svg
deleted file mode 100644
index 9825d2d3..00000000
--- a/internal/frontend/qml/icons/rounded-app.svg
+++ /dev/null
@@ -1,104 +0,0 @@
-
-
diff --git a/internal/frontend/qml/icons/rounded-systray.png b/internal/frontend/qml/icons/rounded-systray.png
deleted file mode 100644
index c488be35..00000000
Binary files a/internal/frontend/qml/icons/rounded-systray.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/rounded-syswarn.png b/internal/frontend/qml/icons/rounded-syswarn.png
deleted file mode 100644
index 6c49d578..00000000
Binary files a/internal/frontend/qml/icons/rounded-syswarn.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/white-syserror.png b/internal/frontend/qml/icons/white-syserror.png
deleted file mode 100644
index f49901f4..00000000
Binary files a/internal/frontend/qml/icons/white-syserror.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/white-systray.png b/internal/frontend/qml/icons/white-systray.png
deleted file mode 100644
index 7d152a4f..00000000
Binary files a/internal/frontend/qml/icons/white-systray.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/white-syswarn.png b/internal/frontend/qml/icons/white-syswarn.png
deleted file mode 100644
index 7f4fe073..00000000
Binary files a/internal/frontend/qml/icons/white-syswarn.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/win10_Dash.png b/internal/frontend/qml/icons/win10_Dash.png
deleted file mode 100644
index 019d5a99..00000000
Binary files a/internal/frontend/qml/icons/win10_Dash.png and /dev/null differ
diff --git a/internal/frontend/qml/icons/win10_Times.png b/internal/frontend/qml/icons/win10_Times.png
deleted file mode 100644
index 5bae8b92..00000000
Binary files a/internal/frontend/qml/icons/win10_Times.png and /dev/null differ
diff --git a/internal/frontend/qml/qtquickcontrols2.conf b/internal/frontend/qml/qtquickcontrols2.conf
deleted file mode 100644
index 3512d512..00000000
--- a/internal/frontend/qml/qtquickcontrols2.conf
+++ /dev/null
@@ -1,2 +0,0 @@
-[Controls]
-Style=Proton
diff --git a/internal/frontend/qml/tests/Buttons.qml b/internal/frontend/qml/tests/Buttons.qml
new file mode 100644
index 00000000..2a4bc4fe
--- /dev/null
+++ b/internal/frontend/qml/tests/Buttons.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.Window 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.12
+
+import Proton 4.0
+
+RowLayout {
+ property var colorScheme: parent.colorScheme
+
+ // Primary buttons
+ ButtonsColumn {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ iconLoading: "../icons/Loader_16.svg"
+ }
+
+ // Secondary buttons
+ ButtonsColumn {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ secondary: true
+ iconLoading: "../icons/Loader_16.svg"
+ }
+
+ // Secondary icons
+ ButtonsColumn {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ secondary: true
+ textNormal: ""
+ iconNormal: "../icons/ic-cross-close.svg"
+ textDisabled: ""
+ iconDisabled: "../icons/ic-cross-close.svg"
+ textLoading: ""
+ iconLoading: "../icons/Loader_16.svg"
+ }
+
+ // Icons
+ ButtonsColumn {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ textNormal: ""
+ iconNormal: "../icons/ic-cross-close.svg"
+ textDisabled: ""
+ iconDisabled: "../icons/ic-cross-close.svg"
+ textLoading: ""
+ iconLoading: "../icons/Loader_16.svg"
+ }
+}
diff --git a/internal/frontend/qml/tests/ButtonsColumn.qml b/internal/frontend/qml/tests/ButtonsColumn.qml
new file mode 100644
index 00000000..3420269e
--- /dev/null
+++ b/internal/frontend/qml/tests/ButtonsColumn.qml
@@ -0,0 +1,72 @@
+// 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.Layouts 1.12
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import Proton 4.0
+
+ColumnLayout {
+ id: root
+ property var colorScheme: parent.colorScheme
+
+ property string textNormal: "Button"
+ property string iconNormal: ""
+ property string textDisabled: "Disabled"
+ property string iconDisabled: ""
+ property string textLoading: "Loading"
+ property string iconLoading: ""
+ property bool secondary: false
+
+ Button {
+ Layout.fillWidth: true
+
+ Layout.minimumHeight: implicitHeight
+ Layout.minimumWidth: implicitWidth
+
+ text: root.textNormal
+ icon.source: iconNormal
+ secondary: root.secondary
+ }
+
+
+ Button {
+ Layout.fillWidth: true
+
+ Layout.minimumHeight: implicitHeight
+ Layout.minimumWidth: implicitWidth
+
+ text: root.textDisabled
+ icon.source: iconDisabled
+ secondary: root.secondary
+
+ enabled: false
+ }
+
+ Button {
+ Layout.fillWidth: true
+
+ Layout.minimumHeight: implicitHeight
+ Layout.minimumWidth: implicitWidth
+
+ text: root.textLoading
+ icon.source: iconLoading
+ secondary: root.secondary
+
+ loading: true
+ }
+}
diff --git a/internal/frontend/qml/tests/CheckBoxes.qml b/internal/frontend/qml/tests/CheckBoxes.qml
new file mode 100644
index 00000000..287c61fe
--- /dev/null
+++ b/internal/frontend/qml/tests/CheckBoxes.qml
@@ -0,0 +1,101 @@
+// 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.Window 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.12
+
+import Proton 4.0
+
+RowLayout {
+ property var colorScheme: parent.colorScheme
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ property var colorScheme: parent.colorScheme
+
+ spacing: parent.spacing
+
+ CheckBox {
+ text: "Checkbox"
+ }
+
+ CheckBox {
+ text: "Checkbox"
+ error: true
+ }
+
+ CheckBox {
+ text: "Checkbox"
+ enabled: false
+ }
+ CheckBox {
+ text: ""
+ }
+
+ CheckBox {
+ text: ""
+ error: true
+ }
+
+ CheckBox {
+ text: ""
+ enabled: false
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ property var colorScheme: parent.colorScheme
+
+ spacing: parent.spacing
+
+ CheckBox {
+ text: "Checkbox"
+ checked: true
+ }
+
+ CheckBox {
+ text: "Checkbox"
+ checked: true
+ error: true
+ }
+
+ CheckBox {
+ text: "Checkbox"
+ checked: true
+ enabled: false
+ }
+ CheckBox {
+ text: ""
+ checked: true
+ }
+
+ CheckBox {
+ text: ""
+ checked: true
+ error: true
+ }
+
+ CheckBox {
+ text: ""
+ checked: true
+ enabled: false
+ }
+ }
+}
diff --git a/internal/frontend/qml/tests/RadioButtons.qml b/internal/frontend/qml/tests/RadioButtons.qml
new file mode 100644
index 00000000..26fcc3b7
--- /dev/null
+++ b/internal/frontend/qml/tests/RadioButtons.qml
@@ -0,0 +1,101 @@
+// 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.Window 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.12
+
+import Proton 4.0
+
+RowLayout {
+ property var colorScheme: parent.colorScheme
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ property var colorScheme: parent.colorScheme
+
+ spacing: parent.spacing
+
+ RadioButton {
+ text: "Radio"
+ }
+
+ RadioButton {
+ text: "Radio"
+ error: true
+ }
+
+ RadioButton {
+ text: "Radio"
+ enabled: false
+ }
+ RadioButton {
+ text: ""
+ }
+
+ RadioButton {
+ text: ""
+ error: true
+ }
+
+ RadioButton {
+ text: ""
+ enabled: false
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ property var colorScheme: parent.colorScheme
+
+ spacing: parent.spacing
+
+ RadioButton {
+ text: "Radio"
+ checked: true
+ }
+
+ RadioButton {
+ text: "Radio"
+ checked: true
+ error: true
+ }
+
+ RadioButton {
+ text: "Radio"
+ checked: true
+ enabled: false
+ }
+ RadioButton {
+ text: ""
+ checked: true
+ }
+
+ RadioButton {
+ text: ""
+ checked: true
+ error: true
+ }
+
+ RadioButton {
+ text: ""
+ checked: true
+ enabled: false
+ }
+ }
+}
diff --git a/internal/frontend/qml/tests/Switches.qml b/internal/frontend/qml/tests/Switches.qml
new file mode 100644
index 00000000..87be88a9
--- /dev/null
+++ b/internal/frontend/qml/tests/Switches.qml
@@ -0,0 +1,103 @@
+// 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.Window 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.12
+
+import Proton 4.0
+
+RowLayout {
+ property var colorScheme: parent.colorScheme
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ property var colorScheme: parent.colorScheme
+
+ spacing: parent.spacing
+
+ Switch {
+ text: "Toggle"
+ }
+
+ Switch {
+ text: "Toggle"
+ enabled: false
+ }
+
+ Switch {
+ text: "Toggle"
+ loading: true
+ }
+
+ Switch {
+ text: ""
+ }
+
+ Switch {
+ text: ""
+ enabled: false
+ }
+
+ Switch {
+ text: ""
+ loading: true
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ property var colorScheme: parent.colorScheme
+
+ spacing: parent.spacing
+
+ Switch {
+ text: "Toggle"
+ checked: true
+ }
+
+ Switch {
+ text: "Toggle"
+ checked: true
+ enabled: false
+ }
+
+ Switch {
+ text: "Toggle"
+ checked: true
+ loading: true
+ }
+
+ Switch {
+ text: ""
+ checked: true
+ }
+
+ Switch {
+ text: ""
+ checked: true
+ enabled: false
+ }
+
+ Switch {
+ text: ""
+ checked: true
+ loading: true
+ }
+ }
+}
diff --git a/internal/frontend/qml/tests/TestComponents.qml b/internal/frontend/qml/tests/TestComponents.qml
new file mode 100644
index 00000000..aed175be
--- /dev/null
+++ b/internal/frontend/qml/tests/TestComponents.qml
@@ -0,0 +1,65 @@
+// 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
+
+Rectangle {
+ property var colorScheme
+ color: colorScheme.background_norm
+ clip: true
+
+ ColumnLayout {
+ anchors.fill: parent
+ property var colorScheme: parent.colorScheme
+
+ spacing: 5
+
+ Buttons {
+ Layout.fillWidth: true
+ Layout.margins: 20
+ }
+
+ TextFields {
+ Layout.fillWidth: true
+ Layout.margins: 20
+ }
+
+ TextAreas {
+ Layout.fillWidth: true
+ Layout.margins: 20
+ }
+
+ CheckBoxes {
+ Layout.fillWidth: true
+ Layout.margins: 20
+ }
+
+ RadioButtons {
+ Layout.fillWidth: true
+ Layout.margins: 20
+ }
+
+ Switches {
+ Layout.fillWidth: true
+ Layout.margins: 20
+ }
+ }
+}
diff --git a/internal/frontend/qml/tests/TextAreas.qml b/internal/frontend/qml/tests/TextAreas.qml
new file mode 100644
index 00000000..054c3530
--- /dev/null
+++ b/internal/frontend/qml/tests/TextAreas.qml
@@ -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 .
+
+import QtQuick 2.13
+import QtQuick.Window 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.12
+
+import Proton 4.0
+
+RowLayout {
+ property var colorScheme: parent.colorScheme
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ property var colorScheme: parent.colorScheme
+
+ spacing: parent.spacing
+
+ TextArea {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 100
+
+ placeholderText: "Placeholder"
+ label: "Label"
+ hint: "Hint"
+ assistiveText: "Assistive text"
+ }
+
+ TextArea {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 100
+
+ text: "Value"
+ placeholderText: "Placeholder"
+ label: "Label"
+ hint: "Hint"
+ assistiveText: "Assistive text"
+ }
+
+
+ TextArea {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 100
+
+ error: true
+
+ text: "Value"
+ placeholderText: "Placeholder"
+ label: "Label"
+ hint: "Hint"
+ assistiveText: "Error message"
+ }
+
+
+ TextArea {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 100
+
+ enabled: false
+
+ text: "Value"
+ placeholderText: "Placeholder"
+ label: "Label"
+ hint: "Hint"
+ assistiveText: "Assistive text"
+ }
+ }
+}
diff --git a/internal/frontend/qml/tests/TextFields.qml b/internal/frontend/qml/tests/TextFields.qml
new file mode 100644
index 00000000..9ea770ee
--- /dev/null
+++ b/internal/frontend/qml/tests/TextFields.qml
@@ -0,0 +1,181 @@
+// 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.Window 2.13
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.12
+
+import Proton 4.0
+
+RowLayout {
+ property var colorScheme: parent.colorScheme
+
+ // Norm
+ ColumnLayout {
+ Layout.fillWidth: true
+ property var colorScheme: parent.colorScheme
+
+ spacing: parent.spacing
+
+ TextField {
+ Layout.fillWidth: true
+
+ placeholderText: "Placeholder"
+ label: "Label"
+ hint: "Hint"
+ assistiveText: "Assistive text"
+ }
+
+ TextField {
+ Layout.fillWidth: true
+
+ text: "Value"
+ placeholderText: "Placeholder"
+ label: "Label"
+ hint: "Hint"
+ assistiveText: "Assistive text"
+ }
+
+
+ TextField {
+ Layout.fillWidth: true
+ error: true
+
+ text: "Value"
+ placeholderText: "Placeholder"
+ label: "Label"
+ hint: "Hint"
+ assistiveText: "Error message"
+ }
+
+
+ TextField {
+ Layout.fillWidth: true
+
+ text: "Value"
+ placeholderText: "Placeholder"
+ label: "Label"
+ hint: "Hint"
+ assistiveText: "Assistive text"
+
+ enabled: false
+ }
+ }
+
+ // Masked
+ ColumnLayout {
+ Layout.fillWidth: true
+ property var colorScheme: parent.colorScheme
+
+ spacing: parent.spacing
+
+ TextField {
+ Layout.fillWidth: true
+ echoMode: TextInput.Password
+ placeholderText: "Password"
+ label: "Label"
+ hint: "Hint"
+ assistiveText: "Assistive text"
+ }
+
+ TextField {
+ Layout.fillWidth: true
+ text: "Password"
+
+ echoMode: TextInput.Password
+ placeholderText: "Password"
+ label: "Label"
+ hint: "Hint"
+ assistiveText: "Assistive text"
+ }
+
+ TextField {
+ Layout.fillWidth: true
+ text: "Password"
+ error: true
+
+ echoMode: TextInput.Password
+ placeholderText: "Password"
+ label: "Label"
+ hint: "Hint"
+ assistiveText: "Error message"
+ }
+
+ TextField {
+ Layout.fillWidth: true
+ text: "Password"
+ enabled: false
+
+ echoMode: TextInput.Password
+ placeholderText: "Password"
+ label: "Label"
+ hint: "Hint"
+ assistiveText: "Assistive text"
+ }
+ }
+
+ // Varia
+ ColumnLayout {
+ Layout.fillWidth: true
+ property var colorScheme: parent.colorScheme
+
+ spacing: parent.spacing
+
+ TextField {
+ Layout.fillWidth: true
+
+ placeholderText: "Placeholder"
+ label: "Label"
+ hint: "Hint"
+
+ Rectangle {
+ anchors.fill: parent
+ border.color: "red"
+ border.width: 1
+ z: parent.z - 1
+ }
+ }
+
+ TextField {
+ Layout.fillWidth: true
+
+ placeholderText: "Placeholder"
+ assistiveText: "Assistive text"
+
+ Rectangle {
+ anchors.fill: parent
+ border.color: "red"
+ border.width: 1
+ z: parent.z - 1
+ }
+ }
+
+ TextField {
+ Layout.fillWidth: true
+
+ placeholderText: "Placeholder"
+
+ Rectangle {
+ anchors.fill: parent
+ border.color: "red"
+ border.width: 1
+ z: parent.z - 1
+ }
+ }
+ }
+}