diff --git a/internal/frontend/qml/AccountDelegate.qml b/internal/frontend/qml/AccountDelegate.qml new file mode 100644 index 00000000..58ca0443 --- /dev/null +++ b/internal/frontend/qml/AccountDelegate.qml @@ -0,0 +1,69 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 + +import Proton 4.0 + +RowLayout { + id: root + property var colorScheme: parent.colorScheme + + property var text: "janedoe@protonmail.com" + property var avatarText: "jd" + property var captionText: "50.5 MB / 20 GB" + + spacing: 16 + + Rectangle { + id: avatar + Layout.preferredHeight: account.height + Layout.preferredWidth: account.height + radius: 4 + + color: root.colorScheme.background_avatar + + ProtonLabel { + anchors.centerIn: avatar + color: root.colorScheme.text_norm + text: root.avatarText.toUpperCase() + state: "body" + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + } + } + + ColumnLayout { + id: account + Layout.fillHeight: true + Layout.fillWidth: true + + ProtonLabel { + text: root.text + color: root.colorScheme.text_norm + state: "body" + } + + ProtonLabel { + text: root.captionText + color: root.colorScheme.text_weak + state: "caption" + } + } +} diff --git a/internal/frontend/qml/AccountView.qml b/internal/frontend/qml/AccountView.qml new file mode 100644 index 00000000..4e5ed937 --- /dev/null +++ b/internal/frontend/qml/AccountView.qml @@ -0,0 +1,48 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 + +import Proton 4.0 + +ColumnLayout { + id: root + property var colorScheme: parent.colorScheme + + spacing: 0 + + Rectangle { + Layout.fillWidth: true + Layout.minimumHeight: 277 + Layout.maximumHeight: 277 + + color: root.colorScheme.background_norm + + ColumnLayout { + + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + + color: root.colorScheme.background_weak + } +} diff --git a/internal/frontend/qml/Banners.qml b/internal/frontend/qml/Banners.qml new file mode 100644 index 00000000..19365014 --- /dev/null +++ b/internal/frontend/qml/Banners.qml @@ -0,0 +1,121 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQml 2.12 +import QtQuick 2.13 +import Proton 4.0 +import QtQuick.Controls 2.13 + +Rectangle { + id: root + + property var window + + property bool onTop: false + property bool blocking: root.nDangers != 0 + property int nDangers: 0 + + color: root.getTransparentVersion(window.colorScheme.text_norm,root.blocking ? 0.5 : 0) + + MouseArea { + anchors.fill: root + acceptedButtons: root.blocking ? Qt.AllButtons : Qt.NoButton + enabled: root.blocking + } + + ListModel { + id: notifications + } + + ListView { + id: view + anchors.top : root.top + anchors.bottom : root.bottom + anchors.horizontalCenter : root.horizontalCenter + anchors.topMargin : root.height/20 + anchors.bottomMargin : root.height/20 + + layoutDirection: ListView.Vertical + verticalLayoutDirection: root.onTop ? ListView.TopToBottom : ListView.BottomToTop + + spacing: 5 + + model: notifications + delegate: Banner { + id: bannerDelegate + anchors.horizontalCenter: parent.horizontalCenter + text: model.text + actionText: model.buttonText + state: model.state + + onAccepted: { + switch (model.submitAction) { + case "update": + console.log("I am updating now") + break; + default: + console.log("NOOP") + } + if (model.state == "danger") root.nDangers-=1 + anchors.horizontalCenter = undefined + notifications.remove(index) + } + } + } + + function notify(descriptionText, buttonText, type = "info", submitAction = "noop") { + if (type === "danger") root.nDangers+=1 + notifications.append({ + "text": descriptionText, + "buttonText": buttonText, + "state": type, + "submitAction": submitAction + }) + } + + function notifyOnlyPaidUsers(){ + root.notify( + qsTr("Bridge is exclusive to our paid plans. Upgrade your account to use Bridge."), + qsTr("ok"), "danger" + ) + } + + function notifyConnectionLostWhileLogin(){ + root.notify( + qsTr("Can't connect to the server. Check your internet connection and try again."), + qsTr("ok"), "danger" + ) + } + + function notifyUpdateManually(){ + root.notify( + qsTr("Bridge could not update automatically."), + qsTr("update"), "warning", "update" + ) + } + + function notifyUserAdded(){ + root.notify( + qsTr("Your account has been added to Bridge and you are now signed in."), + qsTr("ok"), "success" + ) + } + + function getTransparentVersion(original, transparency){ + return Qt.rgba(original.r, original.g, original.b, transparency) + } +} diff --git a/internal/frontend/qml/Bridge.qml b/internal/frontend/qml/Bridge.qml index 382774aa..8f729a56 100644 --- a/internal/frontend/qml/Bridge.qml +++ b/internal/frontend/qml/Bridge.qml @@ -15,17 +15,41 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . +import QtQml 2.12 import QtQuick 2.13 import QtQuick.Window 2.13 import Qt.labs.platform 1.0 QtObject { - // default property list children: [] + id: root - property var _mainWindow: MainWindow { + property var backend + property var users + + signal login(string username, string password) + signal login2FA(string username, string code) + signal login2Password(string username, string password) + signal loginAbort(string username) + + property var mainWindow: MainWindow { id: mainWindow - title: "ProtonMail Bridge" - visible: false + visible: true + + backend: root.backend + users: root.users + + onLogin: { + root.login(username, password) + } + onLogin2FA: { + root.login2FA(username, code) + } + onLogin2Password: { + root.login2Password(username, password) + } + onLoginAbort: { + root.loginAbort(username) + } } property var _trayMenu: Window { @@ -33,22 +57,23 @@ QtObject { title: "window 2" visible: false flags: Qt.Dialog - - width: 448 } property var _trayIcon: SystemTrayIcon { id: trayIcon visible: true - iconSource: "./icons/rectangle-systray.png" + iconSource: "./icons/ic-systray.svg" onActivated: { switch (reason) { case SystemTrayIcon.Unknown: break; case SystemTrayIcon.Context: + trayMenu.x = (Screen.desktopAvailableWidth - trayMenu.width) / 2 + trayMenu.visible = !trayMenu.visible break case SystemTrayIcon.DoubleClick: - break + mainWindow.visible = !mainWindow.visible + break; case SystemTrayIcon.Trigger: trayMenu.x = (Screen.desktopAvailableWidth - trayMenu.width) / 2 trayMenu.visible = !trayMenu.visible diff --git a/internal/frontend/qml/BridgeTest/UserControl.qml b/internal/frontend/qml/BridgeTest/UserControl.qml new file mode 100644 index 00000000..826053f9 --- /dev/null +++ b/internal/frontend/qml/BridgeTest/UserControl.qml @@ -0,0 +1,208 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQml 2.12 +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 + +ColumnLayout { + property var user + property var backend + + spacing : 5 + + Layout.fillHeight: true + //Layout.fillWidth: true + + property var colorScheme + + TextField { + Layout.fillWidth: true + + text: user !== undefined ? user.username : "" + + onEditingFinished: { + user.username = text + } + } + + RowLayout { + Layout.fillWidth: true + + Button { + //Layout.fillWidth: true + + text: "Login" + enabled: user !== undefined && !user.loggedIn && user.username.length > 0 + + onClicked: { + if (user === backend.loginUser) { + var newUserObject = backend.userComponent.createObject(backend, {username: user.username, loggedIn: true}) + backend.users.append( { object: newUserObject } ) + + user.username = "" + user.resetLoginRequests() + return + } + + user.loggedIn = true + user.resetLoginRequests() + } + } + + Button { + //Layout.fillWidth: true + + text: "Logout" + enabled: user !== undefined && user.loggedIn && user.username.length > 0 + + onClicked: { + user.loggedIn = false + user.resetLoginRequests() + } + } + } + + + RowLayout { + Layout.fillWidth: true + + Label { + id: loginLabel + text: "Login:" + + Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth) + } + + Button { + text: "name/pass error" + enabled: user !== undefined && user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordProvided + + onClicked: { + user.loginUsernamePasswordError() + user.resetLoginRequests() + } + } + + Button { + text: "free user error" + enabled: user !== undefined && user.isLoginRequested + onClicked: { + user.loginFreeUserError() + user.resetLoginRequests() + } + } + + Button { + text: "connection error" + enabled: user !== undefined && user.isLoginRequested + onClicked: { + user.loginConnectionError() + user.resetLoginRequests() + } + } + } + + RowLayout { + Layout.fillWidth: true + + Label { + id: faLabel + text: "2FA:" + + Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth) + } + + Button { + text: "request" + + enabled: user !== undefined && user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordRequested + onClicked: { + user.login2FARequested() + user.isLogin2FARequested = true + } + } + + Button { + text: "error" + + enabled: user !== undefined && user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided) + onClicked: { + user.login2FAError() + user.isLogin2FAProvided = false + } + } + + Button { + text: "Abort" + + enabled: user !== undefined && user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided) + onClicked: { + user.login2FAErrorAbort() + user.resetLoginRequests() + } + } + } + + RowLayout { + Layout.fillWidth: true + + Label { + id: passLabel + text: "2 Password:" + + Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth) + } + + Button { + text: "request" + + enabled: user !== undefined && user.isLoginRequested && !user.isLogin2PasswordRequested && !(user.isLogin2FARequested && !user.isLogin2FAProvided) + onClicked: { + user.login2PasswordRequested() + user.isLogin2PasswordRequested = true + } + } + + Button { + text: "error" + + enabled: user !== undefined && user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided) + onClicked: { + user.login2PasswordError() + + user.isLogin2PasswordProvided = false + } + } + + Button { + text: "Abort" + + enabled: user !== undefined && user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided) + onClicked: { + user.login2PasswordErrorAbort() + user.resetLoginRequests() + } + } + } + + + Item { + Layout.fillHeight: true + } +} diff --git a/internal/frontend/qml/BridgeTest/UserList.qml b/internal/frontend/qml/BridgeTest/UserList.qml new file mode 100644 index 00000000..356f363e --- /dev/null +++ b/internal/frontend/qml/BridgeTest/UserList.qml @@ -0,0 +1,91 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 + +import Proton 4.0 + +ColumnLayout { + id: root + + property var colorScheme + property var backend + + property alias currentIndex: usersListView.currentIndex + ListView { + id: usersListView + Layout.fillHeight: true + Layout.preferredWidth: 200 + + model: backend.usersTest + highlightFollowsCurrentItem: true + + delegate: Item { + + implicitHeight: children[0].implicitHeight + anchors.topMargin + anchors.bottomMargin + implicitWidth: children[0].implicitWidth + anchors.leftMargin + anchors.rightMargin + + width: usersListView.width + + anchors.margins: 10 + + Label { + text: modelData.username + anchors.margins: 10 + anchors.fill: parent + color: root.colorScheme.text_norm + + MouseArea { + anchors.fill: parent + onClicked: { + usersListView.currentIndex = index + } + } + } + } + + highlight: Rectangle { + color: root.colorScheme.interaction_default_active + } + } + + RowLayout { + Layout.fillWidth: true + + Button { + text: "+" + + onClicked: { + var newUserObject = backend.userComponent.createObject(backend, { username: "test@protonmail.com", loggedIn: false } ) + backend.users.append( { object: newUserObject } ) + } + } + Button { + text: "-" + + enabled: usersListView.currentIndex != 0 + + onClicked: { + // var userObject = backend.users.get(usersListView.currentIndex - 1) + backend.users.remove(usersListView.currentIndex - 1) + // userObject.deleteLater() + } + } + } +} diff --git a/internal/frontend/qml/BridgeTest/UserModel.qml b/internal/frontend/qml/BridgeTest/UserModel.qml new file mode 100644 index 00000000..8b59819a --- /dev/null +++ b/internal/frontend/qml/BridgeTest/UserModel.qml @@ -0,0 +1,28 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQml.Models 2.12 + +ListModel { + // overriding get method to ignore any role and return directly object itself + function get(row) { + if (row < 0 || row >= count) { + return undefined + } + return data(index(row, 0), Qt.DisplayRole) + } +} diff --git a/internal/frontend/qml/Bridge_test.qml b/internal/frontend/qml/Bridge_test.qml index b772401a..ba998bce 100644 --- a/internal/frontend/qml/Bridge_test.qml +++ b/internal/frontend/qml/Bridge_test.qml @@ -15,42 +15,431 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . +import QtQml 2.12 import QtQuick 2.13 import QtQuick.Window 2.13 +import QtQuick.Layouts 1.12 import QtQuick.Controls 2.13 +import QtQml.Models 2.12 + +import Proton 4.0 + +import "./BridgeTest" + Window { - id: testroot - width : 250 - height : 600 - flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint + id: root + + width: 640 + height: 480 + x: 100 + y: 100 + + property var colorScheme: ProtonStyle.darkStyle + + flags : Qt.Window | Qt.Dialog visible : true - title : "GUI test Window" - color : "#10101010" + title : "Bridge Test GUI" + color : colorScheme.background_norm - Column { - anchors.horizontalCenter: parent.horizontalCenter - spacing : 5 - Button { - text: "Show window" - onClicked: { - bridge._mainWindow.visible = true + function _log(msg, color) { + logTextArea.text += "

" + msg + "

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