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. // Make sure the temporary file is deleted.
go (func() { go func() {
<-time.After(10 * time.Minute) <-time.After(10 * time.Minute)
_ = os.RemoveAll(dir) _ = os.RemoveAll(dir)
})() }()
// Make sure the file is only readable for the current user. // Make sure the file is only readable for the current user.
fname = filepath.Clean(filepath.Join(dir, "protonmail.mobileconfig")) fname = filepath.Clean(filepath.Join(dir, "protonmail.mobileconfig"))

View File

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

View File

@ -21,44 +21,51 @@ import QtQuick.Controls 2.12
import Proton 4.0 import Proton 4.0
ScrollView { Item {
id: root id: root
property ColorScheme colorScheme property ColorScheme colorScheme
property var backend property var backend
property var notifications property var notifications
property var user 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 showSignIn()
signal showSetupGuide(var user, string address) signal showSetupGuide(var user, string address)
ColumnLayout { property int _leftMargin: 64
id: pane property int _rightMargin: 64
property int _topMargin: 32
property int _detailsTopMargin: 25
property int _bottomMargin: 12
property int _spacing: 20
property int _lineWidth: 1
ScrollView {
clip: true
anchors.fill: parent
ColumnLayout {
width: root.width 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
Layout.fillWidth: true
ColumnLayout { ColumnLayout {
spacing: root._spacing spacing: root._spacing
Layout.topMargin: root._topBottomMargins
Layout.leftMargin: root._leftRightMargins anchors.fill: parent
Layout.rightMargin: root._leftRightMargins anchors.leftMargin: root._leftMargin
Layout.maximumWidth: root.width - 2*root._leftRightMargins anchors.rightMargin: root._rightMargin
anchors.topMargin: root._topMargin
anchors.bottomMargin: root._bottomMargin
RowLayout { // account delegate with action buttons RowLayout { // account delegate with action buttons
Layout.fillWidth: true Layout.fillWidth: true
@ -68,7 +75,7 @@ ScrollView {
colorScheme: root.colorScheme colorScheme: root.colorScheme
user: root.user user: root.user
type: AccountDelegate.LargeView type: AccountDelegate.LargeView
enabled: root.user.loggedIn enabled: root.user ? root.user.loggedIn : false
} }
Button { Button {
@ -76,8 +83,11 @@ ScrollView {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Sign out") text: qsTr("Sign out")
secondary: true secondary: true
visible: root.user.loggedIn visible: root.user ? root.user.loggedIn : false
onClicked: root.user.logout() onClicked: {
if (!root.user) return
root.user.logout()
}
} }
Button { Button {
@ -85,9 +95,11 @@ ScrollView {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Sign in") text: qsTr("Sign in")
secondary: true secondary: true
visible: !root.user.loggedIn visible: root.user ? !root.user.loggedIn : false
enabled: !root.user.loggedIn onClicked: {
onClicked: root.parent.rightContent.showSignIn() if (!root.user) return
root.parent.rightContent.showSignIn()
}
} }
Button { Button {
@ -95,15 +107,16 @@ ScrollView {
colorScheme: root.colorScheme colorScheme: root.colorScheme
icon.source: "icons/ic-trash.svg" icon.source: "icons/ic-trash.svg"
secondary: true secondary: true
visible: true onClicked: {
enabled: true if (!root.user) return
onClicked: root.user.remove() root.user.remove()
}
} }
} }
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
height: 1 height: root._lineWidth
color: root.colorScheme.border_weak color: root.colorScheme.border_weak
} }
@ -111,11 +124,17 @@ ScrollView {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Email clients") text: qsTr("Email clients")
actionText: qsTr("Configure") actionText: qsTr("Configure")
description: "MISSING WIREFRAME" // TODO 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 type: SettingsItem.Button
enabled: root.user.loggedIn enabled: root.user ? root.user.loggedIn : false
visible: !root.user.splitMode visible: root.user ? !root.user.splitMode || root.user.addresses.length==1 : false
onClicked: root.showSetupGuide(root.user,user.addresses[0]) showSeparator: splitMode.visible
onClicked: {
if (!root.user) return
root.showSetupGuide(root.user, user.addresses[0])
}
Layout.fillWidth: true
} }
SettingsItem { SettingsItem {
@ -124,9 +143,10 @@ ScrollView {
text: qsTr("Split addresses") 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.") 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 type: SettingsItem.Toggle
checked: root.user.splitMode checked: root.user ? root.user.splitMode : false
visible: root.user.addresses.length > 1 visible: root.user ? root.user.addresses.length > 1 : false
enabled: root.user.loggedIn enabled: root.user ? root.user.loggedIn : false
showSeparator: addressSelector.visible
onClicked: { onClicked: {
if (!splitMode.checked){ if (!splitMode.checked){
root.notifications.askEnableSplitMode(user) root.notifications.askEnableSplitMode(user)
@ -134,109 +154,57 @@ ScrollView {
root.user.toggleSplitMode(!splitMode.checked) root.user.toggleSplitMode(!splitMode.checked)
} }
} }
Layout.fillWidth: true
} }
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
enabled: root.user.loggedIn enabled: root.user ? root.user.loggedIn : false
visible: root.user ? root.user.splitMode : false
visible: root.user.splitMode
ComboBox { ComboBox {
id: addressSelector id: addressSelector
colorScheme: root.colorScheme
Layout.fillWidth: true Layout.fillWidth: true
model: root.user.addresses model: root.user ? root.user.addresses : null
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
colorScheme: root.colorScheme
text: addressSelector.displayText
elide: Text.ElideMiddle
}
} }
Button { Button {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Configure") text: qsTr("Configure")
secondary: true secondary: true
onClicked: root.showSetupGuide(root.user, addressSelector.displayText) onClicked: {
if (!root.user) return
root.showSetupGuide(root.user, addressSelector.displayText)
}
}
}
} }
} }
Item {implicitHeight: 1} Rectangle {
} color: root.colorScheme.background_weak
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
Layout.fillWidth: true
ColumnLayout { ColumnLayout {
id: configuration id: configuration
Layout.bottomMargin: root._topBottomMargins
Layout.leftMargin: root._leftRightMargins anchors.fill: parent
Layout.rightMargin: root._leftRightMargins anchors.leftMargin: root._leftMargin
Layout.maximumWidth: root.width - 2*root._leftRightMargins anchors.rightMargin: root._rightMargin
anchors.topMargin: root._detailsTopMargin
anchors.bottomMargin: root._spacing
spacing: root._spacing spacing: root._spacing
visible: root.user.loggedIn visible: root.user ? root.user.loggedIn : false
property string currentAddress: addressSelector.displayText property string currentAddress: addressSelector.displayText
Item {height: 1}
Label { Label {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Mailbox details") text: qsTr("Mailbox details")
@ -249,7 +217,7 @@ ScrollView {
hostname: root.backend.hostname hostname: root.backend.hostname
port: root.backend.portIMAP.toString() port: root.backend.portIMAP.toString()
username: configuration.currentAddress username: configuration.currentAddress
password: root.user.password password: root.user ? root.user.password : ""
security: "STARTTLS" security: "STARTTLS"
} }
@ -259,9 +227,11 @@ ScrollView {
hostname : root.backend.hostname hostname : root.backend.hostname
port : root.backend.portSMTP.toString() port : root.backend.portSMTP.toString()
username : configuration.currentAddress username : configuration.currentAddress
password : root.user.password password : root.user ? root.user.password : ""
security : root.backend.useSSLforSMTP ? "SSL" : "STARTTLS" security : root.backend.useSSLforSMTP ? "SSL" : "STARTTLS"
} }
} }
} }
} }
}
}

View File

@ -42,19 +42,6 @@ QtObject {
backend: root.backend backend: root.backend
notifications: root._notifications notifications: root._notifications
onLogin: {
backend.login(username, password)
}
onLogin2FA: {
backend.login2FA(username, code)
}
onLogin2Password: {
backend.login2Password(username, password)
}
onLoginAbort: {
backend.loginAbort(username)
}
onVisibleChanged: { onVisibleChanged: {
backend.dockIconVisible = visible backend.dockIconVisible = visible
} }
@ -167,12 +154,10 @@ QtObject {
break; break;
case SystemTrayIcon.Context: case SystemTrayIcon.Context:
case SystemTrayIcon.Trigger: case SystemTrayIcon.Trigger:
calcStatusWindowPosition()
toggleWindow(statusWindow)
break
case SystemTrayIcon.DoubleClick: case SystemTrayIcon.DoubleClick:
case SystemTrayIcon.MiddleClick: case SystemTrayIcon.MiddleClick:
toggleWindow(mainWindow) calcStatusWindowPosition()
toggleWindow(statusWindow)
break; break;
default: default:
break; break;
@ -181,13 +166,31 @@ QtObject {
} }
Component.onCompleted: { 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() mainWindow.showAndRise()
} }
if (root.backend.users.count === 1 && root.backend.users.get(0).loggedIn === false) { if (u) {
if (c === 1 && u.loggedIn === false) {
mainWindow.showAndRise() mainWindow.showAndRise()
} }
}
if (root.backend.showOnStartup) { if (root.backend.showOnStartup) {
mainWindow.showAndRise() mainWindow.showAndRise()

View File

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

View File

@ -22,7 +22,7 @@ import QtQuick.Controls.impl 2.12
import Proton 4.0 import Proton 4.0
ColumnLayout { Item {
id: root id: root
Layout.fillWidth: true Layout.fillWidth: true
@ -30,6 +30,12 @@ ColumnLayout {
property string label property string label
property string value property string value
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
ColumnLayout {
width: root.width
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
@ -74,10 +80,10 @@ ColumnLayout {
} }
} }
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
height: 1 height: 1
color: root.colorScheme.border_norm color: root.colorScheme.border_norm
} }
} }
}

View File

@ -28,24 +28,8 @@ Item {
property var backend property var backend
property var notifications 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) 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 { RowLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
@ -183,6 +167,7 @@ Item {
onClicked: { onClicked: {
var user = root.backend.users.get(index) var user = root.backend.users.get(index)
accounts.currentIndex = index accounts.currentIndex = index
if (!user) return
if (user.loggedIn) { if (user.loggedIn) {
rightContent.showAccount() rightContent.showAccount()
} else { } else {
@ -248,8 +233,8 @@ Item {
backend: root.backend backend: root.backend
notifications: root.notifications notifications: root.notifications
user: { user: {
if (accounts.currentIndex < 0) return root.noUser if (accounts.currentIndex < 0) return undefined
if (root.backend.users.count == 0) return root.noUser if (root.backend.users.count == 0) return undefined
return root.backend.users.get(accounts.currentIndex) return root.backend.users.get(accounts.currentIndex)
} }
onShowSignIn: { onShowSignIn: {
@ -261,7 +246,7 @@ Item {
} }
} }
GridLayout { // 1 GridLayout { // 1 Sign In
columns: 2 columns: 2
Button { Button {
@ -271,7 +256,10 @@ Item {
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme colorScheme: root.colorScheme
onClicked: rightContent.showAccount() onClicked: {
signIn.abort()
rightContent.showAccount()
}
icon.source: "icons/ic-arrow-left.svg" icon.source: "icons/ic-arrow-left.svg"
secondary: true secondary: true
horizontalPadding: 8 horizontalPadding: 8
@ -289,11 +277,6 @@ Item {
colorScheme: root.colorScheme colorScheme: root.colorScheme
backend: root.backend 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: { selectedAddress: {
if (accounts.currentIndex < 0) return "" if (accounts.currentIndex < 0) return ""
if (root.backend.users.count == 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 showLocalCacheSettings () { rightContent.currentIndex = 5 }
function showHelpView () { rightContent.currentIndex = 6 } function showHelpView () { rightContent.currentIndex = 6 }
function showBugReport () { rightContent.currentIndex = 7 } function showBugReport () { rightContent.currentIndex = 7 }
Connections {
target: root.backend
onLoginFinished: rightContent.showAccount()
}
} }
} }
} }
@ -353,5 +344,4 @@ Item {
signIn.username = username signIn.username = username
rightContent.showSignIn() rightContent.showSignIn()
} }
} }

View File

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

View File

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

View File

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

View File

@ -41,11 +41,6 @@ ApplicationWindow {
property var backend property var backend
property var notifications 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 // show Setup Guide on every new user
Connections { Connections {
target: root.backend.users target: root.backend.users
@ -98,7 +93,15 @@ ApplicationWindow {
return 1 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 return 1
} }
@ -121,19 +124,6 @@ ApplicationWindow {
onShowSetupGuide: { onShowSetupGuide: {
root.showSetup(user,address) root.showSetup(user,address)
} }
onLogin: {
root.login(username, password)
}
onLogin2FA: {
root.login2FA(username, code)
}
onLogin2Password: {
root.login2Password(username, password)
}
onLoginAbort: {
root.loginAbort(username)
}
} }
WelcomeGuide { WelcomeGuide {
@ -142,19 +132,6 @@ ApplicationWindow {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
onLogin: {
root.login(username, password)
}
onLogin2FA: {
root.login2FA(username, code)
}
onLogin2Password: {
root.login2Password(username, password)
}
onLoginAbort: {
root.loginAbort(username)
}
} }
SetupGuide { SetupGuide {

View File

@ -56,6 +56,8 @@ QtObject {
root.updateSilentRestartNeeded, root.updateSilentRestartNeeded,
root.updateSilentError, root.updateSilentError,
root.updateIsLatestVersion, root.updateIsLatestVersion,
root.loginConnectionError,
root.onlyPaidUsers,
root.disableBeta, root.disableBeta,
root.enableBeta, root.enableBeta,
root.bugReportSendSuccess, root.bugReportSendSuccess,
@ -119,7 +121,7 @@ QtObject {
text: qsTr("Update manually") text: qsTr("Update manually")
onTriggered: { onTriggered: {
Qt.openUrlExternally(root.backend.getLandingPage()) Qt.openUrlExternally(root.backend.landingPageLink)
root.updateManualReady.active = false root.updateManualReady.active = false
} }
}, },
@ -174,7 +176,7 @@ QtObject {
text: qsTr("Update manually") text: qsTr("Update manually")
onTriggered: { onTriggered: {
Qt.openUrlExternally(root.backend.getLandingPage()) Qt.openUrlExternally(root.backend.landingPageLink)
root.updateManualError.active = false root.updateManualError.active = false
} }
}, },
@ -217,7 +219,7 @@ QtObject {
text: qsTr("Update manually") text: qsTr("Update manually")
onTriggered: { onTriggered: {
Qt.openUrlExternally(root.backend.getLandingPage()) Qt.openUrlExternally(root.backend.landingPageLink)
root.updateForce.active = false root.updateForce.active = false
} }
}, },
@ -252,7 +254,7 @@ QtObject {
text: qsTr("Update manually") text: qsTr("Update manually")
onTriggered: { onTriggered: {
Qt.openUrlExternally(root.backend.getLandingPage()) Qt.openUrlExternally(root.backend.landingPageLink)
root.updateForceError.active = false root.updateForceError.active = false
} }
}, },
@ -307,7 +309,7 @@ QtObject {
text: qsTr("Update manually") text: qsTr("Update manually")
onTriggered: { onTriggered: {
Qt.openUrlExternally(root.backend.getLandingPage()) Qt.openUrlExternally(root.backend.landingPageLink)
root.updateSilentError.active = false 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 // Bug reports
property Notification bugReportSendSuccess: Notification { property Notification bugReportSendSuccess: Notification {
@ -420,9 +468,6 @@ QtObject {
onTriggered: { onTriggered: {
root.bugReportSendSuccess.active = false 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.Controls.impl 2.12
import QtQuick.Templates 2.12 as T import QtQuick.Templates 2.12 as T
import "."
T.ApplicationWindow { T.ApplicationWindow {
id: root id: root

View File

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

View File

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

View File

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

View File

@ -21,7 +21,8 @@ import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12 import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T import QtQuick.Templates 2.12 as T
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "."
import "." as Proton
Item { Item {
id: root id: root
@ -128,22 +129,22 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 0 spacing: 0
Label { Proton.Label {
colorScheme: root.colorScheme colorScheme: root.colorScheme
id: label id: label
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
type: Label.LabelType.Body_semibold type: Proton.Label.LabelType.Body_semibold
} }
Label { Proton.Label {
colorScheme: root.colorScheme colorScheme: root.colorScheme
id: hint id: hint
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
type: Label.LabelType.Caption type: Proton.Label.LabelType.Caption
} }
} }
@ -270,7 +271,7 @@ Item {
} }
} }
Button { Proton.Button {
colorScheme: root.colorScheme colorScheme: root.colorScheme
id: eyeButton id: eyeButton
@ -299,7 +300,7 @@ Item {
sourceSize.height: assistiveText.height sourceSize.height: assistiveText.height
} }
Label { Proton.Label {
colorScheme: root.colorScheme colorScheme: root.colorScheme
id: assistiveText id: assistiveText
@ -319,7 +320,7 @@ Item {
return root.colorScheme.text_weak 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 2.13
import QtQuick.Controls.impl 2.13 import QtQuick.Controls.impl 2.13
RowLayout{ Item {
id: root id: root
property var colorScheme property var colorScheme
property bool checked property bool checked
property bool disabled
property bool hovered property bool hovered
property bool loading property bool loading
signal clicked signal clicked
property bool _disabled: !enabled
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
Rectangle { Rectangle {
id: indicator id: indicator
implicitWidth: 40 implicitWidth: 40
@ -38,12 +42,12 @@ RowLayout{
radius: 20 radius: 20
color: { color: {
if (root.loading) return "transparent" 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 return root.colorScheme.background_norm
} }
border { border {
width: 1 width: 1
color: (root.disabled || root.loading) ? "transparent" : colorScheme.field_norm color: (root._disabled || root.loading) ? "transparent" : colorScheme.field_norm
} }
Rectangle { Rectangle {
@ -55,7 +59,7 @@ RowLayout{
radius: 12 radius: 12
color: { color: {
if (root.loading) return "transparent" 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.checked) {
if (root.hovered) return root.colorScheme.interaction_norm_hover if (root.hovered) return root.colorScheme.interaction_norm_hover
@ -101,7 +105,7 @@ RowLayout{
hoverEnabled: true hoverEnabled: true
onEntered: {root.hovered = true } onEntered: {root.hovered = true }
onExited: {root.hovered = false } onExited: {root.hovered = false }
onClicked: { root.clicked();} onClicked: { if (root.enabled) root.clicked();}
onPressed: {root.hovered = true } onPressed: {root.hovered = true }
onReleased: { root.hovered = containsMouse } onReleased: { root.hovered = containsMouse }
} }

View File

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

View File

@ -21,7 +21,7 @@ import QtQuick.Controls 2.12
import Proton 4.0 import Proton 4.0
ColumnLayout { Item {
id: root id: root
property var colorScheme property var colorScheme
@ -32,24 +32,33 @@ ColumnLayout {
property var type: SettingsItem.ActionType.Toggle property var type: SettingsItem.ActionType.Toggle
property bool checked: true property bool checked: true
property bool disabled: false
property bool loading: false property bool loading: false
property bool showSeparator: true
property var _bottomMargin: 20
property var _lineWidth: 1
property var _toggleTopMargin: 6
signal clicked signal clicked
spacing: 20
Layout.fillWidth: true
Layout.maximumWidth: root.parent.Layout.maximumWidth
enum ActionType { enum ActionType {
Toggle = 1, Button = 2, PrimaryButton = 3 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 { RowLayout {
Layout.fillWidth: true anchors.fill: parent
spacing: 16
ColumnLayout { ColumnLayout {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.bottomMargin: root._bottomMargin
spacing: 4
Label { Label {
id: mainLabel id: mainLabel
colorScheme: root.colorScheme colorScheme: root.colorScheme
@ -58,10 +67,10 @@ ColumnLayout {
} }
Label { Label {
Layout.minimumWidth: mainLabel.width Layout.fillHeight: true
Layout.maximumWidth: root.Layout.maximumWidth - root.spacing - ( Layout.fillWidth: true
toggle.visible ? toggle.width : button.width
) Layout.preferredWidth: parent.width
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
colorScheme: root.colorScheme colorScheme: root.colorScheme
@ -70,15 +79,12 @@ ColumnLayout {
} }
} }
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
Toggle { Toggle {
Layout.alignment: Qt.AlignTop
Layout.topMargin: root._toggleTopMargin
id: toggle id: toggle
colorScheme: root.colorScheme colorScheme: root.colorScheme
visible: root.type == SettingsItem.ActionType.Toggle visible: root.type === SettingsItem.ActionType.Toggle
checked: root.checked checked: root.checked
loading: root.loading loading: root.loading
@ -86,20 +92,25 @@ ColumnLayout {
} }
Button { Button {
Layout.alignment: Qt.AlignTop
id: button id: button
colorScheme: root.colorScheme 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 != "" ? " " : "") text: root.actionText + (root.actionIcon != "" ? " " : "")
loading: root.loading loading: root.loading
icon.source: root.actionIcon icon.source: root.actionIcon
onClicked: { if (!root.loading) root.clicked() } onClicked: { if (!root.loading) root.clicked() }
secondary: root.type != SettingsItem.PrimaryButton secondary: root.type !== SettingsItem.PrimaryButton
} }
} }
Rectangle { Rectangle {
Layout.fillWidth: true anchors.left: root.left
anchors.right: root.right
anchors.bottom: root.bottom
color: colorScheme.border_weak 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 import Proton 4.0
ScrollView { Item {
id: root id: root
property var colorScheme property var colorScheme
@ -31,36 +31,45 @@ ScrollView {
signal back() signal back()
property int _leftRightMargins: 64 property int _leftMargin: 64
property int _topBottomMargins: 68 property int _rightMargin: 64
property int _spacing: 22 property int _topMargin: 32
property int _bottomMargin: 32
property int _spacing: 20
ScrollView {
clip: true clip: true
contentWidth: pane.width
contentHeight: pane.height
RowLayout{
id: pane
width:root.width width:root.width
height:root.height
contentWidth: content.width
contentHeight: content.height
ColumnLayout { ColumnLayout {
id: content id: content
spacing: root._spacing spacing: root._spacing
Layout.maximumWidth: root.width - 2*root._leftRightMargins width: root.width - (root._leftMargin + root._rightMargin)
Layout.fillWidth: true
Layout.topMargin: root._topBottomMargins anchors{
Layout.bottomMargin: root._topBottomMargins top: parent.top
Layout.leftMargin: root._leftRightMargins left: parent.left
Layout.rightMargin: root._leftRightMargins topMargin: root._topMargin
bottomMargin: root._bottomMargin
leftMargin: root._leftMargin
rightMargin: root._rightMargin
}
} }
} }
Button { Button {
id: backButton
anchors { anchors {
top: parent.top top: parent.top
left: parent.left left: parent.left
topMargin: 10 topMargin: root._topMargin
leftMargin: 18 leftMargin: (root._leftMargin-backButton.width) / 2
} }
colorScheme: root.colorScheme colorScheme: root.colorScheme
onClicked: root.back() onClicked: root.back()

View File

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

View File

@ -27,31 +27,27 @@ Item {
id: root id: root
property ColorScheme colorScheme property ColorScheme colorScheme
function abort() { function reset() {
root.loginAbort(usernameTextField.text) stackLayout.currentIndex = 0
loginNormalLayout.reset()
login2FALayout.reset()
login2PasswordLayout.reset()
} }
signal login(string username, string password) function abort() {
signal login2FA(string username, string code) root.reset()
signal login2Password(string username, string password) root.backend.loginAbort(usernameTextField.text)
signal loginAbort(string username) }
implicitHeight: children[0].implicitHeight implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth implicitWidth: children[0].implicitWidth
property var backend property var backend
property var window
property alias username: usernameTextField.text property alias username: usernameTextField.text
state: "Page 1" state: "Page 1"
onLoginAbort: {
stackLayout.currentIndex = 0
loginNormalLayout.reset()
login2FALayout.reset()
login2PasswordLayout.reset()
}
property alias currentIndex: stackLayout.currentIndex property alias currentIndex: stackLayout.currentIndex
StackLayout { StackLayout {
@ -83,18 +79,16 @@ Item {
onLoginFreeUserError: { onLoginFreeUserError: {
console.assert(stackLayout.currentIndex == 0, "Unexpected loginFreeUserError") console.assert(stackLayout.currentIndex == 0, "Unexpected loginFreeUserError")
stackLayout.loginFailed() stackLayout.loginFailed()
window.notifyOnlyPaidUsers()
} }
onLoginConnectionError: { onLoginConnectionError: {
if (stackLayout.currentIndex == 0 ) { if (stackLayout.currentIndex == 0 ) {
stackLayout.loginFailed() stackLayout.loginFailed()
} }
window.notifyConnectionLostWhileLogin()
} }
onLogin2FARequested: { onLogin2FARequested: {
console.assert(stackLayout.currentIndex == 0, "Unexpected login2FARequested") console.assert(stackLayout.currentIndex == 0, "Unexpected login2FARequested")
stackLayout.currentIndex = 1 stackLayout.currentIndex = 1
} }
onLogin2FAError: { onLogin2FAError: {
@ -108,19 +102,12 @@ Item {
} }
onLogin2FAErrorAbort: { onLogin2FAErrorAbort: {
console.assert(stackLayout.currentIndex == 1, "Unexpected login2FAErrorAbort") console.assert(stackLayout.currentIndex == 1, "Unexpected login2FAErrorAbort")
root.reset()
stackLayout.currentIndex = 0
loginNormalLayout.reset()
login2FALayout.reset()
login2PasswordLayout.reset()
errorLabel.text = qsTr("Incorrect login credentials. Please try again.") errorLabel.text = qsTr("Incorrect login credentials. Please try again.")
passwordTextField.text = ""
} }
onLogin2PasswordRequested: { onLogin2PasswordRequested: {
console.assert(stackLayout.currentIndex == 0 || stackLayout.currentIndex == 1, "Unexpected login2PasswordRequested") console.assert(stackLayout.currentIndex == 0 || stackLayout.currentIndex == 1, "Unexpected login2PasswordRequested")
stackLayout.currentIndex = 2 stackLayout.currentIndex = 2
} }
onLogin2PasswordError: { onLogin2PasswordError: {
@ -134,22 +121,13 @@ Item {
} }
onLogin2PasswordErrorAbort: { onLogin2PasswordErrorAbort: {
console.assert(stackLayout.currentIndex == 2, "Unexpected login2PasswordErrorAbort") console.assert(stackLayout.currentIndex == 2, "Unexpected login2PasswordErrorAbort")
root.reset()
stackLayout.currentIndex = 0
loginNormalLayout.reset()
login2FALayout.reset()
login2PasswordLayout.reset()
errorLabel.text = qsTr("Incorrect login credentials. Please try again.") errorLabel.text = qsTr("Incorrect login credentials. Please try again.")
passwordTextField.text = ""
} }
onLoginFinished: { onLoginFinished: {
stackLayout.currentIndex = 0 stackLayout.currentIndex = 0
loginNormalLayout.reset() root.reset()
passwordTextField.text = ""
login2FALayout.reset()
login2PasswordLayout.reset()
} }
} }
@ -168,6 +146,7 @@ Item {
passwordTextField.enabled = true passwordTextField.enabled = true
passwordTextField.error = false passwordTextField.error = false
passwordTextField.assistiveText = "" passwordTextField.assistiveText = ""
passwordTextField.text = ""
} }
spacing: 0 spacing: 0
@ -303,7 +282,7 @@ Item {
enabled = false enabled = false
loading = true 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.enabled = true
twoFactorPasswordTextField.error = false twoFactorPasswordTextField.error = false
twoFactorPasswordTextField.assistiveText = "" twoFactorPasswordTextField.assistiveText = ""
twoFactorPasswordTextField.text=""
} }
spacing: 0 spacing: 0
@ -388,7 +368,7 @@ Item {
enabled = false enabled = false
loading = true 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.enabled = true
secondPasswordTextField.error = false secondPasswordTextField.error = false
secondPasswordTextField.assistiveText = "" secondPasswordTextField.assistiveText = ""
secondPasswordTextField.text = ""
} }
spacing: 0 spacing: 0
@ -460,7 +441,7 @@ Item {
enabled = false enabled = false
loading = true 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 { NotificationFilter {
id: notificationFilter id: notificationFilter
source: root.notifications.all source: root.notifications ? root.notifications.all : undefined
whitelist: root.notificationWhitelist whitelist: root.notificationWhitelist
blacklist: root.notificationBlacklist blacklist: root.notificationBlacklist

View File

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

View File

@ -28,12 +28,6 @@ Item {
property ColorScheme colorScheme property ColorScheme colorScheme
property var backend 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 implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth implicitWidth: children[0].implicitWidth
@ -230,22 +224,14 @@ Item {
Layout.preferredWidth: 320 Layout.preferredWidth: 320
Layout.fillWidth: true Layout.fillWidth: true
onLogin: { username: {
root.login(username, password) 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 backend: root.backend
window: root.window
} }
// Right margin // Right margin

View File

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

View File

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

View File

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

View File

@ -20,9 +20,10 @@ import QtQuick.Window 2.13
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import Proton 4.0 import "../Proton"
RowLayout { RowLayout {
id: root
property ColorScheme colorScheme property ColorScheme colorScheme
ColumnLayout { ColumnLayout {
@ -32,30 +33,36 @@ RowLayout {
Switch { Switch {
text: "Toggle" text: "Toggle"
colorScheme: root.colorScheme
} }
Switch { Switch {
text: "Toggle" text: "Toggle"
enabled: false enabled: false
colorScheme: root.colorScheme
} }
Switch { Switch {
text: "Toggle" text: "Toggle"
loading: true loading: true
colorScheme: root.colorScheme
} }
Switch { Switch {
text: "" text: ""
colorScheme: root.colorScheme
} }
Switch { Switch {
text: "" text: ""
enabled: false enabled: false
colorScheme: root.colorScheme
} }
Switch { Switch {
text: "" text: ""
loading: true loading: true
colorScheme: root.colorScheme
} }
} }
@ -67,35 +74,41 @@ RowLayout {
Switch { Switch {
text: "Toggle" text: "Toggle"
checked: true checked: true
colorScheme: root.colorScheme
} }
Switch { Switch {
text: "Toggle" text: "Toggle"
checked: true checked: true
enabled: false enabled: false
colorScheme: root.colorScheme
} }
Switch { Switch {
text: "Toggle" text: "Toggle"
checked: true checked: true
loading: true loading: true
colorScheme: root.colorScheme
} }
Switch { Switch {
text: "" text: ""
checked: true checked: true
colorScheme: root.colorScheme
} }
Switch { Switch {
text: "" text: ""
checked: true checked: true
enabled: false enabled: false
colorScheme: root.colorScheme
} }
Switch { Switch {
text: "" text: ""
checked: true checked: true
loading: 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,16 +19,25 @@ import QtQuick 2.13
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import Proton 4.0 import "../Proton"
Rectangle { Rectangle {
id: root
property ColorScheme colorScheme property ColorScheme colorScheme
color: colorScheme.background_norm color: colorScheme.background_norm
clip: true 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 anchors.fill: parent
ColumnLayout {
anchors.margins: 20
width: root.width
spacing: 5 spacing: 5
Buttons { Buttons {
@ -37,24 +46,18 @@ Rectangle {
Layout.margins: 20 Layout.margins: 20
} }
TextFields {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
TextAreas {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
CheckBoxes { CheckBoxes {
colorScheme: root.colorScheme colorScheme: root.colorScheme
Layout.fillWidth: true Layout.fillWidth: true
Layout.margins: 20 Layout.margins: 20
} }
ComboBoxes {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.margins: 20
}
RadioButtons { RadioButtons {
colorScheme: root.colorScheme colorScheme: root.colorScheme
Layout.fillWidth: true Layout.fillWidth: true
@ -66,5 +69,18 @@ Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.margins: 20 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.Layouts 1.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import Proton 4.0 import "../Proton"
RowLayout { RowLayout {
id: root id: root

View File

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

View File

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

View File

@ -43,6 +43,11 @@ func (f *FrontendQt) watchEvents() {
userChangedCh := f.eventListener.ProvideChannel(events.UserRefreshEvent) userChangedCh := f.eventListener.ProvideChannel(events.UserRefreshEvent)
certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue) 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 { for {
select { select {
case errorDetails := <-errorCh: case errorDetails := <-errorCh:
@ -77,7 +82,7 @@ func (f *FrontendQt) watchEvents() {
case <-updateApplicationCh: case <-updateApplicationCh:
f.updateForce() f.updateForce()
case userID := <-userChangedCh: case userID := <-userChangedCh:
f.userChanged(userID) f.qml.UserChanged(userID)
case <-certIssue: case <-certIssue:
f.qml.ApiCertIssue() f.qml.ApiCertIssue()
} }

View File

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

View File

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

View File

@ -22,79 +22,10 @@ package qt
import ( import (
"context" "context"
"encoding/base64" "encoding/base64"
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "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) { func (f *FrontendQt) login(username, password string) {
var err error var err error
f.password, err = base64.StdEncoding.DecodeString(password) 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) f.authClient, f.auth, err = f.bridge.Login(username, f.password)
if err != nil { if err != nil {
// TODO login free user error
f.qml.LoginUsernamePasswordError(err.Error()) f.qml.LoginUsernamePasswordError(err.Error())
f.loginClean() f.loginClean()
return return
@ -185,29 +117,24 @@ func (f *FrontendQt) login2Password(username, mboxPassword string) {
func (f *FrontendQt) finishLogin() { func (f *FrontendQt) finishLogin() {
defer f.loginClean() defer f.loginClean()
if f.auth == nil || f.authClient == nil { if len(f.password) == 0 || f.auth == nil || f.authClient == nil {
f.log.Errorf("Finish login: Authethication incomplete %p %p", f.auth, f.authClient) 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.") f.qml.Login2PasswordErrorAbort("Missing authentication, try again.")
return return
} }
user, err := f.bridge.FinishLogin(f.authClient, f.auth, f.password) _, err := f.bridge.FinishLogin(f.authClient, f.auth, f.password)
if err != nil { if err != nil && err != users.ErrUserAlreadyConnected {
f.log.Errorf("Authethication incomplete %p %p", f.auth, f.authClient) f.log.WithError(err).Errorf("Finish login failed")
f.qml.Login2PasswordErrorAbort("Missing authentication, try again.") f.qml.Login2PasswordErrorAbort(err.Error())
return return
} }
index := f.qml.Users().indexByID(user.ID()) defer f.qml.LoginFinished()
if index < 0 {
qu := newQMLUserFromBacked(f, user)
qu.SetSetupGuideSeen(false)
f.qml.Users().addUser(qu)
return
}
f.qml.Users().users[index].update(user)
f.qml.LoginFinished()
} }
func (f *FrontendQt) loginAbort(username string) { func (f *FrontendQt) loginAbort(username string) {

View File

@ -28,6 +28,10 @@ import (
"github.com/therecipe/qt/core" "github.com/therecipe/qt/core"
) )
func init() {
QMLBackend_QRegisterMetaType()
}
// QMLBackend connects QML frontend with Go backend. // QMLBackend connects QML frontend with Go backend.
type QMLBackend struct { type QMLBackend struct {
core.QObject core.QObject
@ -138,6 +142,8 @@ type QMLBackend struct {
_ func(address string) `signal:addressChangedLogout` _ func(address string) `signal:addressChangedLogout`
_ func(username string) `signal:userDisconnected` _ func(username string) `signal:userDisconnected`
_ func() `signal:apiCertIssue` _ func() `signal:apiCertIssue`
_ func(userID string) `signal:userChanged`
} }
func (q *QMLBackend) setup(f *FrontendQt) { func (q *QMLBackend) setup(f *FrontendQt) {
@ -150,38 +156,81 @@ func (q *QMLBackend) setup(f *FrontendQt) {
return f.showOnStartup return f.showOnStartup
}) })
q.ConnectIsDockIconVisible(func() bool { q.ConnectIsDockIconVisible(dockIcon.GetDockIconVisibleState)
return dockIcon.GetDockIconVisibleState() q.ConnectSetDockIconVisible(dockIcon.SetDockIconVisibleState)
})
q.ConnectSetDockIconVisible(func(visible bool) {
dockIcon.SetDockIconVisibleState(visible)
})
q.SetUsers(NewQMLUserModel(nil)) um := NewQMLUserModel(q)
f.loadUsers() um.f = f
q.SetUsers(um)
um.load()
q.ConnectUserChanged(um.userChanged)
q.SetGoos(runtime.GOOS) q.SetGoos(runtime.GOOS)
q.ConnectLogin(func(u, p string) { go f.login(u, p) }) q.ConnectLogin(func(u, p string) {
q.ConnectLogin2FA(func(u, p string) { go f.login2FA(u, p) }) go func() {
q.ConnectLogin2Password(func(u, p string) { go f.login2Password(u, p) }) defer f.panicHandler.HandlePanic()
q.ConnectLoginAbort(func(u string) { go f.loginAbort(u) }) 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) go func() {
q.ConnectCheckUpdates(func() { go f.checkUpdatesAndNotify(true) }) defer f.panicHandler.HandlePanic()
f.checkUpdatesAndNotify(false)
}()
q.ConnectCheckUpdates(func() {
go func() {
defer f.panicHandler.HandlePanic()
f.checkUpdatesAndNotify(true)
}()
})
f.setIsDiskCacheEnabled() f.setIsDiskCacheEnabled()
f.setDiskCachePath() 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() 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() f.setIsAutostartOn()
q.ConnectToggleAutostart(f.toggleAutostart) q.ConnectToggleAutostart(f.toggleAutostart)
f.setIsBetaEnabled() 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.SetIsDoHEnabled(f.settings.GetBool(settings.AllowProxyKey))
q.ConnectToggleDoH(f.toggleDoH) q.ConnectToggleDoH(f.toggleDoH)
@ -195,7 +244,12 @@ func (q *QMLBackend) setup(f *FrontendQt) {
q.ConnectChangePorts(f.changePorts) q.ConnectChangePorts(f.changePorts)
q.ConnectIsPortFree(f.isPortFree) q.ConnectIsPortFree(f.isPortFree)
q.ConnectTriggerReset(func() { go f.triggerReset() }) q.ConnectTriggerReset(func() {
go func() {
defer f.panicHandler.HandlePanic()
f.triggerReset()
}()
})
f.setVersion() f.setVersion()
f.setLogsPath() f.setLogsPath()
@ -203,9 +257,24 @@ func (q *QMLBackend) setup(f *FrontendQt) {
f.setLicensePath() f.setLicensePath()
f.setCurrentEmailClient() f.setCurrentEmailClient()
q.ConnectUpdateCurrentMailClient(func() { go f.setCurrentEmailClient() }) q.ConnectUpdateCurrentMailClient(func() {
q.ConnectReportBug(func(d, a, e string, i bool) { go f.reportBug(d, a, e, i) }) 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() 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 package qt
import ( import (
"sync"
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/therecipe/qt/core" "github.com/therecipe/qt/core"
) )
func init() {
QMLUser_QRegisterMetaType()
QMLUserModel_QRegisterMetaType()
}
// QMLUserModel stores list of of users // QMLUserModel stores list of of users
type QMLUserModel struct { type QMLUserModel struct {
core.QAbstractListModel core.QAbstractListModel
@ -33,72 +40,168 @@ type QMLUserModel struct {
_ func() `constructor:"init"` _ func() `constructor:"init"`
_ func(row int) *core.QVariant `slot:"get"` _ func(row int) *core.QVariant `slot:"get"`
users []*QMLUser userIDs []string
userByID map[string]*QMLUser
access sync.RWMutex
f *FrontendQt
} }
func (um *QMLUserModel) init() { func (um *QMLUserModel) init() {
um.SetRoles(map[int]*core.QByteArray{ um.access.Lock()
int(core.Qt__UserRole + 1): newQByteArrayFromString("object"), defer um.access.Unlock()
}) um.SetCount(0)
um.ConnectRowCount(um.rowCount) um.ConnectRowCount(um.rowCount)
um.ConnectData(um.data) um.ConnectData(um.data)
um.ConnectGet(um.get) um.ConnectGet(um.get)
um.users = []*QMLUser{} um.ConnectCount(func() int {
um.setCount() 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 { func (um *QMLUserModel) data(index *core.QModelIndex, property int) *core.QVariant {
if !index.IsValid() { if !index.IsValid() {
um.f.log.WithField("size", len(um.userIDs)).Info("Trying to get user by invalid index")
return core.NewQVariant() return core.NewQVariant()
} }
return um.get(index.Row()) return um.get(index.Row())
} }
func (um *QMLUserModel) get(index int) *core.QVariant { 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 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 { 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() { func (um *QMLUserModel) setCount() {
um.SetCount(len(um.users)) um.SetCount(len(um.userIDs))
} }
func (um *QMLUserModel) addUser(user *QMLUser) { func (um *QMLUserModel) addUser(userID string) {
um.BeginInsertRows(core.NewQModelIndex(), um.rowCount(nil), um.rowCount(nil)) um.BeginInsertRows(core.NewQModelIndex(), len(um.userIDs), len(um.userIDs))
um.users = append(um.users, user) um.access.Lock()
um.setCount() if um.indexByIDNotSafe(userID) < 0 {
um.userIDs = append(um.userIDs, userID)
}
um.access.Unlock()
um.EndInsertRows() um.EndInsertRows()
um.setCount()
} }
func (um *QMLUserModel) removeUser(row int) { func (um *QMLUserModel) removeUser(row int) {
um.BeginRemoveRows(core.NewQModelIndex(), row, row) um.BeginRemoveRows(core.NewQModelIndex(), row, row)
um.users = append(um.users[:row], um.users[row+1:]...) um.access.Lock()
um.setCount() id := um.userIDs[row]
um.userIDs = append(um.userIDs[:row], um.userIDs[row+1:]...)
delete(um.userByID, id)
um.access.Unlock()
um.EndRemoveRows() um.EndRemoveRows()
um.setCount()
} }
func (um *QMLUserModel) clear() { func (um *QMLUserModel) clear() {
um.BeginRemoveRows(core.NewQModelIndex(), 0, um.rowCount(nil)) um.BeginResetModel()
um.users = []*QMLUser{} um.access.Lock()
um.setCount() um.userIDs = []string{}
um.EndRemoveRows() um.userByID = map[string]*QMLUser{}
um.SetCount(0)
um.access.Unlock()
um.EndResetModel()
} }
func (um *QMLUserModel) indexByID(id string) int { func (um *QMLUserModel) load() {
for i, qu := range um.users { um.clear()
if id == qu.ID {
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 i
} }
} }
return -1 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. // QMLUser holds data, slots and signals and for user.
type QMLUser struct { type QMLUser struct {
core.QObject core.QObject
@ -116,18 +219,66 @@ type QMLUser struct {
_ func(makeItActive bool) `slot:"toggleSplitMode"` _ func(makeItActive bool) `slot:"toggleSplitMode"`
_ func() `signal:"toggleSplitModeFinished"` _ func() `signal:"toggleSplitModeFinished"`
_ func() `slot:"logout"` _ func() `slot:"logout"`
_ func() `slot:"remove"`
_ func(address string) `slot:"configureAppleMail"` _ func(address string) `slot:"configureAppleMail"`
ID string 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) { func (qu *QMLUser) update(user types.User) {
username := user.Username() username := user.Username()
qu.SetAvatarText(getInitials(username)) qu.SetAvatarText(getInitials(username))
qu.SetUsername(username) qu.SetUsername(username)
qu.SetLoggedIn(user.IsConnected()) qu.SetLoggedIn(user.IsConnected())
qu.SetSplitMode(!user.IsCombinedAddressMode()) qu.SetSplitMode(!user.IsCombinedAddressMode())
qu.SetSetupGuideSeen(true) qu.SetSetupGuideSeen(false)
qu.SetUsedBytes(1.0) // TODO qu.SetUsedBytes(1.0) // TODO
qu.SetTotalBytes(10000.0) // TODO qu.SetTotalBytes(10000.0) // TODO
qu.SetPassword(user.GetBridgePassword()) qu.SetPassword(user.GetBridgePassword())

View File

@ -40,8 +40,13 @@ var (
log = logrus.WithField("pkg", "users") //nolint[gochecknoglobals] log = logrus.WithField("pkg", "users") //nolint[gochecknoglobals]
isApplicationOutdated = false //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") 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. // 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") 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. // Update the user's credentials with the latest auth used to connect this user.