diff options
author | kali kaneko (leap communications) <kali@leap.se> | 2021-09-06 21:08:14 +0200 |
---|---|---|
committer | kali kaneko (leap communications) <kali@leap.se> | 2021-11-23 21:51:01 +0100 |
commit | 8543125fa656ddc2c114072adfc27e4e7c461695 (patch) | |
tree | 176f90c03f64c2645932dbaf3d87f2ad22b61489 /gui/components | |
parent | b8e0fe3b5003d22043042110e8036f4383602545 (diff) |
[ui] transient connecting state
Diffstat (limited to 'gui/components')
-rw-r--r-- | gui/components/Footer.qml | 121 | ||||
-rw-r--r-- | gui/components/Header.qml | 5 | ||||
-rw-r--r-- | gui/components/Home.qml | 4 | ||||
-rw-r--r-- | gui/components/Locations.qml | 284 | ||||
-rw-r--r-- | gui/components/MainView.qml | 42 | ||||
-rw-r--r-- | gui/components/Preferences.qml | 50 | ||||
-rw-r--r-- | gui/components/SignalIcon.qml | 62 | ||||
-rw-r--r-- | gui/components/Splash.qml | 15 | ||||
-rw-r--r-- | gui/components/StatusBox.qml | 9 | ||||
-rw-r--r-- | gui/components/ThemedPage.qml | 11 | ||||
-rw-r--r-- | gui/components/VPNState.qml | 55 | ||||
-rw-r--r-- | gui/components/WrappedRadioButton.qml | 21 |
12 files changed, 563 insertions, 116 deletions
diff --git a/gui/components/Footer.qml b/gui/components/Footer.qml index 2c7c875..7658fa1 100644 --- a/gui/components/Footer.qml +++ b/gui/components/Footer.qml @@ -2,10 +2,13 @@ import QtQuick 2.0 import QtQuick.Controls 2.4 import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.14 +import QtGraphicalEffects 1.0 + +import "../themes/themes.js" as Theme ToolBar { - Material.background: Material.backgroundColor + Material.background: Theme.bgColor Material.foreground: "black" Material.elevation: 0 visible: stackView.depth > 1 && ctx !== undefined ? false : true @@ -17,10 +20,13 @@ ToolBar { ToolButton { id: gwButton - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 10 - anchors.left: parent.left - anchors.verticalCenterOffset: 5 + visible: hasMultipleGateways() + anchors { + verticalCenter: parent.verticalCenter + leftMargin: 10 + left: parent.left + verticalCenterOffset: 5 + } icon.source: stackView.depth > 1 ? "" : "../resources/globe.svg" onClicked: stackView.push("Locations.qml") } @@ -30,7 +36,8 @@ ToolBar { anchors.left: gwButton.right anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 5 - text: "Seattle" + text: locationStr() + color: getLocationColor() } Item { @@ -40,6 +47,7 @@ ToolBar { Image { id: bridge + visible: isBridgeSelected() height: 24 width: 24 source: "../resources/bridge.png" @@ -58,6 +66,107 @@ ToolBar { anchors.rightMargin: 20 anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 5 + // TODO refactor with SignalIcon + ColorOverlay{ + anchors.fill: gwQuality + source: gwQuality + color: getSignalColor() + antialiasing: true + } + } + } + + function getSignalColor() { + if (ctx && ctx.status == "on") { + return "green" + } else { + return "black" + } + } + + StateGroup { + state: ctx ? ctx.status : "off" + states: [ + State { + name: "on" + PropertyChanges { + target: gwQuality + source: "../resources/reception-4.svg" + } + }, + State { + name: "off" + PropertyChanges { + target: gwQuality + source: "../resources/reception-0.svg" + } + } + ] + } + + function locationStr() { + if (ctx && ctx.status == "on") { + if (ctx.currentLocation && ctx.currentCountry) { + let s = ctx.currentLocation + ", " + ctx.currentCountry + if (root.selectedGateway == "auto") { + s = "🗲 " + s + } + return s + } + } + if (root.selectedGateway == "auto") { + if (ctx && ctx.locations && ctx.bestLocation) { + return "🗲 " + getCanonicalLocation(ctx.bestLocation) + } else { + return qsTr("Recommended") + } + } + if (ctx && ctx.locations && ctx.locationLabels) { + return getCanonicalLocation(root.selectedGateway) + } + } + + // returns the composite of Location, CC + function getCanonicalLocation(label) { + try { + let loc = ctx.locationLabels[label] + return loc[0] + ", " + loc[1] + } catch(e) { + return "unknown" + } + } + + function getLocationColor() { + if (ctx && ctx.status == "on") { + return "black" + } else { + // TODO darker gray + return "gray" + } + } + + function hasMultipleGateways() { + let provider = getSelectedProvider(providers) + if (provider == "riseup") { + return true + } else { + if (!ctx) { + return false + } + return ctx.locations.length > 0 + } + } + + function getSelectedProvider(providers) { + let obj = JSON.parse(providers.getJson()) + return obj['default'] + } + + function isBridgeSelected() { + if (ctx && ctx.transport == "obfs4") { + return true + } else { + return false } } } diff --git a/gui/components/Header.qml b/gui/components/Header.qml index 92f4bdd..6682a28 100644 --- a/gui/components/Header.qml +++ b/gui/components/Header.qml @@ -3,10 +3,12 @@ import QtQuick.Controls 2.4 import QtQuick.Dialogs 1.2 import QtQuick.Controls.Material 2.1 +import "../themes/themes.js" as Theme + ToolBar { visible: stackView.depth > 1 Material.foreground: Material.Black - Material.background: "#ffffff" + Material.background: Theme.bgColor Material.elevation: 0 contentHeight: settingsButton.implicitHeight @@ -27,6 +29,7 @@ ToolBar { Label { text: stackView.currentItem.title + font.bold: true anchors.centerIn: parent } } diff --git a/gui/components/Home.qml b/gui/components/Home.qml index c9eab2a..099bc23 100644 --- a/gui/components/Home.qml +++ b/gui/components/Home.qml @@ -3,6 +3,6 @@ import QtQuick.Controls 2.2 import QtGraphicalEffects 1.0 Page { - - StatusBox {} + StatusBox { + } } diff --git a/gui/components/Locations.qml b/gui/components/Locations.qml index d3e0f5a..955da26 100644 --- a/gui/components/Locations.qml +++ b/gui/components/Locations.qml @@ -1,62 +1,256 @@ import QtQuick 2.9 import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.14 +import QtGraphicalEffects 1.0 import "../themes/themes.js" as Theme -Page { +/* TODO + [ ] build a better gateway object list (location, user-friendly, country, bridge, etc) + [ ] ui: mark manual override icon somehow? + [ ] ui: auto radiobutton should use also the bridge icon + [ ] corner case: manual override, not full list yet + [ ] persist bridges + [ ] persist manual selection + [ ] display the location we know + [ ] corner case: user selects bridges with manual selection + (I think the backend should discard any manual selection when selecting bridges... + unless the current selection provides the bridge, in which case we can maintain it) + */ + +ThemedPage { + + id: locationPage title: qsTr("Select Location") - ListView { - id: gwList - focus: true - currentIndex: -1 - anchors.fill: parent - spacing: 1 + // TODO add ScrollIndicator + // https://doc.qt.io/qt-5.12//qml-qtquick-controls2-scrollindicator.html - delegate: ItemDelegate { - id: loc - Rectangle { - width: parent.width - height: 1 - color: Theme.borderColor - } - width: parent.width - text: model.text - highlighted: ListView.isCurrentItem - icon.color: "transparent" - icon.source: model.icon - onClicked: { - model.triggered() - stackView.pop() - } - MouseArea { - property var onMouseAreaClicked: function () { - parent.clicked() - } - id: mouseArea - anchors.fill: loc - cursorShape: Qt.PointingHandCursor - onReleased: { - onMouseAreaClicked() + //: this is in the radio button for the auto selection + property var autoSelectionLabel: qsTr("Automatically use best connection") + //: Location Selection: label for radio buttons that selects manually + property var manualSelectionLabel: qsTr("Manually select") + //: A little display to signal that the clicked gateway is being switched to + property var switchingLocationLabel: qsTr("Switching gateways...") + //: Subtitle to explain that only bridge locations are shown in the selector + property var onlyBridgesWarning: qsTr("Only locations with bridges") + + property bool switching: false + + ButtonGroup { + id: locsel + } + + Rectangle { + id: autoBox + width: root.width * 0.90 + height: 100 + radius: 10 + color: "white" + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + margins: 10 + } + Rectangle { + anchors { + fill: parent + margins: 10 + } + Label { + id: recommendedLabel + //: Location Selection: label for radio button that selects automatically + text: qsTr("Recommended") + font.bold: true + } + WrappedRadioButton { + id: autoRadioButton + anchors.top: recommendedLabel.bottom + text: getAutoLabel() + ButtonGroup.group: locsel + checked: false + onClicked: { + root.selectedGateway = "auto" + console.debug("Selected gateway: auto") + backend.useAutomaticGateway() } } } + } - model: ListModel { - ListElement { - text: qsTr("Paris") - triggered: function () {} - icon: "../resources/reception-4.svg" + Rectangle { + id: manualBox + visible: root.locationsModel.length > 0 + width: root.width * 0.90 + height: getManualBoxHeight() + radius: 10 + color: Theme.fgColor + anchors { + horizontalCenter: parent.horizontalCenter + top: autoBox.bottom + margins: 10 + } + Rectangle { + anchors { + fill: parent + margins: 10 + } + Label { + id: manualLabel + text: manualSelectionLabel + font.bold: true + } + Label { + id: bridgeWarning + text: onlyBridgesWarning + color: "gray" + visible: isBridgeSelected() + wrapMode: Text.Wrap + anchors { + topMargin: 5 + top: manualLabel.bottom + } + font.pixelSize: Theme.fontSize - 3 } - ListElement { - text: qsTr("Montreal") - triggered: function () {} - icon: "../resources/reception-4.svg" + ColumnLayout { + id: gatewayListColumn + width: parent.width + spacing: 1 + anchors.top: getManualAnchor() + + Repeater { + id: gwManualSelectorList + width: parent.width + model: root.locationsModel + + RowLayout { + width: parent.width + WrappedRadioButton { + text: getLocationLabel(modelData) + location: modelData + ButtonGroup.group: locsel + checked: false + enabled: locationPage.switching ? false : true + onClicked: { + if (ctx.status == "on") { + locationPage.switching = true + } + root.selectedGateway = location + backend.useLocation(location) + } + } + Item { + Layout.fillWidth: true + } + Image { + height: 16 + width: 16 + visible: isBridgeSelected() + source: "../resources/bridge.png" + Layout.alignment: Qt.AlignRight + Layout.rightMargin: 10 + } + SignalIcon { + // TODO mocked! + quality: getSignalFor(modelData) + Layout.alignment: Qt.AlignRight + Layout.rightMargin: 20 + } + } + } } - ListElement { - text: qsTr("Seattle") - triggered: function () {} - icon: "../resources/reception-2.svg" + } + } + + StateGroup { + states: [ + State { + when: locationPage.switching && ctx.status != "on" + PropertyChanges { + target: manualLabel + text: switchingLocationLabel + } + }, + State { + when: ctx && ctx.status == "on" + PropertyChanges { + target: manualLabel + text: manualSelectionLabel + } + StateChangeScript { + script: { + locationPage.switching = false + } + } + } + ] + } + + function getAutoLabel() { + let l = autoSelectionLabel + if (ctx && ctx.locations && ctx.bestLocation) { + let best = ctx.locationLabels[ctx.bestLocation] + let label = best[0] + ", " + best[1] + l += " (" + label + ")" + } + return l + } + + function getLocationLabel(location) { + if (!ctx) { return ""} + let l = ctx.locationLabels[location] + return l[0] + ", " + l[1] + } + + function getManualBoxHeight() { + let h = gatewayListColumn.height + manualLabel.height + if (bridgeWarning.visible) { + h += bridgeWarning.height + } + return h + 15 + } + + function getSignalFor(location) { + switch(location) { + case "amsterdam": + case "paris": + return "good" + case "newyork": + return "medium" + case "montreal": + return "medium" + default: + return "low" + } + } + + function isBridgeSelected() { + if (ctx && ctx.transport == "obfs4") { + return true + } else { + return false + } + } + + function getManualAnchor() { + if (isBridgeSelected()) { + return bridgeWarning.bottom + } else { + return manualLabel.bottom + } + } + + Component.onCompleted: { + if (root.selectedGateway == "auto") { + autoRadioButton.checked = true; + } else { + let match = false + for (var i=1; i<locsel.buttons.length; i++) { + let b = locsel.buttons[i] + if (b.location == root.selectedGateway) { + match = true; + b.checked = true; + } } } } diff --git a/gui/components/MainView.qml b/gui/components/MainView.qml index 1918c7f..5407178 100644 --- a/gui/components/MainView.qml +++ b/gui/components/MainView.qml @@ -82,44 +82,12 @@ Page { } } - Drawer { - id: locationsDrawer - - width: root.width - height: root.height - - ListView { - focus: true - currentIndex: -1 - anchors.fill: parent - - delegate: ItemDelegate { - width: parent.width - text: model.text - highlighted: ListView.isCurrentItem - onClicked: { - locationsDrawer.close() - model.triggered() - } - } - - model: ListModel { - ListElement { - text: qsTr("Montreal, CA") - triggered: function () {} - } - ListElement { - text: qsTr("Paris, FR") - triggered: function () {} - } - } - - ScrollIndicator.vertical: ScrollIndicator {} - } + header: Header { + id: header + } + footer: Footer { + id: footer } - - header: Header {} - footer: Footer {} Dialog { id: aboutDialog diff --git a/gui/components/Preferences.qml b/gui/components/Preferences.qml index 481444a..5c708a3 100644 --- a/gui/components/Preferences.qml +++ b/gui/components/Preferences.qml @@ -2,15 +2,17 @@ import QtQuick 2.9 import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.1 -Page { +import "../themes/themes.js" as Theme + +ThemedPage { title: qsTr("Preferences") Column { id: prefCol // FIXME the checkboxes seem to have a bigger lineHeight themselves, need to pack more. spacing: 1 - topPadding: root.width * 0.1 - leftPadding: root.width * 0.15 + topPadding: root.width * 0.05 + leftPadding: root.width * 0.1 rightPadding: root.width * 0.15 Rectangle { @@ -18,6 +20,7 @@ Page { visible: false height: 40 width: 300 + color: Theme.bgColor anchors.horizontalCenter: parent.horizontalCenter @@ -38,12 +41,20 @@ Page { id: useBridgesCheckBox checked: false text: qsTr("Use obfs4 bridges") + onClicked: { + // TODO there's a corner case that needs to be dealt with in the backend, + // if an user has a manual location selected and switches to bridges: + // we need to fallback to "auto" selection if such location does not + // offer bridges + useBridges(checked) + } } CheckBox { id: useSnowflake - checked: false text: qsTr("Use Snowflake (experimental)") + enabled: false + checked: false } Label { @@ -53,8 +64,9 @@ Page { CheckBox { id: useUDP - checked: false text: qsTr("UDP") + enabled: false + checked: false } } @@ -69,11 +81,11 @@ Page { } PropertyChanges { target: useBridgesCheckBox - checkable: false + enabled: false } PropertyChanges { target: useUDP - checkable: false + enabled: false } }, State { @@ -84,11 +96,11 @@ Page { } PropertyChanges { target: useBridgesCheckBox - checkable: false + enabled: false } PropertyChanges { target: useUDP - checkable: false + enabled: false } }, State { @@ -99,13 +111,29 @@ Page { } PropertyChanges { target: useBridgesCheckBox - checkable: true + enabled: true } PropertyChanges { target: useUDP - checkable: true + enabled: true } } ] } + + function useBridges(value) { + if (value == true) { + console.debug("use obfs4") + backend.setTransport("obfs4") + } else { + console.debug("use regular") + backend.setTransport("openvpn") + } + } + + Component.onCompleted: { + if (ctx && ctx.transport == "obfs4") { + useBridgesCheckBox.checked = true + } + } } diff --git a/gui/components/SignalIcon.qml b/gui/components/SignalIcon.qml new file mode 100644 index 0000000..63bde5c --- /dev/null +++ b/gui/components/SignalIcon.qml @@ -0,0 +1,62 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.14 +import QtGraphicalEffects 1.0 + +import "../themes/themes.js" as Theme + +Image { + id: icon + height: 16 + width: 16 + // one of: good, medium, low + property var quality: "good" + + ColorOverlay{ + anchors.fill: icon + source: icon + color: getQualityColor() + antialiasing: true + } + + StateGroup { + state: quality + states: [ + State { + name: "good" + PropertyChanges { + target: icon + source: "../resources/reception-4.svg" + } + }, + State { + name: "medium" + PropertyChanges { + target: icon + source: "../resources/reception-2.svg" + } + }, + State { + name: "low" + PropertyChanges { + target: icon + source: "../resources/reception-0.svg" + } + } + ] + } + + function getQualityColor() { + // I like this better than with states + switch (quality) { + case "good": + return Theme.signalGood + case "medium": + return Theme.signalMedium + case "low": + return Theme.signalLow + default: + return Theme.signalGood + } + } +} diff --git a/gui/components/Splash.qml b/gui/components/Splash.qml index 6bdd3ab..15acf48 100644 --- a/gui/components/Splash.qml +++ b/gui/components/Splash.qml @@ -4,7 +4,8 @@ import QtGraphicalEffects 1.0 Page { id: splash - property int timeoutInterval: 1600 + property int timeoutInterval: 200 + //property int timeoutInterval: 1600 property alias errors: splashErrorBox Column { @@ -40,7 +41,7 @@ Page { function delay(delayTime, cb) { splashTimer.interval = delayTime - splashTimer.repeat = false + splashTimer.repeat = true splashTimer.triggered.connect(cb) splashTimer.start() } @@ -50,9 +51,13 @@ Page { return } if (ctx && ctx.isReady) { + splashTimer.stop() loader.source = "MainView.qml" } else { - delay(100, loadMainViewWhenReady) + if (!splashTimer.running) { + console.debug('delay...') + delay(500, loadMainViewWhenReady) + } } } @@ -65,7 +70,5 @@ Page { } } - Component.onCompleted: { - - } + Component.onCompleted: {} } diff --git a/gui/components/StatusBox.qml b/gui/components/StatusBox.qml index a20b930..a3a5c18 100644 --- a/gui/components/StatusBox.qml +++ b/gui/components/StatusBox.qml @@ -7,6 +7,7 @@ import QtQuick.Templates 2.12 as T import QtQuick.Controls.impl 2.12 import QtQuick.Controls.Material 2.12 import QtQuick.Controls.Material.impl 2.12 + import "../themes/themes.js" as Theme Item { @@ -18,10 +19,15 @@ Item { } Rectangle { + color: Theme.bgColor + anchors.fill: parent + } + + Rectangle { id: statusBoxBackground + color: Theme.fgColor height: 300 radius: 10 - color: Theme.bgColor antialiasing: true anchors { fill: parent @@ -124,6 +130,7 @@ Item { if (vpn.state === "on") { backend.switchOff() } else if (vpn.state === "off") { + vpn.startingUI = true backend.switchOn() } else { console.debug("unknown state") diff --git a/gui/components/ThemedPage.qml b/gui/components/ThemedPage.qml new file mode 100644 index 0000000..f7ee647 --- /dev/null +++ b/gui/components/ThemedPage.qml @@ -0,0 +1,11 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 + +import "../themes/themes.js" as Theme + +Page { + Rectangle { + color: Theme.bgColor + anchors.fill: parent + } +} diff --git a/gui/components/VPNState.qml b/gui/components/VPNState.qml index 9d443ce..5e659a9 100644 --- a/gui/components/VPNState.qml +++ b/gui/components/VPNState.qml @@ -11,17 +11,49 @@ StateGroup { property var stopping: "stopping" property var failed: "failed" - state: ctx ? ctx.status : vpnStates.off + property bool startingUI: false + + state: ctx ? ctx.status : off states: [ State { name: initializing }, State { - name: off + when: ctx && ctx.status == "off" && startingUI == true + PropertyChanges { + target: connectionState + text: qsTr("Connecting") + } + PropertyChanges { + target: statusBoxBackground + border.color: Theme.accentConnecting + } + PropertyChanges { + target: connectionImage + source: "../resources/birds.svg" + anchors.horizontalCenter: parent.horizontalCenter + } + PropertyChanges { + target: toggleVPN + enabled: false + text: ("...") + } + PropertyChanges { + target: systray + tooltip: toHuman("connecting") + icon.source: icons["wait"] + } + PropertyChanges { + target: systray.statusItem + text: toHuman("connecting") + } + }, + State { + name: "off" PropertyChanges { target: connectionState - text: qsTr("Connection\nUnsecured") + text: qsTr("Unsecured\nConnection") } PropertyChanges { target: statusBoxBackground @@ -33,6 +65,7 @@ StateGroup { } PropertyChanges { target: toggleVPN + enabled: true text: qsTr("Turn on") } PropertyChanges { @@ -44,14 +77,16 @@ StateGroup { text: toHuman("off") } StateChangeScript { - script: {} + script: { + console.debug("status off") + } } }, State { name: on PropertyChanges { target: connectionState - text: qsTr("Connection\nSecured") + text: qsTr("Secured\nConnection") } PropertyChanges { target: statusBoxBackground @@ -63,6 +98,7 @@ StateGroup { } PropertyChanges { target: toggleVPN + enabled: true text: qsTr("Turn off") } PropertyChanges { @@ -75,7 +111,9 @@ StateGroup { text: toHuman("on") } StateChangeScript { - script: {} + script: { + vpn.startingUI = false + } } }, State { @@ -95,6 +133,7 @@ StateGroup { } PropertyChanges { target: toggleVPN + enabled: true text: qsTr("Cancel") } PropertyChanges { @@ -107,7 +146,9 @@ StateGroup { text: toHuman("connecting") } StateChangeScript { - script: {} + script: { + vpn.startingUI = false + } } }, State { diff --git a/gui/components/WrappedRadioButton.qml b/gui/components/WrappedRadioButton.qml new file mode 100644 index 0000000..04643b1 --- /dev/null +++ b/gui/components/WrappedRadioButton.qml @@ -0,0 +1,21 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.12 +import QtQuick.Controls.Material.impl 2.12 + +import "../themes/themes.js" as Theme + +RadioButton { + id: control + width: parent.width + property var location + + contentItem: Label { + text: control.text + font: control.font + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + leftPadding: control.indicator.width + control.spacing + wrapMode: Label.Wrap + } +} |