diff options
-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 | ||||
-rw-r--r-- | gui/gui.qrc | 3 | ||||
-rw-r--r-- | gui/main.qml | 14 | ||||
-rw-r--r-- | gui/themes/themes.js | 7 | ||||
-rw-r--r-- | pkg/backend/status.go | 42 | ||||
-rw-r--r-- | pkg/bitmask/bitmask.go | 2 | ||||
-rw-r--r-- | pkg/vpn/bonafide/bonafide.go | 11 | ||||
-rw-r--r-- | pkg/vpn/bonafide/gateways.go | 27 | ||||
-rw-r--r-- | pkg/vpn/openvpn.go | 4 | ||||
-rw-r--r-- | pkg/vpn/status.go | 4 |
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() } |