summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gui/components/Footer.qml121
-rw-r--r--gui/components/Header.qml5
-rw-r--r--gui/components/Home.qml4
-rw-r--r--gui/components/Locations.qml284
-rw-r--r--gui/components/MainView.qml42
-rw-r--r--gui/components/Preferences.qml50
-rw-r--r--gui/components/SignalIcon.qml62
-rw-r--r--gui/components/Splash.qml15
-rw-r--r--gui/components/StatusBox.qml9
-rw-r--r--gui/components/ThemedPage.qml11
-rw-r--r--gui/components/VPNState.qml55
-rw-r--r--gui/components/WrappedRadioButton.qml21
-rw-r--r--gui/gui.qrc3
-rw-r--r--gui/main.qml14
-rw-r--r--gui/themes/themes.js7
-rw-r--r--pkg/backend/status.go42
-rw-r--r--pkg/bitmask/bitmask.go2
-rw-r--r--pkg/vpn/bonafide/bonafide.go11
-rw-r--r--pkg/vpn/bonafide/gateways.go27
-rw-r--r--pkg/vpn/openvpn.go4
-rw-r--r--pkg/vpn/status.go4
21 files changed, 655 insertions, 138 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
+ }
+}
diff --git a/gui/gui.qrc b/gui/gui.qrc
index c794ef1..7c0a418 100644
--- a/gui/gui.qrc
+++ b/gui/gui.qrc
@@ -6,15 +6,18 @@
<!-- gui components -->
<file>themes/themes.js</file>
<file>components/MainView.qml</file>
+ <file>components/ThemedPage.qml</file>
<file>components/Splash.qml</file>
<file>components/Home.qml</file>
<file>components/Header.qml</file>
<file>components/Footer.qml</file>
+ <file>components/WrappedRadioButton.qml</file>
<file>components/StatusBox.qml</file>
<file>components/Spinner.qml</file>
<file>components/Systray.qml</file>
<file>components/Help.qml</file>
<file>components/Locations.qml</file>
+ <file>components/SignalIcon.qml</file>
<file>components/Preferences.qml</file>
<file>components/BoldLabel.qml</file>
<file>components/LightLabel.qml</file>
diff --git a/gui/main.qml b/gui/main.qml
index f847377..515330a 100644
--- a/gui/main.qml
+++ b/gui/main.qml
@@ -9,9 +9,9 @@
- [x] font: monserrat
- [x] nested states
- [x] splash init errors
- - [ ] minimize/hide from systray
- - [ ] gateway selector
+ - [.] gateway selector
- [ ] bridges
+ - [ ] minimize/hide from systray
- [ ] control actions from systray
- [ ] add gateway to systray
- [ ] donation dialog
@@ -43,8 +43,13 @@ ApplicationWindow {
property var ctx
property var error: ""
+
+ // TODO can move properties to some state sub-item to unclutter
property bool isDonationService: false
property bool showDonationReminder: false
+ property var locationsModel: []
+ // TODO get from persistance
+ property var selectedGateway: "auto"
property var icons: {
"off": "qrc:/assets/icon/png/white/vpn_off.png",
@@ -84,6 +89,9 @@ ApplicationWindow {
console.debug(j)
}
ctx = JSON.parse(j)
+ if (ctx != undefined) {
+ locationsModel = Object.keys(ctx.locations)
+ }
if (ctx.errors) {
console.debug("errors, setting root.error")
root.error = ctx.errors
@@ -96,7 +104,7 @@ ApplicationWindow {
if (ctx.donateDialog == 'true') {
showDonationReminder = true;
}
- //gwSelector.model = Object.keys(ctx.locations)
+
// TODO check donation
//if (needsDonate && !shownDonate) {
// donate.visible = true;
diff --git a/gui/themes/themes.js b/gui/themes/themes.js
index 265af9a..c04ef24 100644
--- a/gui/themes/themes.js
+++ b/gui/themes/themes.js
@@ -48,4 +48,9 @@ const blueButton = {
"focusBorder": blueFocusBorder,
};
-const bgColor = "white";
+const bgColor = "#f3f3f3";
+const fgColor = "#ffffff";
+
+const signalGood = "green";
+const signalMedium = "orange";
+const signalLow = "red";
diff --git a/pkg/backend/status.go b/pkg/backend/status.go
index 0ffd853..c5f79d1 100644
--- a/pkg/backend/status.go
+++ b/pkg/backend/status.go
@@ -32,24 +32,27 @@ var updateMutex sync.Mutex
// them.
type connectionCtx struct {
- AppName string `json:"appName"`
- Provider string `json:"provider"`
- TosURL string `json:"tosURL"`
- HelpURL string `json:"helpURL"`
- AskForDonations bool `json:"askForDonations"`
- DonateDialog bool `json:"donateDialog"`
- DonateURL string `json:"donateURL"`
- LoginDialog bool `json:"loginDialog"`
- LoginOk bool `json:"loginOk"`
- Version string `json:"version"`
- Errors string `json:"errors"`
- Status status `json:"status"`
- Locations map[string]float64 `json:"locations"`
- CurrentGateway string `json:"currentGateway"`
- CurrentLocation string `json:"currentLocation"`
- CurrentCountry string `json:"currentCountry"`
- ManualLocation bool `json:"manualLocation"`
- IsReady bool `json:"isReady"`
+ AppName string `json:"appName"`
+ Provider string `json:"provider"`
+ TosURL string `json:"tosURL"`
+ HelpURL string `json:"helpURL"`
+ AskForDonations bool `json:"askForDonations"`
+ DonateDialog bool `json:"donateDialog"`
+ DonateURL string `json:"donateURL"`
+ LoginDialog bool `json:"loginDialog"`
+ LoginOk bool `json:"loginOk"`
+ Version string `json:"version"`
+ Errors string `json:"errors"`
+ Status status `json:"status"`
+ Locations map[string]float64 `json:"locations"`
+ LocationLabels map[string][]string `json:"locationLabels"`
+ CurrentGateway string `json:"currentGateway"`
+ CurrentLocation string `json:"currentLocation"`
+ CurrentCountry string `json:"currentCountry"`
+ BestLocation string `json:"bestLocation"`
+ Transport string `json:"transport"`
+ ManualLocation bool `json:"manualLocation"`
+ IsReady bool `json:"isReady"`
bm bitmask.Bitmask
autostart bitmask.Autostart
cfg *config.Config
@@ -60,9 +63,12 @@ func (c *connectionCtx) toJson() ([]byte, error) {
if c.bm != nil {
transport := c.bm.GetTransport()
c.Locations = c.bm.ListLocationFullness(transport)
+ c.LocationLabels = c.bm.ListLocationLabels(transport)
c.CurrentGateway = c.bm.GetCurrentGateway()
c.CurrentLocation = c.bm.GetCurrentLocation()
c.CurrentCountry = c.bm.GetCurrentCountry()
+ c.BestLocation = c.bm.GetBestLocation(transport)
+ c.Transport = transport
c.ManualLocation = c.bm.IsManualLocation()
}
defer statusMutex.Unlock()
diff --git a/pkg/bitmask/bitmask.go b/pkg/bitmask/bitmask.go
index 364312e..1d7217c 100644
--- a/pkg/bitmask/bitmask.go
+++ b/pkg/bitmask/bitmask.go
@@ -28,6 +28,8 @@ type Bitmask interface {
InstallHelpers() error
VPNCheck() (helpers bool, priviledge bool, err error)
ListLocationFullness(protocol string) map[string]float64
+ ListLocationLabels(protocol string) map[string][]string
+ GetBestLocation(protocol string) string
UseGateway(name string)
UseAutomaticGateway()
GetTransport() string
diff --git a/pkg/vpn/bonafide/bonafide.go b/pkg/vpn/bonafide/bonafide.go
index a9a7d85..cff5fc2 100644
--- a/pkg/vpn/bonafide/bonafide.go
+++ b/pkg/vpn/bonafide/bonafide.go
@@ -278,6 +278,10 @@ func (b *Bonafide) ListLocationFullness(transport string) map[string]float64 {
return b.gateways.listLocationFullness(transport)
}
+func (b *Bonafide) ListLocationLabels(transport string) map[string][]string {
+ return b.gateways.listLocationLabels(transport)
+}
+
func (b *Bonafide) SetManualGateway(label string) {
b.gateways.setUserChoice(label)
}
@@ -286,6 +290,13 @@ func (b *Bonafide) SetAutomaticGateway() {
b.gateways.setAutomaticChoice()
}
+func (b *Bonafide) GetBestLocation(transport string) string {
+ if b.gateways == nil {
+ return ""
+ }
+ return b.gateways.getBestLocation(transport, b.tzOffsetHours)
+}
+
func (b *Bonafide) IsManualLocation() bool {
if b.gateways == nil {
return false
diff --git a/pkg/vpn/bonafide/gateways.go b/pkg/vpn/bonafide/gateways.go
index 53ab320..4299bb2 100644
--- a/pkg/vpn/bonafide/gateways.go
+++ b/pkg/vpn/bonafide/gateways.go
@@ -113,6 +113,20 @@ func (p *gatewayPool) listLocationFullness(transport string) map[string]float64
return cm
}
+/* returns a map of location: labels for the ui to use */
+func (p *gatewayPool) listLocationLabels(transport string) map[string][]string {
+ cm := make(map[string][]string)
+ locations := p.getLocations()
+ if len(locations) == 0 {
+ return cm
+ }
+ for _, loc := range locations {
+ current := p.locations[loc]
+ cm[loc] = []string{current.Name, current.CountryCode}
+ }
+ return cm
+}
+
/* this method should only be used if we have no usable menshen list. */
func (p *gatewayPool) getRandomGatewaysByLocation(location, transport string) ([]Gateway, error) {
if !p.isValidLocation(location) {
@@ -274,6 +288,19 @@ func (p *gatewayPool) getBest(transport string, tz, max int) ([]Gateway, error)
}
}
+/* returns the location for the first recommended gateway */
+func (p *gatewayPool) getBestLocation(transport string, tz int) string {
+ best, err := p.getBest(transport, tz, 1)
+ if err != nil {
+ return ""
+ }
+ if len(best) != 1 {
+ return ""
+ }
+ return best[0].Location
+
+}
+
func (p *gatewayPool) getAll(transport string, tz int) ([]Gateway, error) {
if len(p.recommended) != 0 {
return p.getGatewaysFromMenshen(transport, 999)
diff --git a/pkg/vpn/openvpn.go b/pkg/vpn/openvpn.go
index b15530b..fe10b69 100644
--- a/pkg/vpn/openvpn.go
+++ b/pkg/vpn/openvpn.go
@@ -326,6 +326,10 @@ func (b *Bitmask) ListLocationFullness(transport string) map[string]float64 {
return b.bonafide.ListLocationFullness(transport)
}
+func (b *Bitmask) ListLocationLabels(transport string) map[string][]string {
+ return b.bonafide.ListLocationLabels(transport)
+}
+
// UseGateway selects a gateway, by label, as the default gateway
func (b *Bitmask) UseGateway(label string) {
b.bonafide.SetManualGateway(label)
diff --git a/pkg/vpn/status.go b/pkg/vpn/status.go
index 0b04c3b..88735e6 100644
--- a/pkg/vpn/status.go
+++ b/pkg/vpn/status.go
@@ -103,6 +103,10 @@ func (b *Bitmask) GetCurrentCountry() string {
return b.onGateway.CountryCode
}
+func (b *Bitmask) GetBestLocation(transport string) string {
+ return b.bonafide.GetBestLocation(transport)
+}
+
func (b *Bitmask) IsManualLocation() bool {
return b.bonafide.IsManualLocation()
}