GODT-1346: GODT-1340 GODT-1315 QML changes

GODT-1365: Create ComboBox component
GODT-1338: GODT-1343 Help view buttons
GODT-1340: Not crashing, user list updating in main thread.
GODT-1345: adding panic handlers
This commit is contained in:
Jakub Cuth
2021-09-28 12:45:47 +00:00
committed by Jakub
parent 2c8feff97a
commit d11cf57879
46 changed files with 1267 additions and 727 deletions

View File

@ -99,10 +99,10 @@ func saveConfigTemporarily(mc *mobileconfig.Config) (fname string, err error) {
}
// Make sure the temporary file is deleted.
go (func() {
go func() {
<-time.After(10 * time.Minute)
_ = os.RemoveAll(dir)
})()
}()
// Make sure the file is only readable for the current user.
fname = filepath.Clean(filepath.Join(dir, "protonmail.mobileconfig"))

View File

@ -48,9 +48,9 @@ Item {
if (root.usedFraction < .75) return root.colorScheme.signal_warning
return root.colorScheme.signal_danger
}
property real usedFraction: root.user.totalBytes ? root.user.usedBytes / root.user.totalBytes : 0
property string totalSpace: root.spaceWithUnits(root.user.totalBytes)
property string usedSpace: root.spaceWithUnits(root.user.usedBytes)
property real usedFraction: root.user && root.user.totalBytes ? Math.abs(root.user.usedBytes / root.user.totalBytes) : 0
property string totalSpace: root.spaceWithUnits(root.user ? root.user.totalBytes : 0)
property string usedSpace: root.spaceWithUnits(root.user ? root.user.usedBytes : 0)
function spaceWithUnits(bytes){
if (bytes*1 !== bytes ) return "0 kB"
@ -96,7 +96,7 @@ Item {
Label {
colorScheme: root.colorScheme
anchors.fill: parent
text: root.user.avatarText.toUpperCase()
text: root.user ? root.user.avatarText.toUpperCase(): ""
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Body
@ -128,7 +128,7 @@ Item {
)
colorScheme: root.colorScheme
text: user.username
text: root.user ? user.username : ""
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Body
@ -143,7 +143,7 @@ Item {
RowLayout {
Label {
colorScheme: root.colorScheme
text: user.loggedIn ? root.usedSpace : qsTr("Signed out")
text: root.user && root.user.loggedIn ? root.usedSpace : qsTr("Signed out")
color: root.usedSpaceColor
type: {
switch (root.type) {
@ -155,7 +155,7 @@ Item {
Label {
colorScheme: root.colorScheme
text: user.loggedIn ? " / " + root.totalSpace : ""
text: root.user && root.user.loggedIn ? " / " + root.totalSpace : ""
color: root.colorScheme.text_weak
type: {
switch (root.type) {
@ -168,7 +168,7 @@ Item {
Rectangle {
visible: root.type == AccountDelegate.LargeView && user.loggedIn
visible: root.user ? root.type == AccountDelegate.LargeView : false
width: 140
height: 4
radius: 3
@ -177,6 +177,7 @@ Item {
Rectangle {
radius: 3
color: root.usedSpaceColor
visible: root.user ? parent.visible && root.user.loggedIn : false
anchors {
top : parent.top
bottom : parent.bottom

View File

@ -21,246 +21,216 @@ import QtQuick.Controls 2.12
import Proton 4.0
ScrollView {
Item {
id: root
property ColorScheme colorScheme
property var backend
property var notifications
property var user
clip: true
contentWidth: pane.width
contentHeight: pane.height
property int _leftRightMargins: 64
property int _topBottomMargins: 68
property int _spacing: 22
Rectangle {
anchors {
bottom: pane.bottom
}
color: root.colorScheme.background_weak
width: root.width
height: configuration.height + root._topBottomMargins
}
signal showSignIn()
signal showSetupGuide(var user, string address)
ColumnLayout {
id: pane
property int _leftMargin: 64
property int _rightMargin: 64
property int _topMargin: 32
property int _detailsTopMargin: 25
property int _bottomMargin: 12
property int _spacing: 20
property int _lineWidth: 1
width: root.width
ScrollView {
clip: true
anchors.fill: parent
ColumnLayout {
spacing: root._spacing
Layout.topMargin: root._topBottomMargins
Layout.leftMargin: root._leftRightMargins
Layout.rightMargin: root._leftRightMargins
Layout.maximumWidth: root.width - 2*root._leftRightMargins
width: root.width
spacing: 0
Rectangle {
id: topRectangle
color: root.colorScheme.background_norm
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
RowLayout { // account delegate with action buttons
Layout.fillWidth: true
AccountDelegate {
Layout.fillWidth: true
colorScheme: root.colorScheme
user: root.user
type: AccountDelegate.LargeView
enabled: root.user.loggedIn
}
ColumnLayout {
spacing: root._spacing
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
text: qsTr("Sign out")
secondary: true
visible: root.user.loggedIn
onClicked: root.user.logout()
}
anchors.fill: parent
anchors.leftMargin: root._leftMargin
anchors.rightMargin: root._rightMargin
anchors.topMargin: root._topMargin
anchors.bottomMargin: root._bottomMargin
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
text: qsTr("Sign in")
secondary: true
visible: !root.user.loggedIn
enabled: !root.user.loggedIn
onClicked: root.parent.rightContent.showSignIn()
}
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
icon.source: "icons/ic-trash.svg"
secondary: true
visible: true
enabled: true
onClicked: root.user.remove()
RowLayout { // account delegate with action buttons
Layout.fillWidth: true
AccountDelegate {
Layout.fillWidth: true
colorScheme: root.colorScheme
user: root.user
type: AccountDelegate.LargeView
enabled: root.user ? root.user.loggedIn : false
}
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
text: qsTr("Sign out")
secondary: true
visible: root.user ? root.user.loggedIn : false
onClicked: {
if (!root.user) return
root.user.logout()
}
}
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
text: qsTr("Sign in")
secondary: true
visible: root.user ? !root.user.loggedIn : false
onClicked: {
if (!root.user) return
root.parent.rightContent.showSignIn()
}
}
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
icon.source: "icons/ic-trash.svg"
secondary: true
onClicked: {
if (!root.user) return
root.user.remove()
}
}
}
Rectangle {
Layout.fillWidth: true
height: root._lineWidth
color: root.colorScheme.border_weak
}
SettingsItem {
colorScheme: root.colorScheme
text: qsTr("Email clients")
actionText: qsTr("Configure")
description: qsTr("Proton Mail Bridge works with email clients that support IMAP/SMPT to send and receive messages. Using the mailbox details below, you can (re)configure your client at any point.")
type: SettingsItem.Button
enabled: root.user ? root.user.loggedIn : false
visible: root.user ? !root.user.splitMode || root.user.addresses.length==1 : false
showSeparator: splitMode.visible
onClicked: {
if (!root.user) return
root.showSetupGuide(root.user, user.addresses[0])
}
Layout.fillWidth: true
}
SettingsItem {
id: splitMode
colorScheme: root.colorScheme
text: qsTr("Split addresses")
description: qsTr("Split addresses allows you to configure multiple email addresses individually. Changing its mode will require you to delete your accounts(s) from your email client and begin the setup process from scratch.")
type: SettingsItem.Toggle
checked: root.user ? root.user.splitMode : false
visible: root.user ? root.user.addresses.length > 1 : false
enabled: root.user ? root.user.loggedIn : false
showSeparator: addressSelector.visible
onClicked: {
if (!splitMode.checked){
root.notifications.askEnableSplitMode(user)
} else {
root.user.toggleSplitMode(!splitMode.checked)
}
}
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
enabled: root.user ? root.user.loggedIn : false
visible: root.user ? root.user.splitMode : false
ComboBox {
id: addressSelector
colorScheme: root.colorScheme
Layout.fillWidth: true
model: root.user ? root.user.addresses : null
}
Button {
colorScheme: root.colorScheme
text: qsTr("Configure")
secondary: true
onClicked: {
if (!root.user) return
root.showSetupGuide(root.user, addressSelector.displayText)
}
}
}
}
}
Rectangle {
color: root.colorScheme.background_weak
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak
}
SettingsItem {
colorScheme: root.colorScheme
text: qsTr("Email clients")
actionText: qsTr("Configure")
description: "MISSING WIREFRAME" // TODO
type: SettingsItem.Button
enabled: root.user.loggedIn
visible: !root.user.splitMode
onClicked: root.showSetupGuide(root.user,user.addresses[0])
}
ColumnLayout {
id: configuration
SettingsItem {
id: splitMode
colorScheme: root.colorScheme
text: qsTr("Split addresses")
description: qsTr("Split addresses allows you to configure multiple email addresses individually. Changing its mode will require you to delete your accounts(s) from your email client and begin the setup process from scratch.")
type: SettingsItem.Toggle
checked: root.user.splitMode
visible: root.user.addresses.length > 1
enabled: root.user.loggedIn
onClicked: {
if (!splitMode.checked){
root.notifications.askEnableSplitMode(user)
} else {
root.user.toggleSplitMode(!splitMode.checked)
}
}
}
anchors.fill: parent
anchors.leftMargin: root._leftMargin
anchors.rightMargin: root._rightMargin
anchors.topMargin: root._detailsTopMargin
anchors.bottomMargin: root._spacing
RowLayout {
Layout.fillWidth: true
enabled: root.user.loggedIn
spacing: root._spacing
visible: root.user ? root.user.loggedIn : false
visible: root.user.splitMode
ComboBox {
id: addressSelector
Layout.fillWidth: true
model: root.user.addresses
property var _topBottomMargins : 8
property var _leftRightMargins : 16
background: RoundedRectangle {
radiusTopLeft : 6
radiusTopRight : 6
radiusBottomLeft : addressSelector.down ? 0 : 6
radiusBottomRight : addressSelector.down ? 0 : 6
height: addressSelector.contentItem.height
//width: addressSelector.contentItem.width
fillColor : root.colorScheme.background_norm
strokeColor : root.colorScheme.border_norm
strokeWidth : 1
}
delegate: Rectangle {
id: listItem
width: root.width
height: children[0].height + 4 + 2*addressSelector._topBottomMargins
Label {
anchors {
top : parent.top
left : parent.left
topMargin : addressSelector._topBottomMargins + 4
leftMargin : addressSelector._leftRightMargins
}
colorScheme: root.colorScheme
text: modelData
elide: Text.ElideMiddle
}
property bool isOver: false
color: {
if (listItem.isOver) return root.colorScheme.interaction_weak_hover
if (addressSelector.highlightedIndex === index) return root.colorScheme.interaction_weak
return root.colorScheme.background_norm
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: listItem.isOver = true
onExited: listItem.isOver = false
onClicked : {
addressSelector.currentIndex = index
addressSelector.popup.close()
}
}
}
contentItem: Label {
topPadding : addressSelector._topBottomMargins+4
bottomPadding : addressSelector._topBottomMargins
leftPadding : addressSelector._leftRightMargins
rightPadding : addressSelector._leftRightMargins
property string currentAddress: addressSelector.displayText
Label {
colorScheme: root.colorScheme
text: addressSelector.displayText
elide: Text.ElideMiddle
text: qsTr("Mailbox details")
type: Label.Body_semibold
}
Configuration {
colorScheme: root.colorScheme
title: qsTr("IMAP")
hostname: root.backend.hostname
port: root.backend.portIMAP.toString()
username: configuration.currentAddress
password: root.user ? root.user.password : ""
security: "STARTTLS"
}
Configuration {
colorScheme: root.colorScheme
title: qsTr("SMTP")
hostname : root.backend.hostname
port : root.backend.portSMTP.toString()
username : configuration.currentAddress
password : root.user ? root.user.password : ""
security : root.backend.useSSLforSMTP ? "SSL" : "STARTTLS"
}
}
Button {
colorScheme: root.colorScheme
text: qsTr("Configure")
secondary: true
onClicked: root.showSetupGuide(root.user, addressSelector.displayText)
}
}
Item {implicitHeight: 1}
}
ColumnLayout {
id: configuration
Layout.bottomMargin: root._topBottomMargins
Layout.leftMargin: root._leftRightMargins
Layout.rightMargin: root._leftRightMargins
Layout.maximumWidth: root.width - 2*root._leftRightMargins
spacing: root._spacing
visible: root.user.loggedIn
property string currentAddress: addressSelector.displayText
Item {height: 1}
Label {
colorScheme: root.colorScheme
text: qsTr("Mailbox details")
type: Label.Body_semibold
}
Configuration {
colorScheme: root.colorScheme
title: qsTr("IMAP")
hostname: root.backend.hostname
port: root.backend.portIMAP.toString()
username: configuration.currentAddress
password: root.user.password
security: "STARTTLS"
}
Configuration {
colorScheme: root.colorScheme
title: qsTr("SMTP")
hostname : root.backend.hostname
port : root.backend.portSMTP.toString()
username : configuration.currentAddress
password : root.user.password
security : root.backend.useSSLforSMTP ? "SSL" : "STARTTLS"
}
}
}

View File

@ -42,19 +42,6 @@ QtObject {
backend: root.backend
notifications: root._notifications
onLogin: {
backend.login(username, password)
}
onLogin2FA: {
backend.login2FA(username, code)
}
onLogin2Password: {
backend.login2Password(username, password)
}
onLoginAbort: {
backend.loginAbort(username)
}
onVisibleChanged: {
backend.dockIconVisible = visible
}
@ -167,12 +154,10 @@ QtObject {
break;
case SystemTrayIcon.Context:
case SystemTrayIcon.Trigger:
calcStatusWindowPosition()
toggleWindow(statusWindow)
break
case SystemTrayIcon.DoubleClick:
case SystemTrayIcon.MiddleClick:
toggleWindow(mainWindow)
calcStatusWindowPosition()
toggleWindow(statusWindow)
break;
default:
break;
@ -181,12 +166,30 @@ QtObject {
}
Component.onCompleted: {
if (root.backend.users.count === 0) {
if (!root.backend) {
console.log("backend not loaded")
}
if (!root.backend.users) {
console.log("users not loaded")
}
var c = root.backend.users.count
var u = root.backend.users.get(0)
// DEBUG
if (c != 0) {
console.log("users non zero", c)
console.log("first user", u )
}
if (c === 0) {
mainWindow.showAndRise()
}
if (root.backend.users.count === 1 && root.backend.users.get(0).loggedIn === false) {
mainWindow.showAndRise()
if (u) {
if (c === 1 && u.loggedIn === false) {
mainWindow.showAndRise()
}
}
if (root.backend.showOnStartup) {

View File

@ -116,10 +116,10 @@ ColumnLayout {
Button {
colorScheme: root.colorScheme
text: "name/pass error"
enabled: user !== undefined && user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordProvided
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordProvided
onClicked: {
user.loginUsernamePasswordError()
root.backend.loginUsernamePasswordError("")
user.resetLoginRequests()
}
}
@ -127,9 +127,9 @@ ColumnLayout {
Button {
colorScheme: root.colorScheme
text: "free user error"
enabled: user !== undefined && user.isLoginRequested
enabled: user !== undefined //&& user.isLoginRequested
onClicked: {
user.loginFreeUserError()
root.backend.loginFreeUserError("")
user.resetLoginRequests()
}
}
@ -137,9 +137,9 @@ ColumnLayout {
Button {
colorScheme: root.colorScheme
text: "connection error"
enabled: user !== undefined && user.isLoginRequested
enabled: user !== undefined //&& user.isLoginRequested
onClicked: {
user.loginConnectionError()
root.backend.loginConnectionError("")
user.resetLoginRequests()
}
}
@ -160,9 +160,9 @@ ColumnLayout {
colorScheme: root.colorScheme
text: "request"
enabled: user !== undefined && user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordRequested
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordRequested
onClicked: {
user.login2FARequested()
root.backend.login2FARequested()
user.isLogin2FARequested = true
}
}
@ -171,9 +171,9 @@ ColumnLayout {
colorScheme: root.colorScheme
text: "error"
enabled: user !== undefined && user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
onClicked: {
user.login2FAError()
root.backend.login2FAError("")
user.isLogin2FAProvided = false
}
}
@ -182,9 +182,9 @@ ColumnLayout {
colorScheme: root.colorScheme
text: "Abort"
enabled: user !== undefined && user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
onClicked: {
user.login2FAErrorAbort()
root.backend.login2FAErrorAbort("")
user.resetLoginRequests()
}
}
@ -205,9 +205,9 @@ ColumnLayout {
colorScheme: root.colorScheme
text: "request"
enabled: user !== undefined && user.isLoginRequested && !user.isLogin2PasswordRequested && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2PasswordRequested && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
onClicked: {
user.login2PasswordRequested()
root.backend.login2PasswordRequested("")
user.isLogin2PasswordRequested = true
}
}
@ -216,9 +216,9 @@ ColumnLayout {
colorScheme: root.colorScheme
text: "error"
enabled: user !== undefined && user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
onClicked: {
user.login2PasswordError()
root.backend.login2PasswordError("")
user.isLogin2PasswordProvided = false
}
@ -228,9 +228,9 @@ ColumnLayout {
colorScheme: root.colorScheme
text: "Abort"
enabled: user !== undefined && user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
onClicked: {
user.login2PasswordErrorAbort()
root.backend.login2PasswordErrorAbort("")
user.resetLoginRequests()
}
}

View File

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

View File

@ -28,24 +28,8 @@ Item {
property var backend
property var notifications
signal login(string username, string password)
signal login2FA(string username, string code)
signal login2Password(string username, string password)
signal loginAbort(string username)
signal showSetupGuide(var user, string address)
property var noUser: QtObject {
property var avatarText: ""
property var username: ""
property var password: ""
property var usedBytes: 1
property var totalBytes: 1
property var loggedIn: false
property var splitMode: false
property var addresses: []
}
RowLayout {
anchors.fill: parent
spacing: 0
@ -183,6 +167,7 @@ Item {
onClicked: {
var user = root.backend.users.get(index)
accounts.currentIndex = index
if (!user) return
if (user.loggedIn) {
rightContent.showAccount()
} else {
@ -248,8 +233,8 @@ Item {
backend: root.backend
notifications: root.notifications
user: {
if (accounts.currentIndex < 0) return root.noUser
if (root.backend.users.count == 0) return root.noUser
if (accounts.currentIndex < 0) return undefined
if (root.backend.users.count == 0) return undefined
return root.backend.users.get(accounts.currentIndex)
}
onShowSignIn: {
@ -261,7 +246,7 @@ Item {
}
}
GridLayout { // 1
GridLayout { // 1 Sign In
columns: 2
Button {
@ -271,7 +256,10 @@ Item {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
onClicked: rightContent.showAccount()
onClicked: {
signIn.abort()
rightContent.showAccount()
}
icon.source: "icons/ic-arrow-left.svg"
secondary: true
horizontalPadding: 8
@ -289,11 +277,6 @@ Item {
colorScheme: root.colorScheme
backend: root.backend
onLogin : { root.backend.login ( username , password ) }
onLogin2FA : { root.backend.login2FA ( username , code ) }
onLogin2Password : { root.backend.login2Password ( username , password ) }
onLoginAbort : { root.backend.loginAbort ( username ) }
}
}
@ -330,7 +313,9 @@ Item {
selectedAddress: {
if (accounts.currentIndex < 0) return ""
if (root.backend.users.count == 0) return ""
return root.backend.users.get(accounts.currentIndex).addresses[0]
var user = root.backend.users.get(accounts.currentIndex)
if (!user) return ""
return user.addresses[0]
}
}
@ -342,6 +327,12 @@ Item {
function showLocalCacheSettings () { rightContent.currentIndex = 5 }
function showHelpView () { rightContent.currentIndex = 6 }
function showBugReport () { rightContent.currentIndex = 7 }
Connections {
target: root.backend
onLoginFinished: rightContent.showAccount()
}
}
}
}
@ -353,5 +344,4 @@ Item {
signIn.username = username
rightContent.showSignIn()
}
}

View File

@ -43,6 +43,8 @@ SettingsView {
type: SettingsItem.Toggle
checked: root.backend.isAutomaticUpdateOn
onClicked: root.backend.toggleAutomaticUpdate(!autoUpdate.checked)
Layout.fillWidth: true
}
SettingsItem {
@ -62,6 +64,8 @@ SettingsView {
autostart.loading = false
}
}
Layout.fillWidth: true
}
SettingsItem {
@ -78,6 +82,8 @@ SettingsView {
root.notifications.askDisableBeta()
}
}
Layout.fillWidth: true
}
RowLayout {
@ -117,6 +123,8 @@ SettingsView {
type: SettingsItem.Toggle
checked: root.backend.isDoHEnabled
onClicked: root.backend.toggleDoH(!doh.checked)
Layout.fillWidth: true
}
SettingsItem {
@ -128,6 +136,8 @@ SettingsView {
description: qsTr("Choose which ports are used by default.")
type: SettingsItem.Button
onClicked: root.parent.showPortSettings()
Layout.fillWidth: true
}
SettingsItem {
@ -139,6 +149,8 @@ SettingsView {
description: qsTr("Change the protocol Bridge and your client use to connect.")
type: SettingsItem.Button
onClicked: root.parent.showSMTPSettings()
Layout.fillWidth: true
}
SettingsItem {
@ -150,6 +162,8 @@ SettingsView {
description: qsTr("Configure Bridge's local cache settings.")
type: SettingsItem.Button
onClicked: root.parent.showLocalCacheSettings()
Layout.fillWidth: true
}
SettingsItem {
@ -163,6 +177,8 @@ SettingsView {
onClicked: {
root.notifications.askResetBridge()
}
Layout.fillWidth: true
}
onBack: root.parent.showAccount()

View File

@ -39,7 +39,9 @@ SettingsView {
actionIcon: "./icons/ic-external-link.svg"
description: qsTr("Get help setting up your client with our instructions and FAQs.")
type: SettingsItem.PrimaryButton
onClicked: {Qt.openUrlExternally("https://protonmail.com/bridge/install")}
onClicked: {Qt.openUrlExternally("https://protonmail.com/support/categories/bridge/")}
Layout.fillWidth: true
}
SettingsItem {
@ -55,6 +57,8 @@ SettingsView {
}
Connections {target: root.backend; onCheckUpdatesFinished: checkUpdates.loading = false}
Layout.fillWidth: true
}
SettingsItem {
@ -64,7 +68,9 @@ SettingsView {
actionText: qsTr("View logs")
description: qsTr("Open and review logs to troubleshoot.")
type: SettingsItem.Button
onClicked: {Qt.openUrlExternally(root.backend.logsPath)}
onClicked: {Qt.openUrlExternally("file://"+root.backend.logsPath)}
Layout.fillWidth: true
}
SettingsItem {
@ -78,6 +84,8 @@ SettingsView {
root.backend.updateCurrentMailClient()
root.parent.showBugReport()
}
Layout.fillWidth: true
}
Label {
@ -91,7 +99,7 @@ SettingsView {
text: {
var version = root.backend.version
var license = qsTr("License")
var licensePath = root.backend.licensePath
var licensePath = "file://"+root.backend.licensePath
var release= qsTr("Release notes")
var releaseNotesLink = root.backend.releaseNotesLink
return `<p style="text-align:center;">Proton Mail Bridge v${version}<br>

View File

@ -54,6 +54,8 @@ SettingsView {
type: SettingsItem.Toggle
checked: root._diskCacheEnabled
onClicked: root._diskCacheEnabled = !root._diskCacheEnabled
Layout.fillWidth: true
}
SettingsItem {
@ -67,6 +69,8 @@ SettingsView {
pathDialog.open()
}
Layout.fillWidth: true
FileDialog {
id: pathDialog
title: qsTr("Select cache location")

View File

@ -41,11 +41,6 @@ ApplicationWindow {
property var backend
property var notifications
signal login(string username, string password)
signal login2FA(string username, string code)
signal login2Password(string username, string password)
signal loginAbort(string username)
// show Setup Guide on every new user
Connections {
target: root.backend.users
@ -98,7 +93,15 @@ ApplicationWindow {
return 1
}
if (backend.users.count === 1 && backend.users.get(0).loggedIn === false) {
var u = backend.users.get(0)
if (!u) {
console.trace()
console.log("empty user")
return 1
}
if (backend.users.count === 1 && u.loggedIn === false) {
return 1
}
@ -121,19 +124,6 @@ ApplicationWindow {
onShowSetupGuide: {
root.showSetup(user,address)
}
onLogin: {
root.login(username, password)
}
onLogin2FA: {
root.login2FA(username, code)
}
onLogin2Password: {
root.login2Password(username, password)
}
onLoginAbort: {
root.loginAbort(username)
}
}
WelcomeGuide {
@ -142,19 +132,6 @@ ApplicationWindow {
Layout.fillHeight: true
Layout.fillWidth: true
onLogin: {
root.login(username, password)
}
onLogin2FA: {
root.login2FA(username, code)
}
onLogin2Password: {
root.login2Password(username, password)
}
onLoginAbort: {
root.loginAbort(username)
}
}
SetupGuide {

View File

@ -56,6 +56,8 @@ QtObject {
root.updateSilentRestartNeeded,
root.updateSilentError,
root.updateIsLatestVersion,
root.loginConnectionError,
root.onlyPaidUsers,
root.disableBeta,
root.enableBeta,
root.bugReportSendSuccess,
@ -119,7 +121,7 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(root.backend.getLandingPage())
Qt.openUrlExternally(root.backend.landingPageLink)
root.updateManualReady.active = false
}
},
@ -174,7 +176,7 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(root.backend.getLandingPage())
Qt.openUrlExternally(root.backend.landingPageLink)
root.updateManualError.active = false
}
},
@ -217,7 +219,7 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(root.backend.getLandingPage())
Qt.openUrlExternally(root.backend.landingPageLink)
root.updateForce.active = false
}
},
@ -252,7 +254,7 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(root.backend.getLandingPage())
Qt.openUrlExternally(root.backend.landingPageLink)
root.updateForceError.active = false
}
},
@ -307,7 +309,7 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(root.backend.getLandingPage())
Qt.openUrlExternally(root.backend.landingPageLink)
root.updateSilentError.active = false
}
}
@ -399,6 +401,52 @@ QtObject {
]
}
// login
property Notification loginConnectionError: Notification {
text: qsTr("Bridge is not able to contact the server, please check your internet connection.")
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration
Connections {
target: root.backend
onLoginConnectionError: {
root.loginConnectionError.active = true
}
}
action: [
Action {
text: qsTr("OK")
onTriggered: {
root.loginConnectionError.active = false
}
}
]
}
property Notification onlyPaidUsers: Notification {
text: qsTr("Bridge is exclusive to our paid plans. Upgrade your account to use Bridge.")
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration
Connections {
target: root.backend
onLoginFreeUserError: {
root.onlyPaidUsers.active = true
}
}
action: [
Action {
text: qsTr("OK")
onTriggered: {
root.onlyPaidUsers.active = false
}
}
]
}
// Bug reports
property Notification bugReportSendSuccess: Notification {
@ -420,9 +468,6 @@ QtObject {
onTriggered: {
root.bugReportSendSuccess.active = false
}
},
Action {
text: "test"
}
]
}

View File

@ -22,8 +22,6 @@ import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
import "."
T.ApplicationWindow {
id: root

View File

@ -19,7 +19,8 @@ import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
import "."
import "." as Proton
T.Button {
property ColorScheme colorScheme
@ -32,7 +33,7 @@ T.Button {
property bool borderless: false
property int labelType: Label.LabelType.Body
property int labelType: Proton.Label.LabelType.Body
// TODO: store previous enabled state and restore it?
// For now assuming that only enabled buttons could have loading state
@ -104,7 +105,7 @@ T.Button {
return control.display === AbstractButton.TextUnderIcon ? textImplicitHeight + iconImplicitHeight + spacing : Math.max(textImplicitHeight, iconImplicitHeight)
}
Label {
Proton.Label {
colorScheme: root.colorScheme
id: label
anchors.left: labelIcon.left

View File

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

View File

@ -21,8 +21,6 @@ import QtQuick.Templates 2.12 as T
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import "."
T.Dialog {
id: root
property ColorScheme colorScheme

View File

@ -19,7 +19,8 @@ import QtQuick 2.13
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
import "."
import "." as Proton
T.Label {
id: root
@ -46,7 +47,7 @@ T.Label {
// weight 700, size 12, height 16, spacing 0.4
Caption_bold
}
property int type: Label.LabelType.Body
property int type: Proton.Label.LabelType.Body
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
palette.link: root.colorScheme.interaction_norm
@ -56,78 +57,78 @@ T.Label {
font.weight: {
switch (root.type) {
case Label.LabelType.Heading:
case Proton.Label.LabelType.Heading:
return Style.fontWeight_700
case Label.LabelType.Title:
case Proton.Label.LabelType.Title:
return Style.fontWeight_700
case Label.LabelType.Lead:
case Proton.Label.LabelType.Lead:
return Style.fontWeight_400
case Label.LabelType.Body:
case Proton.Label.LabelType.Body:
return Style.fontWeight_400
case Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_semibold:
return Style.fontWeight_600
case Label.LabelType.Body_bold:
case Proton.Label.LabelType.Body_bold:
return Style.fontWeight_700
case Label.LabelType.Caption:
case Proton.Label.LabelType.Caption:
return Style.fontWeight_400
case Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_semibold:
return Style.fontWeight_600
case Label.LabelType.Caption_bold:
case Proton.Label.LabelType.Caption_bold:
return Style.fontWeight_700
}
}
font.pixelSize: {
switch (root.type) {
case Label.LabelType.Heading:
case Proton.Label.LabelType.Heading:
return Style.heading_font_size
case Label.LabelType.Title:
case Proton.Label.LabelType.Title:
return Style.title_font_size
case Label.LabelType.Lead:
case Proton.Label.LabelType.Lead:
return Style.lead_font_size
case Label.LabelType.Body:
case Label.LabelType.Body_semibold:
case Label.LabelType.Body_bold:
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return Style.body_font_size
case Label.LabelType.Caption:
case Label.LabelType.Caption_semibold:
case Label.LabelType.Caption_bold:
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return Style.caption_font_size
}
}
lineHeight: {
switch (root.type) {
case Label.LabelType.Heading:
case Proton.Label.LabelType.Heading:
return Style.heading_line_height
case Label.LabelType.Title:
case Proton.Label.LabelType.Title:
return Style.title_line_height
case Label.LabelType.Lead:
case Proton.Label.LabelType.Lead:
return Style.lead_line_height
case Label.LabelType.Body:
case Label.LabelType.Body_semibold:
case Label.LabelType.Body_bold:
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return Style.body_line_height
case Label.LabelType.Caption:
case Label.LabelType.Caption_semibold:
case Label.LabelType.Caption_bold:
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return Style.caption_line_height
}
}
font.letterSpacing: {
switch (root.type) {
case Label.LabelType.Heading:
case Label.LabelType.Title:
case Label.LabelType.Lead:
case Proton.Label.LabelType.Heading:
case Proton.Label.LabelType.Title:
case Proton.Label.LabelType.Lead:
return 0
case Label.LabelType.Body:
case Label.LabelType.Body_semibold:
case Label.LabelType.Body_bold:
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return Style.body_letter_spacing
case Label.LabelType.Caption:
case Label.LabelType.Caption_semibold:
case Label.LabelType.Caption_bold:
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return Style.caption_letter_spacing
}
}

View File

@ -20,7 +20,8 @@ import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
import "."
import "." as Proton
Item {
id: root
@ -131,7 +132,7 @@ Item {
border.width: 1
}
Label {
Proton.Label {
colorScheme: root.colorScheme
id: label
@ -141,10 +142,10 @@ Item {
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
type: Label.LabelType.Body_semibold
type: Proton.Label.LabelType.Body_semibold
}
Label {
Proton.Label {
colorScheme: root.colorScheme
id: hint
@ -154,7 +155,7 @@ Item {
color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled
type: Label.LabelType.Caption
type: Proton.Label.LabelType.Caption
}
ColorImage {
@ -168,7 +169,7 @@ Item {
color: root.colorScheme.signal_danger
}
Label {
Proton.Label {
colorScheme: root.colorScheme
id: assistiveText
@ -189,7 +190,7 @@ Item {
return root.colorScheme.text_weak
}
type: root.error ? Label.LabelType.Caption_semibold : Label.LabelType.Caption
type: root.error ? Proton.Label.LabelType.Caption_semibold : Proton.Label.LabelType.Caption
}
ScrollView {

View File

@ -21,7 +21,8 @@ import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T
import QtQuick.Layouts 1.12
import "."
import "." as Proton
Item {
id: root
@ -128,22 +129,22 @@ Item {
Layout.fillWidth: true
spacing: 0
Label {
Proton.Label {
colorScheme: root.colorScheme
id: label
Layout.fillHeight: true
Layout.fillWidth: true
type: Label.LabelType.Body_semibold
type: Proton.Label.LabelType.Body_semibold
}
Label {
Proton.Label {
colorScheme: root.colorScheme
id: hint
Layout.fillHeight: true
Layout.fillWidth: true
color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled
horizontalAlignment: Text.AlignRight
type: Label.LabelType.Caption
type: Proton.Label.LabelType.Caption
}
}
@ -270,7 +271,7 @@ Item {
}
}
Button {
Proton.Button {
colorScheme: root.colorScheme
id: eyeButton
@ -299,7 +300,7 @@ Item {
sourceSize.height: assistiveText.height
}
Label {
Proton.Label {
colorScheme: root.colorScheme
id: assistiveText
@ -319,7 +320,7 @@ Item {
return root.colorScheme.text_weak
}
type: root.error ? Label.LabelType.Caption_semibold : Label.LabelType.Caption
type: root.error ? Proton.Label.LabelType.Caption_semibold : Proton.Label.LabelType.Caption
}
}
}

View File

@ -20,16 +20,20 @@ import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
RowLayout{
Item {
id: root
property var colorScheme
property bool checked
property bool disabled
property bool hovered
property bool loading
signal clicked
property bool _disabled: !enabled
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
Rectangle {
id: indicator
implicitWidth: 40
@ -38,12 +42,12 @@ RowLayout{
radius: 20
color: {
if (root.loading) return "transparent"
if (root.disabled) return root.colorScheme.background_strong
if (root._disabled) return root.colorScheme.background_strong
return root.colorScheme.background_norm
}
border {
width: 1
color: (root.disabled || root.loading) ? "transparent" : colorScheme.field_norm
color: (root._disabled || root.loading) ? "transparent" : colorScheme.field_norm
}
Rectangle {
@ -55,7 +59,7 @@ RowLayout{
radius: 12
color: {
if (root.loading) return "transparent"
if (root.disabled) return root.colorScheme.field_disabled
if (root._disabled) return root.colorScheme.field_disabled
if (root.checked) {
if (root.hovered) return root.colorScheme.interaction_norm_hover
@ -101,7 +105,7 @@ RowLayout{
hoverEnabled: true
onEntered: {root.hovered = true }
onExited: {root.hovered = false }
onClicked: { root.clicked();}
onClicked: { if (root.enabled) root.clicked();}
onPressed: {root.hovered = true }
onReleased: { root.hovered = containsMouse }
}

View File

@ -24,6 +24,7 @@ ColorScheme 4.0 ColorScheme.qml
ApplicationWindow 4.0 ApplicationWindow.qml
Button 4.0 Button.qml
CheckBox 4.0 CheckBox.qml
ComboBox 4.0 ComboBox.qml
Dialog 4.0 Dialog.qml
Label 4.0 Label.qml
Menu 4.0 Menu.qml

View File

@ -21,7 +21,7 @@ import QtQuick.Controls 2.12
import Proton 4.0
ColumnLayout {
Item {
id: root
property var colorScheme
@ -32,36 +32,45 @@ ColumnLayout {
property var type: SettingsItem.ActionType.Toggle
property bool checked: true
property bool disabled: false
property bool loading: false
property bool showSeparator: true
property var _bottomMargin: 20
property var _lineWidth: 1
property var _toggleTopMargin: 6
signal clicked
spacing: 20
Layout.fillWidth: true
Layout.maximumWidth: root.parent.Layout.maximumWidth
enum ActionType {
Toggle = 1, Button = 2, PrimaryButton = 3
}
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
RowLayout {
Layout.fillWidth: true
anchors.fill: parent
spacing: 16
ColumnLayout {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.bottomMargin: root._bottomMargin
spacing: 4
Label {
id:mainLabel
id: mainLabel
colorScheme: root.colorScheme
text: root.text
type: Label.Body_semibold
}
Label {
Layout.minimumWidth: mainLabel.width
Layout.maximumWidth: root.Layout.maximumWidth - root.spacing - (
toggle.visible ? toggle.width : button.width
)
Layout.fillHeight: true
Layout.fillWidth: true
Layout.preferredWidth: parent.width
wrapMode: Text.WordWrap
colorScheme: root.colorScheme
@ -70,15 +79,12 @@ ColumnLayout {
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
Toggle {
Layout.alignment: Qt.AlignTop
Layout.topMargin: root._toggleTopMargin
id: toggle
colorScheme: root.colorScheme
visible: root.type == SettingsItem.ActionType.Toggle
visible: root.type === SettingsItem.ActionType.Toggle
checked: root.checked
loading: root.loading
@ -86,20 +92,25 @@ ColumnLayout {
}
Button {
Layout.alignment: Qt.AlignTop
id: button
colorScheme: root.colorScheme
visible: root.type == SettingsItem.Button || root.type == SettingsItem.PrimaryButton
visible: root.type === SettingsItem.Button || root.type === SettingsItem.PrimaryButton
text: root.actionText + (root.actionIcon != "" ? " " : "")
loading: root.loading
icon.source: root.actionIcon
onClicked: { if (!root.loading) root.clicked() }
secondary: root.type != SettingsItem.PrimaryButton
secondary: root.type !== SettingsItem.PrimaryButton
}
}
Rectangle {
Layout.fillWidth: true
anchors.left: root.left
anchors.right: root.right
anchors.bottom: root.bottom
color: colorScheme.border_weak
height: 1
height: root._lineWidth
visible: root.showSeparator
}
}

View File

@ -22,7 +22,7 @@ import QtQuick.Controls.impl 2.13
import Proton 4.0
ScrollView {
Item {
id: root
property var colorScheme
@ -31,36 +31,45 @@ ScrollView {
signal back()
property int _leftRightMargins: 64
property int _topBottomMargins: 68
property int _spacing: 22
property int _leftMargin: 64
property int _rightMargin: 64
property int _topMargin: 32
property int _bottomMargin: 32
property int _spacing: 20
clip: true
contentWidth: pane.width
contentHeight: pane.height
RowLayout{
id: pane
width: root.width
ScrollView {
clip: true
width:root.width
height:root.height
contentWidth: content.width
contentHeight: content.height
ColumnLayout {
id: content
spacing: root._spacing
Layout.maximumWidth: root.width - 2*root._leftRightMargins
Layout.fillWidth: true
Layout.topMargin: root._topBottomMargins
Layout.bottomMargin: root._topBottomMargins
Layout.leftMargin: root._leftRightMargins
Layout.rightMargin: root._leftRightMargins
width: root.width - (root._leftMargin + root._rightMargin)
anchors{
top: parent.top
left: parent.left
topMargin: root._topMargin
bottomMargin: root._bottomMargin
leftMargin: root._leftMargin
rightMargin: root._rightMargin
}
}
}
Button {
id: backButton
anchors {
top: parent.top
left: parent.left
topMargin: 10
leftMargin: 18
topMargin: root._topMargin
leftMargin: (root._leftMargin-backButton.width) / 2
}
colorScheme: root.colorScheme
onClicked: root.back()

View File

@ -28,7 +28,6 @@ Item {
property ColorScheme colorScheme
property var backend
property var user
property string address
@ -124,7 +123,9 @@ Item {
console.log(" TODO configure ", model.name)
return
}
root.user.configureAppleMail(root.address)
if (user) {
root.user.configureAppleMail(root.address)
}
root.dismissed()
}
}
@ -139,7 +140,9 @@ Item {
flat: true
onClicked: {
user.setupGuideSeen = true
if (user) {
user.setupGuideSeen = true
}
root.dismissed()
}
}

View File

@ -27,31 +27,27 @@ Item {
id: root
property ColorScheme colorScheme
function abort() {
root.loginAbort(usernameTextField.text)
function reset() {
stackLayout.currentIndex = 0
loginNormalLayout.reset()
login2FALayout.reset()
login2PasswordLayout.reset()
}
signal login(string username, string password)
signal login2FA(string username, string code)
signal login2Password(string username, string password)
signal loginAbort(string username)
function abort() {
root.reset()
root.backend.loginAbort(usernameTextField.text)
}
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
property var backend
property var window
property alias username: usernameTextField.text
state: "Page 1"
onLoginAbort: {
stackLayout.currentIndex = 0
loginNormalLayout.reset()
login2FALayout.reset()
login2PasswordLayout.reset()
}
property alias currentIndex: stackLayout.currentIndex
StackLayout {
@ -83,18 +79,16 @@ Item {
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: {
@ -108,19 +102,12 @@ Item {
}
onLogin2FAErrorAbort: {
console.assert(stackLayout.currentIndex == 1, "Unexpected login2FAErrorAbort")
stackLayout.currentIndex = 0
loginNormalLayout.reset()
login2FALayout.reset()
login2PasswordLayout.reset()
root.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: {
@ -134,22 +121,13 @@ Item {
}
onLogin2PasswordErrorAbort: {
console.assert(stackLayout.currentIndex == 2, "Unexpected login2PasswordErrorAbort")
stackLayout.currentIndex = 0
loginNormalLayout.reset()
login2FALayout.reset()
login2PasswordLayout.reset()
root.reset()
errorLabel.text = qsTr("Incorrect login credentials. Please try again.")
passwordTextField.text = ""
}
onLoginFinished: {
stackLayout.currentIndex = 0
loginNormalLayout.reset()
passwordTextField.text = ""
login2FALayout.reset()
login2PasswordLayout.reset()
root.reset()
}
}
@ -168,6 +146,7 @@ Item {
passwordTextField.enabled = true
passwordTextField.error = false
passwordTextField.assistiveText = ""
passwordTextField.text = ""
}
spacing: 0
@ -303,7 +282,7 @@ Item {
enabled = false
loading = true
root.login(usernameTextField.text, Qt.btoa(passwordTextField.text))
root.backend.login(usernameTextField.text, Qt.btoa(passwordTextField.text))
}
}
@ -331,6 +310,7 @@ Item {
twoFactorPasswordTextField.enabled = true
twoFactorPasswordTextField.error = false
twoFactorPasswordTextField.assistiveText = ""
twoFactorPasswordTextField.text=""
}
spacing: 0
@ -388,7 +368,7 @@ Item {
enabled = false
loading = true
root.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text))
root.backend.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text))
}
}
}
@ -402,6 +382,7 @@ Item {
secondPasswordTextField.enabled = true
secondPasswordTextField.error = false
secondPasswordTextField.assistiveText = ""
secondPasswordTextField.text = ""
}
spacing: 0
@ -460,7 +441,7 @@ Item {
enabled = false
loading = true
root.login2Password(usernameTextField.text, Qt.btoa(secondPasswordTextField.text))
root.backend.login2Password(usernameTextField.text, Qt.btoa(secondPasswordTextField.text))
}
}
}

View File

@ -42,7 +42,7 @@ Item {
NotificationFilter {
id: notificationFilter
source: root.notifications.all
source: root.notifications ? root.notifications.all : undefined
whitelist: root.notificationWhitelist
blacklist: root.notificationBlacklist
@ -59,19 +59,19 @@ Item {
label.text = topmost.text
switch (topmost.type) {
case Notification.NotificationType.Danger:
case Notification.NotificationType.Danger:
image.color = root.colorScheme.signal_danger
label.color = root.colorScheme.signal_danger
break;
case Notification.NotificationType.Warning:
case Notification.NotificationType.Warning:
image.color = root.colorScheme.signal_warning
label.color = root.colorScheme.signal_warning
break;
case Notification.NotificationType.Success:
case Notification.NotificationType.Success:
image.color = root.colorScheme.signal_success
label.color = root.colorScheme.signal_success
break;
case Notification.NotificationType.Info:
case Notification.NotificationType.Info:
image.color = root.colorScheme.signal_info
label.color = root.colorScheme.signal_info
break;

View File

@ -198,7 +198,7 @@ Window {
Button {
Layout.margins: 12
colorScheme: root.colorScheme
visible: !viewItem.user.loggedIn
visible: viewItem.user ? !viewItem.user.loggedIn : false
text: qsTr("Sign in")
onClicked: {
root.showSignIn(viewItem.username)

View File

@ -28,12 +28,6 @@ Item {
property ColorScheme 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)
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
@ -230,22 +224,14 @@ Item {
Layout.preferredWidth: 320
Layout.fillWidth: true
onLogin: {
root.login(username, password)
username: {
if (root.backend.users.count !== 1) return ""
var user = root.backend.users.get(0)
if (user) return ""
if (user.loggedIn) return ""
return user.username
}
onLogin2FA: {
root.login2FA(username, code)
}
onLogin2Password: {
root.login2Password(username, password)
}
onLoginAbort: {
root.loginAbort(username)
}
username: (backend.users.count === 1 && backend.users.get(0).loggedIn === false) ? backend.users.get(0).username : ""
backend: root.backend
window: root.window
}
// Right margin

View File

@ -20,9 +20,10 @@ import QtQuick.Window 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
import "../Proton"
RowLayout {
id: root
property ColorScheme colorScheme
// Primary buttons

View File

@ -18,7 +18,8 @@
import QtQuick.Layouts 1.12
import QtQuick 2.12
import QtQuick.Controls 2.12
import Proton 4.0
import "../Proton"
ColumnLayout {
id: root

View File

@ -20,7 +20,7 @@ import QtQuick.Window 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
import "../Proton"
RowLayout {
id: root
@ -33,29 +33,35 @@ RowLayout {
CheckBox {
text: "Checkbox"
colorScheme: root.colorScheme
}
CheckBox {
text: "Checkbox"
error: true
colorScheme: root.colorScheme
}
CheckBox {
text: "Checkbox"
enabled: false
colorScheme: root.colorScheme
}
CheckBox {
text: ""
colorScheme: root.colorScheme
}
CheckBox {
text: ""
error: true
colorScheme: root.colorScheme
}
CheckBox {
text: ""
enabled: false
colorScheme: root.colorScheme
}
}
@ -67,34 +73,40 @@ RowLayout {
CheckBox {
text: "Checkbox"
checked: true
colorScheme: root.colorScheme
}
CheckBox {
text: "Checkbox"
checked: true
error: true
colorScheme: root.colorScheme
}
CheckBox {
text: "Checkbox"
checked: true
enabled: false
colorScheme: root.colorScheme
}
CheckBox {
text: ""
checked: true
colorScheme: root.colorScheme
}
CheckBox {
text: ""
checked: true
error: true
colorScheme: root.colorScheme
}
CheckBox {
text: ""
checked: true
enabled: false
colorScheme: root.colorScheme
}
}
}

View File

@ -0,0 +1,100 @@
// 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 <https://www.gnu.org/licenses/>.
import QtQuick 2.13
import QtQuick.Window 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import "../Proton"
RowLayout {
id: root
property ColorScheme colorScheme
ColumnLayout {
Layout.fillWidth: true
ComboBox {
Layout.fillWidth: true
model: ["First", "Second", "Third"]
colorScheme: root.colorScheme
}
ComboBox {
Layout.fillWidth: true
model: ["First", "Second", "Third"]
editable: true
colorScheme: root.colorScheme
}
}
ColumnLayout {
Layout.fillWidth: true
ComboBox {
Layout.fillWidth: true
model: ["First", "Second", "Third"]
colorScheme: root.colorScheme
enabled: false
}
ComboBox {
Layout.fillWidth: true
model: ["First", "Second", "Third"]
editable: true
colorScheme: root.colorScheme
enabled: false
}
}
ColumnLayout {
Layout.fillWidth: true
ComboBox {
Layout.fillWidth: true
model: ["First", "Second", "Third"]
colorScheme: root.colorScheme
LayoutMirroring.enabled: true
}
ComboBox {
Layout.fillWidth: true
model: ["First", "Second", "Third"]
editable: true
colorScheme: root.colorScheme
LayoutMirroring.enabled: true
}
}
ColumnLayout {
Layout.fillWidth: true
ComboBox {
Layout.fillWidth: true
model: ["First", "Second", "Third"]
colorScheme: root.colorScheme
enabled: false
LayoutMirroring.enabled: true
}
ComboBox {
Layout.fillWidth: true
model: ["First", "Second", "Third"]
editable: true
colorScheme: root.colorScheme
enabled: false
LayoutMirroring.enabled: true
}
}
}

View File

@ -20,9 +20,10 @@ import QtQuick.Window 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
import "../Proton"
RowLayout {
id: root
property ColorScheme colorScheme
ColumnLayout {

View File

@ -20,9 +20,10 @@ import QtQuick.Window 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
import "../Proton"
RowLayout {
id: root
property ColorScheme colorScheme
ColumnLayout {
@ -32,30 +33,36 @@ RowLayout {
Switch {
text: "Toggle"
colorScheme: root.colorScheme
}
Switch {
text: "Toggle"
enabled: false
colorScheme: root.colorScheme
}
Switch {
text: "Toggle"
loading: true
colorScheme: root.colorScheme
}
Switch {
text: ""
colorScheme: root.colorScheme
}
Switch {
text: ""
enabled: false
colorScheme: root.colorScheme
}
Switch {
text: ""
loading: true
colorScheme: root.colorScheme
}
}
@ -67,35 +74,41 @@ RowLayout {
Switch {
text: "Toggle"
checked: true
colorScheme: root.colorScheme
}
Switch {
text: "Toggle"
checked: true
enabled: false
colorScheme: root.colorScheme
}
Switch {
text: "Toggle"
checked: true
loading: true
colorScheme: root.colorScheme
}
Switch {
text: ""
checked: true
colorScheme: root.colorScheme
}
Switch {
text: ""
checked: true
enabled: false
colorScheme: root.colorScheme
}
Switch {
text: ""
checked: true
loading: true
colorScheme: root.colorScheme
}
}
}

View File

@ -0,0 +1,30 @@
// 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 <https://www.gnu.org/licenses/>.
import QtQuick.Window 2.13
import "../Proton"
Window {
width: 800
height: 600
visible: true
TestComponents {
anchors.fill: parent
colorScheme: ProtonStyle.currentStyle
}
}

View File

@ -19,52 +19,68 @@ import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
import "../Proton"
Rectangle {
id: root
property ColorScheme colorScheme
color: colorScheme.background_norm
clip: true
ColumnLayout {
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
ScrollView {
anchors.fill: parent
spacing: 5
ColumnLayout {
anchors.margins: 20
Buttons {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
width: root.width
TextFields {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
spacing: 5
TextAreas {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
Buttons {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
CheckBoxes {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
CheckBoxes {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
RadioButtons {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
ComboBoxes {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
Switches {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
RadioButtons {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
Switches {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
TextAreas {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
TextFields {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
}
}
}

View File

@ -20,7 +20,7 @@ import QtQuick.Window 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
import "../Proton"
RowLayout {
id: root

View File

@ -20,9 +20,10 @@ import QtQuick.Window 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
import "../Proton"
RowLayout {
id: root
property ColorScheme colorScheme
// Norm
@ -148,6 +149,23 @@ RowLayout {
placeholderText: "Placeholder"
label: "Label"
hint: "Hint"
assistiveText: "Assistive text"
}
TextField {
colorScheme: root.colorScheme
Layout.fillWidth: true
placeholderText: "Placeholder"
label: "Label"
}
TextField {
colorScheme: root.colorScheme
Layout.fillWidth: true
placeholderText: "Placeholder"
hint: "Hint"
}
TextField {
@ -157,12 +175,5 @@ RowLayout {
placeholderText: "Placeholder"
assistiveText: "Assistive text"
}
TextField {
colorScheme: root.colorScheme
Layout.fillWidth: true
placeholderText: "Placeholder"
}
}
}

View File

@ -60,7 +60,6 @@ type FrontendQt struct {
newVersionInfo updater.VersionInfo
log *logrus.Entry
usersMtx sync.Mutex
initializing sync.WaitGroup
initializationDone sync.Once

View File

@ -43,6 +43,11 @@ func (f *FrontendQt) watchEvents() {
userChangedCh := f.eventListener.ProvideChannel(events.UserRefreshEvent)
certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
// This loop is executed outside main Qt application thread. In order
// to make sure that all signals are propagated correctly to QML we
// must call QMLBackend signals to apply any changes to GUI. The
// signals will make sure the changes are executed in main Qt app
// thread.
for {
select {
case errorDetails := <-errorCh:
@ -77,7 +82,7 @@ func (f *FrontendQt) watchEvents() {
case <-updateApplicationCh:
f.updateForce()
case userID := <-userChangedCh:
f.userChanged(userID)
f.qml.UserChanged(userID)
case <-certIssue:
f.qml.ApiCertIssue()
}

View File

@ -48,7 +48,7 @@ func (f *FrontendQt) initiateQtApplication() error {
// QML Engine and path
f.engine = qml.NewQQmlApplicationEngine(f.app)
f.qml = NewQMLBackend(nil)
f.qml = NewQMLBackend(f.engine)
f.qml.setup(f)
f.engine.RootContext().SetContextProperty("go", f.qml)

View File

@ -40,7 +40,7 @@ func (f *FrontendQt) checkUpdates() error {
func (f *FrontendQt) checkUpdatesAndNotify(isRequestFromUser bool) {
checkingUpdates.Lock()
defer checkingUpdates.Lock()
defer checkingUpdates.Unlock()
defer f.qml.CheckUpdatesFinished()
if err := f.checkUpdates(); err != nil {
@ -68,7 +68,7 @@ func (f *FrontendQt) checkUpdatesAndNotify(isRequestFromUser bool) {
func (f *FrontendQt) updateForce() {
checkingUpdates.Lock()
defer checkingUpdates.Lock()
defer checkingUpdates.Unlock()
version := ""
if err := f.checkUpdates(); err == nil {

View File

@ -22,79 +22,10 @@ package qt
import (
"context"
"encoding/base64"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
func (f *FrontendQt) loadUsers() {
f.usersMtx.Lock()
defer f.usersMtx.Unlock()
f.qml.Users().clear()
for _, user := range f.bridge.GetUsers() {
f.qml.Users().addUser(newQMLUserFromBacked(f, user))
}
// If there are no active accounts.
if f.qml.Users().Count() == 0 {
f.log.Info("No active accounts")
}
}
func (f *FrontendQt) userChanged(userID string) {
f.usersMtx.Lock()
defer f.usersMtx.Unlock()
fUsers := f.qml.Users()
index := fUsers.indexByID(userID)
user, err := f.bridge.GetUser(userID)
if user == nil || err != nil {
if index >= 0 { // delete existing user
fUsers.removeUser(index)
}
return
}
if index < 0 { // add non-existing user
fUsers.addUser(newQMLUserFromBacked(f, user))
return
}
// update exiting user
fUsers.users[index].update(user)
}
func newQMLUserFromBacked(f *FrontendQt, user types.User) *QMLUser {
qu := NewQMLUser(nil)
qu.ID = user.ID()
qu.update(user)
qu.ConnectToggleSplitMode(func(activateSplitMode bool) {
go func() {
defer qu.ToggleSplitModeFinished()
if activateSplitMode == user.IsCombinedAddressMode() {
user.SwitchAddressMode()
}
qu.SetSplitMode(!user.IsCombinedAddressMode())
}()
})
qu.ConnectLogout(func() {
qu.SetLoggedIn(false)
go user.Logout()
})
qu.ConnectConfigureAppleMail(func(address string) {
go f.configureAppleMail(qu.ID, address)
})
return qu
}
func (f *FrontendQt) login(username, password string) {
var err error
f.password, err = base64.StdEncoding.DecodeString(password)
@ -107,6 +38,7 @@ func (f *FrontendQt) login(username, password string) {
f.authClient, f.auth, err = f.bridge.Login(username, f.password)
if err != nil {
// TODO login free user error
f.qml.LoginUsernamePasswordError(err.Error())
f.loginClean()
return
@ -185,29 +117,24 @@ func (f *FrontendQt) login2Password(username, mboxPassword string) {
func (f *FrontendQt) finishLogin() {
defer f.loginClean()
if f.auth == nil || f.authClient == nil {
f.log.Errorf("Finish login: Authethication incomplete %p %p", f.auth, f.authClient)
if len(f.password) == 0 || f.auth == nil || f.authClient == nil {
f.log.
WithField("hasPass", len(f.password) != 0).
WithField("hasAuth", f.auth != nil).
WithField("hasClient", f.authClient != nil).
Error("Finish login: authethication incomplete")
f.qml.Login2PasswordErrorAbort("Missing authentication, try again.")
return
}
user, err := f.bridge.FinishLogin(f.authClient, f.auth, f.password)
if err != nil {
f.log.Errorf("Authethication incomplete %p %p", f.auth, f.authClient)
f.qml.Login2PasswordErrorAbort("Missing authentication, try again.")
_, err := f.bridge.FinishLogin(f.authClient, f.auth, f.password)
if err != nil && err != users.ErrUserAlreadyConnected {
f.log.WithError(err).Errorf("Finish login failed")
f.qml.Login2PasswordErrorAbort(err.Error())
return
}
index := f.qml.Users().indexByID(user.ID())
if index < 0 {
qu := newQMLUserFromBacked(f, user)
qu.SetSetupGuideSeen(false)
f.qml.Users().addUser(qu)
return
}
f.qml.Users().users[index].update(user)
f.qml.LoginFinished()
defer f.qml.LoginFinished()
}
func (f *FrontendQt) loginAbort(username string) {

View File

@ -28,6 +28,10 @@ import (
"github.com/therecipe/qt/core"
)
func init() {
QMLBackend_QRegisterMetaType()
}
// QMLBackend connects QML frontend with Go backend.
type QMLBackend struct {
core.QObject
@ -138,6 +142,8 @@ type QMLBackend struct {
_ func(address string) `signal:addressChangedLogout`
_ func(username string) `signal:userDisconnected`
_ func() `signal:apiCertIssue`
_ func(userID string) `signal:userChanged`
}
func (q *QMLBackend) setup(f *FrontendQt) {
@ -150,38 +156,81 @@ func (q *QMLBackend) setup(f *FrontendQt) {
return f.showOnStartup
})
q.ConnectIsDockIconVisible(func() bool {
return dockIcon.GetDockIconVisibleState()
})
q.ConnectSetDockIconVisible(func(visible bool) {
dockIcon.SetDockIconVisibleState(visible)
})
q.ConnectIsDockIconVisible(dockIcon.GetDockIconVisibleState)
q.ConnectSetDockIconVisible(dockIcon.SetDockIconVisibleState)
q.SetUsers(NewQMLUserModel(nil))
f.loadUsers()
um := NewQMLUserModel(q)
um.f = f
q.SetUsers(um)
um.load()
q.ConnectUserChanged(um.userChanged)
q.SetGoos(runtime.GOOS)
q.ConnectLogin(func(u, p string) { go f.login(u, p) })
q.ConnectLogin2FA(func(u, p string) { go f.login2FA(u, p) })
q.ConnectLogin2Password(func(u, p string) { go f.login2Password(u, p) })
q.ConnectLoginAbort(func(u string) { go f.loginAbort(u) })
q.ConnectLogin(func(u, p string) {
go func() {
defer f.panicHandler.HandlePanic()
f.login(u, p)
}()
})
q.ConnectLogin2FA(func(u, p string) {
go func() {
defer f.panicHandler.HandlePanic()
f.login2FA(u, p)
}()
})
q.ConnectLogin2Password(func(u, p string) {
go func() {
defer f.panicHandler.HandlePanic()
f.login2Password(u, p)
}()
})
q.ConnectLoginAbort(func(u string) {
go func() {
defer f.panicHandler.HandlePanic()
f.loginAbort(u)
}()
})
go f.checkUpdatesAndNotify(false)
q.ConnectCheckUpdates(func() { go f.checkUpdatesAndNotify(true) })
go func() {
defer f.panicHandler.HandlePanic()
f.checkUpdatesAndNotify(false)
}()
q.ConnectCheckUpdates(func() {
go func() {
defer f.panicHandler.HandlePanic()
f.checkUpdatesAndNotify(true)
}()
})
f.setIsDiskCacheEnabled()
f.setDiskCachePath()
q.ConnectChangeLocalCache(func(e bool, d string) { go f.changeLocalCache(e, d) })
q.ConnectChangeLocalCache(func(e bool, d string) {
go func() {
defer f.panicHandler.HandlePanic()
f.changeLocalCache(e, d)
}()
})
f.setIsAutomaticUpdateOn()
q.ConnectToggleAutomaticUpdate(func(m bool) { go f.toggleAutomaticUpdate(m) })
q.ConnectToggleAutomaticUpdate(func(m bool) {
go func() {
defer f.panicHandler.HandlePanic()
f.toggleAutomaticUpdate(m)
}()
})
f.setIsAutostartOn()
q.ConnectToggleAutostart(f.toggleAutostart)
f.setIsBetaEnabled()
q.ConnectToggleBeta(func(m bool) { go f.toggleBeta(m) })
q.ConnectToggleBeta(func(m bool) {
go func() {
defer f.panicHandler.HandlePanic()
f.toggleBeta(m)
}()
})
q.SetIsDoHEnabled(f.settings.GetBool(settings.AllowProxyKey))
q.ConnectToggleDoH(f.toggleDoH)
@ -195,7 +244,12 @@ func (q *QMLBackend) setup(f *FrontendQt) {
q.ConnectChangePorts(f.changePorts)
q.ConnectIsPortFree(f.isPortFree)
q.ConnectTriggerReset(func() { go f.triggerReset() })
q.ConnectTriggerReset(func() {
go func() {
defer f.panicHandler.HandlePanic()
f.triggerReset()
}()
})
f.setVersion()
f.setLogsPath()
@ -203,9 +257,24 @@ func (q *QMLBackend) setup(f *FrontendQt) {
f.setLicensePath()
f.setCurrentEmailClient()
q.ConnectUpdateCurrentMailClient(func() { go f.setCurrentEmailClient() })
q.ConnectReportBug(func(d, a, e string, i bool) { go f.reportBug(d, a, e, i) })
q.ConnectUpdateCurrentMailClient(func() {
go func() {
defer f.panicHandler.HandlePanic()
f.setCurrentEmailClient()
}()
})
q.ConnectReportBug(func(d, a, e string, i bool) {
go func() {
defer f.panicHandler.HandlePanic()
f.reportBug(d, a, e, i)
}()
})
f.setKeychain()
q.ConnectSelectKeychain(func(k string) { go f.selectKeychain(k) })
q.ConnectSelectKeychain(func(k string) {
go func() {
defer f.panicHandler.HandlePanic()
f.selectKeychain(k)
}()
})
}

View File

@ -20,10 +20,17 @@
package qt
import (
"sync"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/therecipe/qt/core"
)
func init() {
QMLUser_QRegisterMetaType()
QMLUserModel_QRegisterMetaType()
}
// QMLUserModel stores list of of users
type QMLUserModel struct {
core.QAbstractListModel
@ -33,72 +40,168 @@ type QMLUserModel struct {
_ func() `constructor:"init"`
_ func(row int) *core.QVariant `slot:"get"`
users []*QMLUser
userIDs []string
userByID map[string]*QMLUser
access sync.RWMutex
f *FrontendQt
}
func (um *QMLUserModel) init() {
um.SetRoles(map[int]*core.QByteArray{
int(core.Qt__UserRole + 1): newQByteArrayFromString("object"),
})
um.access.Lock()
defer um.access.Unlock()
um.SetCount(0)
um.ConnectRowCount(um.rowCount)
um.ConnectData(um.data)
um.ConnectGet(um.get)
um.users = []*QMLUser{}
um.setCount()
um.ConnectCount(func() int {
um.access.RLock()
defer um.access.RUnlock()
return len(um.userIDs)
})
um.userIDs = []string{}
um.userByID = map[string]*QMLUser{}
}
func (um *QMLUserModel) data(index *core.QModelIndex, property int) *core.QVariant {
if !index.IsValid() {
um.f.log.WithField("size", len(um.userIDs)).Info("Trying to get user by invalid index")
return core.NewQVariant()
}
return um.get(index.Row())
}
func (um *QMLUserModel) get(index int) *core.QVariant {
if index < 0 || index >= um.rowCount(nil) {
um.access.Lock()
defer um.access.Unlock()
if index < 0 || index >= len(um.userIDs) {
um.f.log.WithField("index", index).WithField("size", len(um.userIDs)).Info("Trying to get user by wrong index")
return core.NewQVariant()
}
return um.users[index].ToVariant()
u, err := um.getUserByID(um.userIDs[index])
if err != nil {
um.f.log.WithError(err).Error("Cannot get user from backend")
return core.NewQVariant()
}
return u.ToVariant()
}
func (um *QMLUserModel) getUserByID(userID string) (*QMLUser, error) {
u, ok := um.userByID[userID]
if ok {
return u, nil
}
user, err := um.f.bridge.GetUser(userID)
if err != nil {
return nil, err
}
u = newQMLUserFromBacked(um, user)
um.userByID[userID] = u
return u, nil
}
func (um *QMLUserModel) rowCount(*core.QModelIndex) int {
return len(um.users)
um.access.RLock()
defer um.access.RUnlock()
return len(um.userIDs)
}
func (um *QMLUserModel) setCount() {
um.SetCount(len(um.users))
um.SetCount(len(um.userIDs))
}
func (um *QMLUserModel) addUser(user *QMLUser) {
um.BeginInsertRows(core.NewQModelIndex(), um.rowCount(nil), um.rowCount(nil))
um.users = append(um.users, user)
um.setCount()
func (um *QMLUserModel) addUser(userID string) {
um.BeginInsertRows(core.NewQModelIndex(), len(um.userIDs), len(um.userIDs))
um.access.Lock()
if um.indexByIDNotSafe(userID) < 0 {
um.userIDs = append(um.userIDs, userID)
}
um.access.Unlock()
um.EndInsertRows()
um.setCount()
}
func (um *QMLUserModel) removeUser(row int) {
um.BeginRemoveRows(core.NewQModelIndex(), row, row)
um.users = append(um.users[:row], um.users[row+1:]...)
um.setCount()
um.access.Lock()
id := um.userIDs[row]
um.userIDs = append(um.userIDs[:row], um.userIDs[row+1:]...)
delete(um.userByID, id)
um.access.Unlock()
um.EndRemoveRows()
um.setCount()
}
func (um *QMLUserModel) clear() {
um.BeginRemoveRows(core.NewQModelIndex(), 0, um.rowCount(nil))
um.users = []*QMLUser{}
um.setCount()
um.EndRemoveRows()
um.BeginResetModel()
um.access.Lock()
um.userIDs = []string{}
um.userByID = map[string]*QMLUser{}
um.SetCount(0)
um.access.Unlock()
um.EndResetModel()
}
func (um *QMLUserModel) indexByID(id string) int {
for i, qu := range um.users {
if id == qu.ID {
func (um *QMLUserModel) load() {
um.clear()
for _, user := range um.f.bridge.GetUsers() {
um.addUser(user.ID())
// We need mark that all existing users already saw setup
// guide. This it is OK to construct QML here because it is in main thread.
u, err := um.getUserByID(user.ID())
if err != nil {
um.f.log.WithError(err).Error("Cannot get QMLUser while loading users")
}
u.SetSetupGuideSeen(true)
}
// If there are no active accounts.
if um.Count() == 0 {
um.f.log.Info("No active accounts")
}
}
func (um *QMLUserModel) userChanged(userID string) {
index := um.indexByIDNotSafe(userID)
user, err := um.f.bridge.GetUser(userID)
if user == nil || err != nil {
if index >= 0 { // delete existing user
um.removeUser(index)
}
// if not exiting do nothing
return
}
if index < 0 { // add non-existing user
um.addUser(userID)
return
}
// update exiting user
um.userByID[userID].update(user)
}
func (um *QMLUserModel) indexByIDNotSafe(wantID string) int {
for i, id := range um.userIDs {
if id == wantID {
return i
}
}
return -1
}
func (um *QMLUserModel) indexByID(id string) int {
um.access.RLock()
defer um.access.RUnlock()
return um.indexByIDNotSafe(id)
}
// QMLUser holds data, slots and signals and for user.
type QMLUser struct {
core.QObject
@ -116,18 +219,66 @@ type QMLUser struct {
_ func(makeItActive bool) `slot:"toggleSplitMode"`
_ func() `signal:"toggleSplitModeFinished"`
_ func() `slot:"logout"`
_ func() `slot:"remove"`
_ func(address string) `slot:"configureAppleMail"`
ID string
}
func newQMLUserFromBacked(um *QMLUserModel, user types.User) *QMLUser {
qu := NewQMLUser(um)
qu.ID = user.ID()
qu.update(user)
qu.ConnectToggleSplitMode(func(activateSplitMode bool) {
go func() {
defer um.f.panicHandler.HandlePanic()
defer qu.ToggleSplitModeFinished()
if activateSplitMode == user.IsCombinedAddressMode() {
user.SwitchAddressMode()
}
qu.SetSplitMode(!user.IsCombinedAddressMode())
}()
})
qu.ConnectLogout(func() {
qu.SetLoggedIn(false)
go func() {
defer um.f.panicHandler.HandlePanic()
user.Logout()
}()
})
qu.ConnectRemove(func() {
go func() {
defer um.f.panicHandler.HandlePanic()
// TODO: remove preferences
if err := um.f.bridge.DeleteUser(qu.ID, false); err != nil {
um.f.log.WithError(err).Error("Failed to remove user")
// TODO: notification
}
}()
})
qu.ConnectConfigureAppleMail(func(address string) {
go func() {
defer um.f.panicHandler.HandlePanic()
um.f.configureAppleMail(qu.ID, address)
}()
})
return qu
}
func (qu *QMLUser) update(user types.User) {
username := user.Username()
qu.SetAvatarText(getInitials(username))
qu.SetUsername(username)
qu.SetLoggedIn(user.IsConnected())
qu.SetSplitMode(!user.IsCombinedAddressMode())
qu.SetSetupGuideSeen(true)
qu.SetSetupGuideSeen(false)
qu.SetUsedBytes(1.0) // TODO
qu.SetTotalBytes(10000.0) // TODO
qu.SetPassword(user.GetBridgePassword())

View File

@ -40,8 +40,13 @@ var (
log = logrus.WithField("pkg", "users") //nolint[gochecknoglobals]
isApplicationOutdated = false //nolint[gochecknoglobals]
// ErrWrongMailboxPassword is returned when login password is OK but not the mailbox one.
// ErrWrongMailboxPassword is returned when login password is OK but
// not the mailbox one.
ErrWrongMailboxPassword = errors.New("wrong mailbox password")
// ErrUserAlreadyConnected is returned when authentication was OK but
// there is already active account for this user.
ErrUserAlreadyConnected = errors.New("user is already connected")
)
// Users is a struct handling users.
@ -212,7 +217,7 @@ func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password []by
logrus.WithError(err).Warn("Failed to delete new auth session")
}
return nil, errors.New("user is already connected")
return nil, ErrUserAlreadyConnected
}
// Update the user's credentials with the latest auth used to connect this user.